1########################################################################
2##
3## Copyright (C) 2013-2021 The Octave Project Developers
4##
5## See the file COPYRIGHT.md in the top-level directory of this
6## distribution or <https://octave.org/copyright/>.
7##
8## This file is part of Octave.
9##
10## Octave is free software: you can redistribute it and/or modify it
11## under the terms of the GNU General Public License as published by
12## the Free Software Foundation, either version 3 of the License, or
13## (at your option) any later version.
14##
15## Octave is distributed in the hope that it will be useful, but
16## WITHOUT ANY WARRANTY; without even the implied warranty of
17## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18## GNU General Public License for more details.
19##
20## You should have received a copy of the GNU General Public License
21## along with Octave; see the file COPYING.  If not, see
22## <https://www.gnu.org/licenses/>.
23##
24########################################################################
25
26## -*- texinfo -*-
27## @deftypefn  {} {} imformats ()
28## @deftypefnx {} {@var{formats} =} imformats (@var{ext})
29## @deftypefnx {} {@var{formats} =} imformats (@var{format})
30## @deftypefnx {} {@var{formats} =} imformats ("add", @var{format})
31## @deftypefnx {} {@var{formats} =} imformats ("remove", @var{ext})
32## @deftypefnx {} {@var{formats} =} imformats ("update", @var{ext}, @var{format})
33## @deftypefnx {} {@var{formats} =} imformats ("factory")
34## Manage supported image formats.
35##
36## @var{formats} is a structure with information about each supported file
37## format, or from a specific format @var{ext}, the value displayed on the
38## field @var{ext}.  It contains the following fields:
39##
40## @table @asis
41## @item ext
42## The name of the file format.  This may match the file extension but Octave
43## will automatically detect the file format.
44##
45## @item description
46## A long description of the file format.
47##
48## @item @nospell{isa}
49## A function handle to confirm if a file is of the specified format.
50##
51## @item write
52## A function handle to write if a file is of the specified format.
53##
54## @item read
55## A function handle to open files the specified format.
56##
57## @item info
58## A function handle to obtain image information of the specified format.
59##
60## @item alpha
61## Logical value if format supports alpha channel (transparency or matte).
62##
63## @item multipage
64## Logical value if format supports multipage (multiple images per file).
65## @end table
66##
67## It is possible to change the way Octave manages file formats with the
68## options @qcode{"add"}, @qcode{"remove"}, and @qcode{"update"}, and supplying
69## a structure @var{format} with the required fields.  The option
70## @qcode{"factory"} resets the configuration to the default.
71##
72## This can be used by Octave packages to extend the image reading capabilities
73## Octave, through use of the PKG_ADD and PKG_DEL commands.
74##
75## @seealso{imfinfo, imread, imwrite}
76## @end deftypefn
77
78function varargout = imformats (arg1, arg2, arg3)
79
80  if (nargin > 3)
81    print_usage ();
82  endif
83
84  mlock (); # prevent formats to be removed by "clear all"
85  persistent formats = default_formats ();
86
87  if (nargin == 0 && nargout == 0)
88    pretty_print_formats (formats);
89  elseif (nargin >= 1)
90    if (isstruct (arg1))
91      arrayfun (@is_valid_format, arg1);
92      ## FIXME: what is the return value in this situation?
93      formats = arg1;
94
95    elseif (ischar (arg1))
96      switch (tolower (arg1))
97        case "add",
98          if (! isstruct (arg2))
99            error ("imformats: FORMAT to %s must be a structure.", arg1);
100          endif
101          arrayfun (@is_valid_format, arg2);
102          formats(end + 1: end + numel (arg2)) = arg2;
103          varargout{1} = formats;
104
105        case {"remove", "update"},
106          if (! ischar (arg2))
107            error ("imformats: EXT to %s must be a string.", arg1);
108          endif
109          ## FIXME: suppose a format with multiple extensions.  If one of
110          ##        them is requested to be removed, should we remove the
111          ##        whole format, or just that extension from the format?
112          match = find_ext_idx (formats, arg2);
113          if (! any (match))
114            error ("imformats: no EXT '%s' found.", arg2);
115          endif
116          if (strcmpi (arg1, "remove"))
117            formats(match) = [];
118          else
119            ## then it's update
120            if (! isstruct (arg3))
121              error ("imformats: FORMAT to update must be a structure.");
122            endif
123            is_valid_format (arg3);
124            formats(match) = arg3;
125          endif
126          varargout{1} = formats;
127
128        case "factory",
129          formats = default_formats ();
130        otherwise
131          ## then we look for a format with that extension.
132          match = find_ext_idx (formats, arg1);
133          ## For matlab compatibility, if we don't find any format we must
134          ## return an empty struct with NO fields.  We can't use match as mask
135          if (any (match))
136            varargout{1} = formats(match);
137          else
138            varargout{1} = struct ();
139          endif
140      endswitch
141    else
142      error ("imformats: first argument must be either a structure or string.");
143    endif
144  else
145    varargout{1} = formats;
146  endif
147
148endfunction
149
150function rformats = default_formats ()
151
152  ## The available formats are dependent on what the user has installed at
153  ## a given time, and how GraphicsMagick was built.  Checking for
154  ## GraphicsMagick features when building Octave is not enough since it
155  ## delegates some of them to external programs which can be removed or
156  ## installed at any time.
157  ## The recommended method would be to use CoderInfoList() to get a list of
158  ## all available coders and try to write and read back a small test image.
159  ## But this will not work since some coders are readable or writable only.
160  ## It will still fail if we test only the ones marked as readable and
161  ## writable because some RW coders are not of image formats (NULL, 8BIM,
162  ## or EXIF for example).
163  ## So we'd need a blacklist (unacceptable because a 'bad' coder may be
164  ## added later) or a whitelist.  A whitelist means that even with a
165  ## super-fancy recent build of GraphicsMagick, some formats won't be listed
166  ## by imformats but in truth, we will still be able to read and write them
167  ## since imread() and imwrite() will give it a try anyway.
168  ##
169  ## For more info and comments from the GraphicsMagick main developer, see
170  ## http://sourceforge.net/mailarchive/forum.php?thread_name=alpine.GSO.2.01.1304301916050.2267%40freddy.simplesystems.org&forum_name=graphicsmagick-help
171
172  persistent formats = struct ( "coder", {},
173                                "ext", {},
174                                "isa", {},
175                                "info", {},
176                                "read", {},
177                                "write", {},
178                                "alpha", {},
179                                "description", {},
180                                "multipage", {});
181
182  ## Image IO abilities won't change during the same Octave session,
183  ## there's no need to go and calculate it all over again if we are
184  ## requested to reset back to factory.
185  if (! isempty (formats))
186    rformats = formats;
187    return;
188  endif
189
190  ##      Building the formats info
191  ##
192  ## As mentioned above we start with a whitelist of coders.  Since the
193  ## GraphicsMagick build may be missing some coders, we will remove those
194  ## from the list.  Some info can be obtained directly from GraphicsMagick
195  ## through the CoderInfo object.  However, some will need to be hardcoded.
196  ##
197  ## The association between file extensions and coders needs to be done
198  ## with a manually coded list (file extensions do not define the image
199  ## format and GraphicsMagick will not be fooled by changing the extension).
200  ##
201  ## We can get the read, write, description and multipage fields from
202  ## CoderInfo in C++.  We should do the same for alpha (GraphicsMagick
203  ## calls it matte) but it's not available from CoderInfo.  The only way to
204  ## check it is to create a sample image with each coder, then try to read
205  ## it back with GraphicsMagick and use the matte method on the Image class.
206  ## But making such test for each Octave session... meh! While technically
207  ## it may be possible that the same coder has different support for alpha
208  ## channel in different versions and builds, this doesn't seem to happen.
209  ## So we also hardcode those.  In the future, maybe the CoderInfo class will
210  ## have a matte method like it does for multipage.
211  ##
212  ## Other notes: some formats have more than one coder that do the same.  For
213  ## example, for jpeg images there is both the JPG and JPEG coders.  However,
214  ## it seems that when reading images, GraphicsMagick only uses one of them
215  ## and that's the one we list (it's the one reported by imfinfo and that we
216  ## can use for isa).  However, in some cases GraphicsMagick seems to rely
217  ## uniquely on the file extension (JBIG and JBG at least.  Create an image
218  ## with each of those coders, swap their extension and it will report the
219  ## other coder).  We don't have such cases on the whitelist but if we did, we
220  ## would need two entries for such cases.
221
222  ## each row: 1st => Coder, 2nd=> file extensions, 3rd=> alpha
223  coders = {"BMP",  {"bmp"},          true;
224            "CUR",  {"cur"},          false;
225            "GIF",  {"gif"},          true;
226            "ICO",  {"ico"},          true;
227            "JBG",  {"jbg"},          false;
228            "JBIG", {"jbig"},         false;
229            "JP2",  {"jp2", "jpx"},   true;
230            "JPEG", {"jpg", "jpeg"},  false; # there is also a JPG coder
231            "PBM",  {"pbm"},          false;
232            "PCX",  {"pcx"},          true;
233            "PGM",  {"pgm"},          false;
234            "PNG",  {"png"},          true;
235            ## PNM is a family of formats supporting portable bitmaps (PBM),
236            ## graymaps (PGM), and pixmaps (PPM).  There is no file format
237            ## associated with pnm itself.  If PNM is used as the output format
238            ## specifier, then GraphicsMagick automatically selects the most
239            ## appropriate format to represent the image.
240            "PNM",  {"pnm"},          true;
241            "PPM",  {"ppm"},          false;
242            "SUN",  {"ras"},          true; # SUN Rasterfile
243            "TGA",  {"tga", "tpic"},  true;
244            "TIFF", {"tif", "tiff"},  true;
245            "XBM",  {"xbm"},          false;
246            "XPM",  {"xpm"},          true;
247            "XWD",  {"xwd"},          false;
248            };
249
250  for fidx = 1: rows(coders)
251    formats(fidx).coder = coders{fidx, 1};
252    formats(fidx).ext   = coders{fidx, 2};
253    formats(fidx).alpha = coders{fidx, 3};
254    ## default isa is to check if the format returned by imfinfo is the coder
255    formats(fidx).isa   = @(x) isa_magick (coders{fidx,1}, x);
256  endfor
257
258  ## the default info, read, and write functions
259  [formats.info ] = deal (@__imfinfo__);
260  [formats.read ] = deal (@__imread__);
261  [formats.write] = deal (@__imwrite__);
262
263  ## fills rest of format information by checking with GraphicsMagick
264  formats = __magick_formats__ (formats);
265
266  rformats = formats;
267
268endfunction
269
270function is_valid_format (format)
271  ## the minimal list of fields required in the structure.  We don't
272  ## require multipage because it doesn't exist in matlab
273  min_fields  = {"ext", "read", "isa", "write", "info", "alpha", "description"};
274  fields_mask = isfield (format, min_fields);
275  if (! all (fields_mask))
276    error ("imformats: structure has missing field '%s'.", min_fields(! fields_mask){1});
277  endif
278
279endfunction
280
281function match = find_ext_idx (formats, ext)
282  ## FIXME: what should we do if there's more than one hit?
283  ##        Should this function prevent the addition of
284  ##        duplicated extensions?
285  match = cellfun (@(x) any (strcmpi (x, ext)), {formats.ext});
286endfunction
287
288function bool = isa_magick (coder, filename)
289
290  bool = false;
291  try
292    info = __magick_ping__ (filename, 1);
293    bool = strcmp (coder, info.Format);
294  end_try_catch
295
296endfunction
297
298function pretty_print_formats (formats)
299  ## define header names (none should be shorter than 3 characters)
300  headers = {"Extension", "isa", "Info", "Read", "Write", "Alpha", "Description"};
301  cols_length = cellfun (@numel, headers);
302
303  ## Adjust the maximal length of the extensions column
304  extensions = cellfun (@strjoin, {formats.ext}, {", "},
305                        "UniformOutput", false);
306  cols_length(1) = max (max (cellfun (@numel, extensions)), cols_length(1));
307  headers{1} = postpad (headers{1}, cols_length(1), " ");
308
309  ## Print the headers
310  disp (strjoin (headers, " | "));
311  under_headers = cellfun (@(x) repmat ("-", 1, numel (x)), headers,
312                           "UniformOutput", false);
313  disp (strjoin (under_headers, "-+-"));
314
315  template = strjoin (arrayfun (@(x) sprintf ("%%-%is", x), cols_length,
316                                "UniformOutput", false), " | ");
317
318  ## Print the function handle for this things won't be a pretty table.  So
319  ## instead we replace them with "yes" or "no", based on the support it has.
320  yes_no_cols = cat (2, {formats.isa}(:), {formats.info}(:), {formats.read}(:),
321                     {formats.write}(:), {formats.alpha}(:));
322  empty = cellfun (@isempty, yes_no_cols);
323  yes_no_cols(empty) = "no";
324  yes_no_cols(! empty) = "yes";
325
326  descriptions = {formats.description};
327  table = cat (2, extensions(:), yes_no_cols, descriptions(:));
328  printf ([template "\n"], table'{:});
329
330endfunction
331
332
333## This must work, even without support for image IO
334%!test
335%! formats = imformats ();
336%! assert (isstruct (formats));
337%!
338%! min_fields = {"ext", "read", "isa", "write", "info", "alpha", "description"};
339%! assert (all (ismember (min_fields, fieldnames (formats))));
340%!
341%! if (__have_feature__ ("MAGICK"))
342%!   assert (numel (formats) > 0);
343%! else
344%!   assert (numel (formats), 0);
345%! endif
346
347## When imread or imfinfo are called, the file must exist or the
348## function defined by imformats will never be called.  Because
349## of this, we must create a file for the tests to work.
350
351## changing the function that does the reading
352%!testif HAVE_MAGICK
353%! fname = [tempname() ".jpg"];
354%! def_fmt = imformats ();
355%! fid = fopen (fname, "w");
356%! unwind_protect
357%!   fmt = imformats ("jpg");
358%!   fmt.read = @numel;
359%!   imformats ("update", "jpg", fmt);
360%!   assert (imread (fname), numel (fname));
361%! unwind_protect_cleanup
362%!   fclose (fid);
363%!   unlink (fname);
364%!   imformats (def_fmt);
365%! end_unwind_protect
366
367## adding a new format
368%!testif HAVE_MAGICK
369%! fname = [tempname() ".new_fmt"];
370%! def_fmt = imformats ();
371%! fid = fopen (fname, "w");
372%! unwind_protect
373%!   fmt = imformats ("jpg"); # take jpg as template
374%!   fmt.ext = "new_fmt";
375%!   fmt.read = @() true ();
376%!   imformats ("add", fmt);
377%!   assert (imread (fname), true);
378%! unwind_protect_cleanup
379%!   fclose (fid);
380%!   unlink (fname);
381%!   imformats (def_fmt);
382%! end_unwind_protect
383
384## adding multiple formats at the same time
385%!testif HAVE_MAGICK
386%! fname1 = [tempname() ".new_fmt1"];
387%! fid1 = fopen (fname1, "w");
388%! fname2 = [tempname() ".new_fmt2"];
389%! fid2 = fopen (fname2, "w");
390%! def_fmt = imformats ();
391%! unwind_protect
392%!   fmt = imformats ("jpg"); # take jpg as template
393%!   fmt.ext = "new_fmt1";
394%!   fmt.read = @() true();
395%!   fmt(2) = fmt(1);
396%!   fmt(2).ext = "new_fmt2";
397%!   imformats ("add", fmt);
398%!   assert (imread (fname1), true);
399%!   assert (imread (fname2), true);
400%! unwind_protect_cleanup
401%!   fclose (fid1);
402%!   fclose (fid2);
403%!   unlink (fname1);
404%!   unlink (fname2);
405%!   imformats (def_fmt);
406%! end_unwind_protect
407
408## changing format and resetting back to default
409%!testif HAVE_MAGICK
410%! ori_fmt = mod_fmt = imformats ("jpg");
411%! mod_fmt.description = "Another description";
412%! imformats ("update", "jpg", mod_fmt);
413%! new_fmt = imformats ("jpg");
414%! assert (new_fmt.description, mod_fmt.description);
415%! imformats ("factory");
416%! new_fmt = imformats ("jpg");
417%! assert (new_fmt.description, ori_fmt.description);
418
419## updating to an invalid format should cause an error
420%!testif HAVE_MAGICK
421%! fmt = imformats ("jpg");
422%! fmt = rmfield (fmt, "read");
423%! error_thrown = false;
424%! try
425%!   imformats ("update", "jpg", fmt);
426%! catch
427%!   error_thrown = true;
428%! end_try_catch
429%! assert (error_thrown, true);
430