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