Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions mathics/builtin/image/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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]:
Expand Down
10 changes: 4 additions & 6 deletions mathics/builtin/image/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,18 +78,16 @@ 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
except Exception as e:
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),
Expand Down
43 changes: 29 additions & 14 deletions mathics/eval/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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])
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading