Skip to content

Commit cf0898c

Browse files
authored
Isolate more PIL routines into eval functions (#1836)
Some other slight tidying and linting done as well. Windows CI hangs worked around by removing pytest for gs (gries-scheider logic tests)
1 parent 206e7c0 commit cf0898c

4 files changed

Lines changed: 39 additions & 24 deletions

File tree

.github/workflows/windows.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ jobs:
5454
# we needs some CI that tests running when packages aren't available
5555
# So "dev" only below, not "dev,full".
5656
run: |
57-
make pytest gstest
57+
# The below pytest is hanging (not failing) on Windows. This is probably.
58+
# So remove for now
59+
# make pytest gstest
5860
make doctest DOCTEST_OPTIONS="--exclude WordCloud"
5961
# make check

mathics/builtin/image/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Image(Atom):
3232
class_head_name = "System`Image"
3333

3434
# FIXME: pixels should be optional if pillow is provided.
35-
def __init__(self, pixels, color_space, pillow=None, metadata={}, **kwargs):
35+
def __init__(self, pixels, color_space: str, pillow=None, metadata={}, **kwargs):
3636
super(Image, self).__init__(**kwargs)
3737

3838
if pillow is not None:
@@ -79,7 +79,7 @@ def __hash__(self):
7979
def __str__(self):
8080
return "-Image-"
8181

82-
def color_convert(self, to_color_space, preserve_alpha=True):
82+
def color_convert(self, to_color_space: str, preserve_alpha=True):
8383
if to_color_space == self.color_space and preserve_alpha:
8484
return self
8585
else:
@@ -94,7 +94,7 @@ def color_convert(self, to_color_space, preserve_alpha=True):
9494
def channels(self):
9595
return self.pixels.shape[2]
9696

97-
def default_format(self, evaluation, form):
97+
def default_format(self, evaluation, form) -> str:
9898
return "-Image-"
9999

100100
def dimensions(self) -> Tuple[int, int]:

mathics/builtin/image/misc.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from mathics.core.symbols import Symbol
1818
from mathics.core.systemsymbols import SymbolFailed, SymbolRule
1919
from mathics.eval.files_io.importexport import eval_ImageExport
20-
from mathics.eval.image import extract_exif
20+
from mathics.eval.image import eval_ImageImport
2121

2222
# The following classes are used to allow inclusion of
2323
# Builtin Functions only when certain Python packages
@@ -78,18 +78,16 @@ class ImageImport(Builtin):
7878
def eval(self, path: String, evaluation: Evaluation):
7979
"""ImageImport[path_String]"""
8080
try:
81-
pillow = PIL.Image.open(path.value)
81+
pillow, pixels, is_rgb, options_from_exif = eval_ImageImport(
82+
path.value, evaluation
83+
)
8284
except PIL.UnidentifiedImageError:
8385
evaluation.message("ImageImport", "infer", path)
8486
return SymbolFailed
8587
except Exception as e:
8688
evaluation.message("ImageImport", "imgmisc", str(e))
8789
return SymbolFailed
8890

89-
pixels = numpy.asarray(pillow)
90-
is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3
91-
options_from_exif = extract_exif(pillow, evaluation)
92-
9391
image = Image(pixels, "RGB" if is_rgb else "Grayscale", pillow=pillow)
9492
image_list_expression = [
9593
Expression(SymbolRule, String("Image"), image),

mathics/eval/image.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66

77
import functools
88
from operator import itemgetter
9-
from typing import List, Optional, Tuple, Union
9+
from typing import Any, List, Optional, Tuple, Union
1010

1111
import numpy
12+
import numpy.typing as npt
1213
import PIL
1314
import PIL.Image
1415

16+
# TAGS has been in PIL since Pillow 8.2.0
17+
from PIL.ExifTags import TAGS as ExifTags
18+
1519
from mathics.core.atoms import Rational
1620
from mathics.core.builtin import String
1721
from mathics.core.convert.python import from_python
@@ -21,14 +25,10 @@
2125
from mathics.core.symbols import Symbol
2226
from mathics.core.systemsymbols import SymbolRule, SymbolSimplify
2327

24-
try:
25-
from PIL.ExifTags import TAGS as ExifTags
26-
except ImportError:
27-
ExifTags = {}
28-
2928
# Exif: Exchangeable image file format for digital still cameras.
3029
# See http://www.exiv2.org/tags.html
3130

31+
3232
# names overriding the ones given by Pillow
3333
Exif_names = {
3434
37385: "FlashInfo",
@@ -85,11 +85,24 @@ def convolve(in1, in2, fixed=True):
8585
return ret[tuple(slice(p, -p) for p in excess)]
8686

8787

88+
def eval_ImageImport(path: str, evaluation: Evaluation):
89+
"""Called from ImageImport[path_String]"""
90+
91+
# PIL.Image.open can raise an error. The caller will handle that.
92+
pillow = PIL.Image.open(path)
93+
94+
pixels = numpy.asarray(pillow)
95+
is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3
96+
options_from_exif = extract_exif(pillow, evaluation)
97+
98+
return pillow, pixels, is_rgb, options_from_exif
99+
100+
88101
def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]:
89102
"""
90-
Convert Exif information from image into options
91-
that can be passed to Image[].
92-
Return None if there is no Exif information.
103+
Extract and convert EXIF (Exchangeable Image File Format)
104+
metadata from `image` into options that can be passed to
105+
Image[]. Return None if there is no EXIF information.
93106
"""
94107
if hasattr(image, "getexif"):
95108
# PIL seems to have a bug in getting v2_tags,
@@ -112,9 +125,11 @@ def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]:
112125
if not name:
113126
continue
114127

115-
# EXIF has the following types: Short, Long, Rational, Ascii, Byte
116-
# (see http://www.exiv2.org/tags.html). we detect the type from the
117-
# Python type Pillow gives us and do the appropriate MMA handling.
128+
# EXIF has the following types: Short, Long, Rational, Ascii
129+
# (note the capital first letter only), and Byte. See
130+
# http://www.exiv2.org/tags.html. We detect the type from
131+
# the Python type Pillow gives us and do the appropriate WMA
132+
# handling.
118133

119134
if isinstance(v, tuple) and len(v) == 2: # Rational
120135
value = Rational(v[0], v[1])
@@ -124,7 +139,7 @@ def extract_exif(image, evaluation: Evaluation) -> Optional[Expression]:
124139
value = Expression(SymbolSimplify, value).evaluate(evaluation)
125140
elif isinstance(v, bytes): # Byte
126141
value = String(" ".join([str(x) for x in v]))
127-
elif isinstance(v, (int, str)): # Short, Long, ASCII
142+
elif isinstance(v, (int, str)): # Short, Long, Ascii
128143
value = from_python(v)
129144
else:
130145
continue
@@ -176,7 +191,7 @@ def image_pixels(matrix):
176191
return None
177192

178193

179-
def linearize_numpy_array(a: numpy.array) -> Tuple[numpy.array, int]:
194+
def linearize_numpy_array(a: npt.NDArray[Any]) -> Tuple[npt.NDArray[Any], int]:
180195
"""
181196
Transforms a numpy array numpy array and return the array and the number
182197
of dimensions in the array

0 commit comments

Comments
 (0)