diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 5fca8651c..b190932ae 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -54,6 +54,8 @@ jobs: # we needs some CI that tests running when packages aren't available # So "dev" only below, not "dev,full". run: | - make pytest gstest + # The below pytest is hanging (not failing) on Windows. This is probably. + # So remove for now + # make pytest gstest make doctest DOCTEST_OPTIONS="--exclude WordCloud" # make check diff --git a/mathics/builtin/image/base.py b/mathics/builtin/image/base.py index 5a45522ce..56a9fbc95 100644 --- a/mathics/builtin/image/base.py +++ b/mathics/builtin/image/base.py @@ -32,7 +32,7 @@ class Image(Atom): class_head_name = "System`Image" # FIXME: pixels should be optional if pillow is provided. - def __init__(self, pixels, color_space, pillow=None, metadata={}, **kwargs): + def __init__(self, pixels, color_space: str, pillow=None, metadata={}, **kwargs): super(Image, self).__init__(**kwargs) if pillow is not None: @@ -79,7 +79,7 @@ def __hash__(self): def __str__(self): return "-Image-" - def color_convert(self, to_color_space, preserve_alpha=True): + def color_convert(self, to_color_space: str, preserve_alpha=True): if to_color_space == self.color_space and preserve_alpha: return self else: @@ -94,7 +94,7 @@ def color_convert(self, to_color_space, preserve_alpha=True): def channels(self): return self.pixels.shape[2] - def default_format(self, evaluation, form): + def default_format(self, evaluation, form) -> str: return "-Image-" def dimensions(self) -> Tuple[int, int]: diff --git a/mathics/builtin/image/misc.py b/mathics/builtin/image/misc.py index 7dbe4a7e6..277bc2163 100644 --- a/mathics/builtin/image/misc.py +++ b/mathics/builtin/image/misc.py @@ -17,7 +17,7 @@ from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolFailed, SymbolRule from mathics.eval.files_io.importexport import eval_ImageExport -from mathics.eval.image import extract_exif +from mathics.eval.image import eval_ImageImport # The following classes are used to allow inclusion of # Builtin Functions only when certain Python packages @@ -78,7 +78,9 @@ class ImageImport(Builtin): def eval(self, path: String, evaluation: Evaluation): """ImageImport[path_String]""" try: - pillow = PIL.Image.open(path.value) + pillow, pixels, is_rgb, options_from_exif = eval_ImageImport( + path.value, evaluation + ) except PIL.UnidentifiedImageError: evaluation.message("ImageImport", "infer", path) return SymbolFailed @@ -86,10 +88,6 @@ def eval(self, path: String, evaluation: Evaluation): evaluation.message("ImageImport", "imgmisc", str(e)) return SymbolFailed - pixels = numpy.asarray(pillow) - is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 - options_from_exif = extract_exif(pillow, evaluation) - image = Image(pixels, "RGB" if is_rgb else "Grayscale", pillow=pillow) image_list_expression = [ Expression(SymbolRule, String("Image"), image), diff --git a/mathics/eval/image.py b/mathics/eval/image.py index acf4874ae..6fc2f97ac 100644 --- a/mathics/eval/image.py +++ b/mathics/eval/image.py @@ -6,12 +6,16 @@ import functools from operator import itemgetter -from typing import List, Optional, Tuple, Union +from typing import Any, List, Optional, Tuple, Union import numpy +import numpy.typing as npt import PIL import PIL.Image +# TAGS has been in PIL since Pillow 8.2.0 +from PIL.ExifTags import TAGS as ExifTags + from mathics.core.atoms import Rational from mathics.core.builtin import String from mathics.core.convert.python import from_python @@ -21,14 +25,10 @@ from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolRule, SymbolSimplify -try: - from PIL.ExifTags import TAGS as ExifTags -except ImportError: - ExifTags = {} - # Exif: Exchangeable image file format for digital still cameras. # See http://www.exiv2.org/tags.html + # names overriding the ones given by Pillow Exif_names = { 37385: "FlashInfo", @@ -85,11 +85,24 @@ def convolve(in1, in2, fixed=True): return ret[tuple(slice(p, -p) for p in excess)] +def eval_ImageImport(path: str, evaluation: Evaluation): + """Called from ImageImport[path_String]""" + + # PIL.Image.open can raise an error. The caller will handle that. + pillow = PIL.Image.open(path) + + pixels = numpy.asarray(pillow) + is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 + options_from_exif = extract_exif(pillow, evaluation) + + return pillow, pixels, is_rgb, options_from_exif + + def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]: """ - Convert Exif information from image into options - that can be passed to Image[]. - Return None if there is no Exif information. + Extract and convert EXIF (Exchangeable Image File Format) + metadata from `image` into options that can be passed to + Image[]. Return None if there is no EXIF information. """ if hasattr(image, "getexif"): # PIL seems to have a bug in getting v2_tags, @@ -112,9 +125,11 @@ def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]: if not name: continue - # EXIF has the following types: Short, Long, Rational, Ascii, Byte - # (see http://www.exiv2.org/tags.html). we detect the type from the - # Python type Pillow gives us and do the appropriate MMA handling. + # EXIF has the following types: Short, Long, Rational, Ascii + # (note the capital first letter only), and Byte. See + # http://www.exiv2.org/tags.html. We detect the type from + # the Python type Pillow gives us and do the appropriate WMA + # handling. if isinstance(v, tuple) and len(v) == 2: # Rational value = Rational(v[0], v[1]) @@ -124,7 +139,7 @@ def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]: value = Expression(SymbolSimplify, value).evaluate(evaluation) elif isinstance(v, bytes): # Byte value = String(" ".join([str(x) for x in v])) - elif isinstance(v, (int, str)): # Short, Long, ASCII + elif isinstance(v, (int, str)): # Short, Long, Ascii value = from_python(v) else: continue @@ -176,7 +191,7 @@ def image_pixels(matrix): return None -def linearize_numpy_array(a: numpy.array) -> Tuple[numpy.array, int]: +def linearize_numpy_array(a: npt.NDArray[Any]) -> Tuple[npt.NDArray[Any], int]: """ Transforms a numpy array numpy array and return the array and the number of dimensions in the array