changeset 18298:15db54c4a572

Fix input parsing for imageIO functions and make imread accept URL (bug #41234) * image/private/imageIO.m: make this function check existence of the file and handle URLs. It may also modify the input that imread() and imfinfo() received to remove the format name if it was given as a separate argument. The filepath will be modified to give the absolute filepath, and URLs will cause the file to be downloaded, and the filepath for a temporary will be given (and the file removed in the end). If the file can't be found it will thrown an error meaning that if the file is not found, whatever function is set for that format, it will never be called). * image/imfinfo.m, image/imread.m: change functions for the modified call to imageIO() and add new tests. * image/private/__imfinfo__.m, image/private/__imread__.m: remove parsing of input, checking for existence of file, and handling of URL since that is now done by imageIO(). * image/imformats.m: fix tests for the new behaviour.
author Carnë Draug <carandraug@octave.org>
date Thu, 16 Jan 2014 19:12:36 +0000
parents 1589b2fc74ae
children 515187b51411
files scripts/image/imfinfo.m scripts/image/imformats.m scripts/image/imread.m scripts/image/private/__imfinfo__.m scripts/image/private/__imread__.m scripts/image/private/imageIO.m
diffstat 6 files changed, 238 insertions(+), 138 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/image/imfinfo.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/imfinfo.m	Thu Jan 16 19:12:36 2014 +0000
@@ -18,8 +18,8 @@
 
 ## -*- texinfo -*-
 ## @deftypefn  {Function File} {@var{info} =} imfinfo (@var{filename})
-## @deftypefnx {Function File} {@var{info} =} imfinfo (@var{filename}, @var{ext})
 ## @deftypefnx {Function File} {@var{info} =} imfinfo (@var{url})
+## @deftypefnx {Function File} {@var{info} =} imfinfo (@dots{}, @var{ext})
 ## Read image information from a file.
 ##
 ## @code{imfinfo} returns a structure containing information about the image
@@ -148,14 +148,32 @@
 
 ## Author: Soren Hauberg <hauberg@gmail.com>
 
-function info = imfinfo (varargin)
+function info = imfinfo (filename, varargin)
   if (nargin < 1 || nargin > 2)
     print_usage ();
-  elseif (! ischar (varargin{1}))
+  elseif (! ischar (filename))
     error ("imfinfo: FILENAME must be a string");
-  elseif (nargin > 1 && ! ischar (varargin{2}))
+  elseif (nargin > 1 && ! ischar (ext))
     error ("imfinfo: EXT must be a string");
   endif
-  info = imageIO (@__imfinfo__, "info", varargin, varargin{:});
+  info = imageIO ("imfinfo", @__imfinfo__, "info", filename, varargin{:});
 endfunction
 
+## This test is the same as the similar one in imread. imfinfo must check
+## if file exists before calling __imfinfo_. This test confirm this.
+%!testif HAVE_MAGICK
+%! fmt = fmt_ori = imformats ("jpg");
+%! fmt.info = @true;
+%! error_thrown = false;
+%! imformats ("update", "jpg", fmt);
+%! unwind_protect
+%!   try
+%!     imread ("I sure hope this file does not exist.jpg");
+%!   catch
+%!     error_thrown = true;
+%!   end_try_catch
+%! unwind_protect_cleanup
+%!   imformats ("update", "jpg", fmt_ori);
+%! end_unwind_protect
+%! assert (error_thrown, true);
+
--- a/scripts/image/imformats.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/imformats.m	Thu Jan 16 19:12:36 2014 +0000
@@ -280,33 +280,68 @@
   end_try_catch
 endfunction
 
-## changing the function to read
+## When imread or imfinfo are called, the file must exist or the
+## function defined by imformats will never be called.  Because
+## of this, we must create a file for the tests to work.
+
+## changing the function that does the reading
 %!testif HAVE_MAGICK
-%! fmt = imformats ("jpg");
-%! fmt.read = @(x) size (x, 2);
-%! imformats ("update", "jpg", fmt);
-%! assert (imread ("this is 30 characters long.jpg"), 30);
+%! fname = [tmpnam() ".jpg"];
+%! def_fmt = imformats ();
+%! fid = fopen (fname, "w");
+%! unwind_protect
+%!   fmt = imformats ("jpg");
+%!   fmt.read = @numel;
+%!   imformats ("update", "jpg", fmt);
+%!   assert (imread (fname), numel (fname));
+%! unwind_protect_cleanup
+%!   fclose (fid);
+%!   unlink (fname);
+%!   imformats (def_fmt);
+%! end_unwind_protect
 
 ## adding a new format
 %!testif HAVE_MAGICK
-%! fmt = imformats ("jpg");
-%! fmt.ext = "junk";
-%! fmt.read = @(x) true();
-%! imformats ("add", fmt);
-%! assert (imread ("some file.junk"), true);
+%! fname = [tmpnam() ".new_fmt"];
+%! def_fmt = imformats ();
+%! fid = fopen (fname, "w");
+%! unwind_protect
+%!   fmt = imformats ("jpg"); # take jpg as template
+%!   fmt.ext = "new_fmt";
+%!   fmt.read = @() true ();
+%!   imformats ("add", fmt);
+%!   assert (imread (fname), true);
+%! unwind_protect_cleanup
+%!   fclose (fid);
+%!   unlink (fname);
+%!   imformats (def_fmt);
+%! end_unwind_protect
 
-## adding multiple formats in one way
+## adding multiple formats at the same time
 %!testif HAVE_MAGICK
-%! fmt = imformats ("jpg");
-%! fmt.ext = "junk1";
-%! fmt.read = @(x) true();
-%! fmt(2) = fmt(1);
-%! fmt(2).ext = "junk2";
-%! imformats ("add", fmt);
-%! assert (imread ("some file.junk1"), true);
-%! assert (imread ("some file.junk2"), true);
+%! fname1 = [tmpnam() ".new_fmt1"];
+%! fid1 = fopen (fname1, "w");
+%! fname2 = [tmpnam() ".new_fmt2"];
+%! fid2 = fopen (fname2, "w");
+%! def_fmt = imformats ();
+%! unwind_protect
+%!   fmt = imformats ("jpg"); # take jpg as template
+%!   fmt.ext = "new_fmt1";
+%!   fmt.read = @() true();
+%!   fmt(2) = fmt(1);
+%!   fmt(2).ext = "new_fmt2";
+%!   imformats ("add", fmt);
+%!   assert (imread (fname1), true);
+%!   assert (imread (fname2), true);
+%! unwind_protect_cleanup
+%!   fclose (fid1);
+%!   fclose (fid2);
+%!   unlink (fname1);
+%!   unlink (fname2);
+%!   imformats (def_fmt);
+%! end_unwind_protect
 
-## changing format
+## changing format and resetting back to default
 %!testif HAVE_MAGICK
 %! ori_fmt = mod_fmt = imformats ("jpg");
 %! mod_fmt.description = "Another description";
--- a/scripts/image/imread.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/imread.m	Thu Jan 16 19:12:36 2014 +0000
@@ -23,16 +23,16 @@
 
 ## -*- texinfo -*-
 ## @deftypefn  {Function File} {[@var{img}, @var{map}, @var{alpha}] =} imread (@var{filename})
-## @deftypefnx {Function File} {[@dots{}] =} imread (@var{filename}, @var{ext})
 ## @deftypefnx {Function File} {[@dots{}] =} imread (@var{url})
+## @deftypefnx {Function File} {[@dots{}] =} imread (@dots{}, @var{ext})
 ## @deftypefnx {Function File} {[@dots{}] =} imread (@dots{}, @var{idx})
 ## @deftypefnx {Function File} {[@dots{}] =} imread (@dots{}, @var{param1}, @var{val1}, @dots{})
 ## Read images from various file formats.
 ##
 ## Reads an image as a matrix from the file @var{filename}.  If there is
 ## no file @var{filename}, and @var{ext} was specified, it will look for
-## a file named @var{filename} and extension @var{ext}, i.e., a file named
-## @var{filename}.@var{ext}.
+## a file with the extension @var{ext}.  Finally, it will attempt to download
+## and read an image from @var{url}.
 ##
 ## The size and class of the output depends on the
 ## format of the image.  A color image is returned as an
@@ -88,23 +88,14 @@
 ## Author: Stefan van der Walt <stefan@sun.ac.za>
 ## Author: Andy Adler
 
-function [img, varargout] = imread (varargin)
+function [img, varargout] = imread (filename, varargin)
   if (nargin < 1)
     print_usage ();
-  elseif (! ischar (varargin{1}))
+  elseif (! ischar (filename))
     error ("imread: FILENAME must be a string");
   endif
-  ## In case the file format was specified as a separate argument we
-  ## do this. imageIO() will ignore the second part if filename on its
-  ## own is enough. And if the second argument was a parameter name instead
-  ## of an extension, it is still going to be passed to the next function
-  ## since we are passing the whole function input as well.
-  filename = {varargin{1}};
-  if (nargin > 1 && ischar (varargin {2}))
-    filename{2} = varargin{2};
-  endif
 
-  [img, varargout{1:nargout-1}] = imageIO (@__imread__, "read", filename, varargin{:});
+  [img, varargout{1:nargout-1}] = imageIO ("imread", @__imread__, "read", filename, varargin{:});
 endfunction
 
 
@@ -139,3 +130,48 @@
 %! assert (A(:,:,2), uint8 ([0, 255, 0; 255,  28, 255; 0, 255, 0]));
 %! assert (A(:,:,3), uint8 ([0, 255, 0; 255,  36, 255; 0, 255, 0]));
 
+## If a file does not exist, it's the job of imread to check the file
+## exists before sending it over to __imread__ or whatever function
+## is defined in imformats to handle that specific format.  This is the
+## same in imfinfo.  So in this test we replace one format in imformats
+## with something that will not give an error if the file is missing
+## and make sure we do get an error.
+%!testif HAVE_MAGICK
+%! fmt = fmt_ori = imformats ("jpg");
+%! fmt.read = @true;
+%! error_thrown = false;
+%! imformats ("update", "jpg", fmt);
+%! unwind_protect
+%!   try
+%!     imread ("I sure hope this file does not exist.jpg");
+%!   catch
+%!     error_thrown = true;
+%!   end_try_catch
+%! unwind_protect_cleanup
+%!   imformats ("update", "jpg", fmt_ori);
+%! end_unwind_protect
+%! assert (error_thrown, true);
+
+## make one of the formats read, return what it received as input to
+## confirm that the input parsing is working correcly
+%!testif HAVE_MAGICK
+%! fname = [tmpnam() ".jpg"];
+%! def_fmt = imformats ();
+%! fid = fopen (fname, "w");
+%! unwind_protect
+%!   fmt = imformats ("jpg");
+%!   fmt.read = @(varargin) varargin;
+%!   imformats ("update", "jpg", fmt);
+%!   assert (imread (fname), {fname});
+%!   assert (imread (fname, "jpg"), {fname});
+%!   assert (imread (fname(1:end-4), "jpg"), {fname});
+%!   extra_inputs = {"some", 89, i, {6 7 8}};
+%!   assert (imread (fname, extra_inputs{:}), {fname, extra_inputs{:}});
+%!   assert (imread (fname, "jpg", extra_inputs{:}), {fname, extra_inputs{:}});
+%!   assert (imread (fname(1:end-4), "jpg", extra_inputs{:}), {fname, extra_inputs{:}});
+%! unwind_protect_cleanup
+%!   fclose (fid);
+%!   unlink (fname);
+%!   imformats (def_fmt);
+%! end_unwind_protect
+
--- a/scripts/image/private/__imfinfo__.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/private/__imfinfo__.m	Thu Jan 16 19:12:36 2014 +0000
@@ -17,56 +17,22 @@
 ## along with Octave; see the file COPYING.  If not, see
 ## <http://www.gnu.org/licenses/>.
 
-## This function does al the work of imfinfo. It exists here as private
+## This function does all the work of imfinfo. It exists here as private
 ## function so that imfinfo can use other functions if imformats is
 ## configured to. It is also needed so that imformats can create a
 ## function handle for it.
 
 ## Author: Soren Hauberg <hauberg@gmail.com>
 
-function info = __imfinfo__ (filename, ext)
+function info = __imfinfo__ (filename)
 
-  if (nargin < 1 || nargin > 2)
+  if (nargin != 1)
     print_usage ("imfinfo");
+  elseif (! ischar (filename))
+    error ("imfinfo: FILENAME must be a string");
   endif
 
-  if (! ischar (filename))
-    error ("imfinfo: FILENAME must be a string");
-  elseif (nargin >= 2 && ! ischar (ext))
-    error ("imfinfo: EXT must be a string");
-  endif
-  filename = tilde_expand (filename);
-
-  delete_file = false;
-  unwind_protect
-
-    fn = file_in_path (IMAGE_PATH, filename);
-    if (isempty (fn))
-      ## We couldn't find the file so...
-      if (nargin >= 2)
-        ## try adding a possible file extesion
-        filename  = [filename "." ext];
-        fn        = file_in_path (IMAGE_PATH, filename);
-        if (isempty (fn))
-          error ("imfinfo: cannot find file %s", filename);
-        endif
-      else
-        ## try filename as an URL
-        [fn, status, msg] = urlwrite (filename, tmpnam ());
-        if (! status)
-          error ("imfinfo: cannot find or download %s: %s", filename, msg);
-        endif
-        delete_file = true;
-      endif
-    endif
-
-    info = __magick_finfo__ (fn);
-
-  unwind_protect_cleanup
-    if (delete_file)
-      unlink (fn);
-    endif
-  end_unwind_protect
+  info = __magick_finfo__ (filename);
 
 endfunction
 
--- a/scripts/image/private/__imread__.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/private/__imread__.m	Thu Jan 16 19:12:36 2014 +0000
@@ -44,18 +44,6 @@
   ## keep track of the varargin offset we're looking at each moment
   offset    = 1;
 
-  filename  = tilde_expand (filename);
-  fn        = file_in_path (IMAGE_PATH, filename);
-  if (isempty (fn) && nargin >= offset + 1 && ischar (varargin{offset}))
-    ## if we can't find the file, check if the next input is the file extension
-    filename  = [filename "." varargin{offset}];
-    fn        = file_in_path (IMAGE_PATH, filename);
-    offset++;
-  endif
-  if (isempty (fn))
-    error ("imread: cannot find %s", filename);
-  endif
-
   ## It is possible for an file with multiple pages to have very different
   ## images on each page. Specifically, they may have different sizes. Because
   ## of this, we need to first find out the index of the images to read so
@@ -92,9 +80,9 @@
 
   ## Use information from the first image to be read to set defaults.
   if (ischar (options.index) && strcmpi (options.index, "all"))
-    info = __magick_ping__ (fn, 1);
+    info = __magick_ping__ (filename, 1);
   else
-    info = __magick_ping__ (fn, options.index(1));
+    info = __magick_ping__ (filename, options.index(1));
   endif
 
   ## Set default for options.
@@ -145,7 +133,7 @@
     endswitch
   endfor
 
-  [varargout{1:nargout}] = __magick_read__ (fn, options);
+  [varargout{1:nargout}] = __magick_read__ (filename, options);
 
 endfunction
 
--- a/scripts/image/private/imageIO.m	Fri Jan 17 19:09:42 2014 +0000
+++ b/scripts/image/private/imageIO.m	Thu Jan 16 19:12:36 2014 +0000
@@ -16,54 +16,111 @@
 ## along with Octave; see the file COPYING.  If not, see
 ## <http://www.gnu.org/licenses/>.
 
-## This function simply connects imread and imfinfo() to the function
-## to be used based on their format. It does it by checking the file extension
-## of the file and redirecting to the appropriate function after checking
-## with imformats.
+## This function the image input functions imread() and imfinfo() to the
+## functions that will actually be used, based on their format.  See below
+## on the details on how it identifies the format, and to what it defaults.
+##
+## It will change the input arguments that were passed to imread() and
+## imfinfo().  It will change the filename to provide the absolute filepath
+## for the file, it will extract the possible format name from the rest of
+## the input arguments (in case there was one), and will give an error if
+## the file can't be found.
 ##
-## First argument is a function handle for the default imageIO function (what
-## to use if the file extension for the image file is not listed by imformats).
-## Second argument is the fieldname in the struct returned by imformats with a
-## function handle for the function to use. Third argument is a cell array, its
-## first element the filename, and the second, an optional file extension to
-## add to filename, if filename alone does not exist. All the others are the
-## original input arguments passed to the original imageIO function which will
-## be passed on to the destination function.
+## Usage:
 ##
-## No input checking whatsoever is performed. That should be performed by the
-## function calling it.
-
-function varargout = imageIO (core_func, fieldname, filename, varargin)
+## func      - Function name to use on error message.
+## core_func - Function handle for the default function to use if we can't
+##             find the format in imformats.
+## fieldname - Name of the field in the struct returned by imformats that
+##             has the function to use.
+## filename  - Most likely the first input argument from the function that
+##             called this. May be missing the file extension which can be
+##             on varargin.
+## varargin  - Followed by all the OTHER arguments passed to imread and
+##             imfinfo.
 
-  ## It should not be this function job to check if the file exists or not.
-  ## However, we need to know the file extension to use with imformats and
-  ## that means we need to know the actual filename that will be used which
-  ## is dependent on whether a file exists.
-  ##
-  ## If a file named filename{1} exists, then that's it, we will use that
-  ## wether it has an extension or not. If it does not exist and we have
-  ## something in filename{2}, then we will consider it the file extension.
-  ## Note the detail that if we find a file using filename{1} only, then we
-  ## should completely ignore filename{2}. It won't even be used by
-  ## imformats() at all, even if filename{1} has no extension to use with
-  ## imformats().
-  if (isscalar (filename) || ! isempty (file_in_path (IMAGE_PATH, filename{1})))
-    [~, ~, ext] = fileparts (filename{1});
-    if (! isempty (ext))
-      ## remove dot from extension
-      ext = ext(2:end);
-    endif
-  else
-    ext = filename{2};
+function varargout = imageIO (func, core_func, fieldname, filename, varargin)
+
+  ## First thing: figure out the filename and possibly download it.
+  ## The first attempt is to try the filename and see if it exists.  If it
+  ## does not, we try to add the next argument since the file extension can
+  ## be given as a separate argument.  If we still can't find the file, it
+  ## can be a URL.  Lastly, if we still didn't found a file, try adding the
+  ## extension to the URL
+
+  file_2_delete = false;  # will we have to remove the file in the end?
+  persistent abs_path = @(x) file_in_path (IMAGE_PATH, tilde_expand (x));
+
+  ## Filename was given with file extension
+  fn = abs_path (filename);
+  if (isempty (fn) && ! isempty (varargin))
+    ## Maybe if we add a file extension
+    fn = abs_path ([filename "." varargin{1}]);
   endif
 
-  fmt = imformats (ext);
-  ## When there is no match, fmt will be a 1x1 structure with no fields,
-  ## so we can't just use `isempty (fmt)'.
-  if (isempty (fieldnames (fmt)))
-    [varargout{1:nargout}] = core_func (varargin{:});
-  else
-    [varargout{1:nargout}] = fmt.(fieldname) (varargin{:});
+  ## Maybe we have an URL
+  if (isempty (fn))
+    file_2_delete = true; # mark file for deletion
+    [fn, ~] = urlwrite (filename, tmpnam ());
+    ## Maybe the URL is missing the file extension
+    if (isempty (fn) && ! isempty (varargin))
+      [fn, ~] = urlwrite ([filename "." varargin{1}], tmpnam ());
+    endif
+
+    if (isempty (fn))
+      error ("%s: unable to find file %s", func, filename);
+    endif
   endif
+
+  ## unwind_protect block because we may have a file to remove in the end
+  unwind_protect
+
+    ## When guessing the format to use, we first check if the second
+    ## argument is a format defined in imformats.  If so, we remove it
+    ## from the rest of arguments before passing them on.  If not, we
+    ## try to guess the format from the file extension.  Finally, if
+    ## we still don't know the format, we use the Octave core functions
+    ## which is the same for all formats.
+    foo = []; # the function we will use
+
+    ## We check if the call to imformats (ext) worked using
+    ## "isempty (fieldnames (fmt))" because when it fails, the returned
+    ## struct is not considered empty.
+
+    ## try the second input argument
+    if (! isempty (varargin) && ischar (varargin{1}))
+      fmt = imformats (varargin{1});
+      if (! isempty (fieldnames (fmt)))
+        foo = fmt.(fieldname);
+        varargin(1) = []; # remove format name from arguments
+      endif
+    endif
+
+    ## try extension from file name
+    if (isempty (foo))
+      [~, ~, ext] = fileparts (fn);
+      if (! isempty (ext))
+        ## remove dot from extension
+        ext = ext(2:end);
+      endif
+      fmt = imformats (ext);
+      if (! isempty (fieldnames (fmt)))
+        foo = fmt.(fieldname);
+      endif
+    endif
+
+    ## use the core function
+    if (isempty (foo))
+      foo = core_func;
+    endif
+
+    [varargout{1:nargout}] = foo (fn, varargin{:});
+
+  unwind_protect_cleanup
+    if (file_2_delete)
+      unlink (fn);
+    endif
+  end_unwind_protect
+
 endfunction