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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = [
"requests>=2.28.2",
"twine>=4.0.1",
"urllib3<2.0.0",
"Pillow>=9.3.0"
]

[tool.setuptools.packages.find]
Expand Down
4 changes: 2 additions & 2 deletions src/ansys/dynamicreporting/core/utils/report_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,8 +1278,8 @@ def set_payload_image(self, img):
if has_qt: # pragma: no cover
if isinstance(img, QtGui.QImage):
tmpimg = img
elif report_utils.is_enve_image(img):
image_data = report_utils.enve_image_to_data(img, str(self.guid))
elif report_utils.is_enve_image_or_pil(img):
image_data = report_utils.image_to_data(img)
if image_data is not None:
self.width = image_data["width"]
self.height = image_data["height"]
Expand Down
245 changes: 215 additions & 30 deletions src/ansys/dynamicreporting/core/utils/report_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import array
import base64
from html.parser import HTMLParser as BaseHTMLParser
import io
import json
import os
import os.path
import platform
Expand All @@ -9,6 +11,8 @@
import tempfile
from typing import List, Optional

from PIL import Image
from PIL.TiffTags import TAGS
import requests

try:
Expand All @@ -19,20 +23,13 @@
except (ImportError, SystemError):
has_enve = False

try:
from PyQt5 import QtCore, QtGui

has_qt = True
except ImportError:
has_qt = False

try:
import numpy

has_numpy = True
except ImportError:
has_numpy = False

TIFFTAG_IMAGEDESCRIPTION: int = 0x010E
text_type = str
"""@package report_utils
Methods that serve as a shim to the enve and ceiversion modules that may not be present
Expand All @@ -51,20 +48,222 @@ def encode_url(s):
return s


def is_enve_image(img):
if has_enve and has_qt: # pragma: no cover
return isinstance(img, enve.image)
return False
def check_if_PIL(img):
"""
Check if the input image can be opened by PIL.

Parameters
----------
img:
filename or bytes representing the picture

Returns
-------
bool:
True if the image can be opened by PIL
"""
# Assume you are getting bytes.
# If string, open it
imghandle = None
imgbytes = None
if isinstance(img, str):
imghandle = open(img, "rb")
elif isinstance(img, bytes):
imgbytes = img
try:
# Check PIL can handle the img opening
if imghandle:
Image.open(imghandle)
elif imgbytes:
Image.open(io.BytesIO(imgbytes))
return True
except Exception:
return False
finally:
if imghandle:
imghandle.close()


def is_enve_image_or_pil(img):
"""
Check if the input image can be handled by enve or PIL.

Parameters
----------

img:
filename or bytes representing the picture

Returns
-------
bool:
True if the image can be opened either by PIL or enve
"""
is_enve = False
if has_enve: # pragma: no cover
is_enve = isinstance(img, enve.image)
is_PIL = check_if_PIL(img)
return is_enve or is_PIL


def is_enhanced(image):
"""
Check if the input PIL image is an enhanced picture.

Parameters
----------
image:
the input PIL image

Returns
-------
str:
The json metadata, if enhanced. None otherwise
"""
if not image.format == "TIFF":
return None
frames = image.n_frames
if frames != 3:
return None
image.seek(0)
first_channel = image.getbands() == ("R", "G", "B")
image.seek(1)
second_channel = image.getbands() == ("R", "G", "B", "A")
image.seek(2)
third_channel = image.getbands() == ("F",)
if not all([first_channel, second_channel, third_channel]):
return None
image.seek(0)
meta_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2}
if not meta_dict.get("ImageDescription"):
return None
json_description = meta_dict["ImageDescription"][0]
description = json.loads(json_description)
if not description.get("parts"):
return None
if not description.get("variables"):
return None
return json_description


def create_new_pil_image(pil_image):
"""
Convert the existing PIL image into a new PIL image for enhanced export. Reading an
enhanced picture with PIL and save it directly does not work, so a new set of
pictures for each frame needs to be generated.

Parameters
----------
pil_image:
the PIL image currently handled

Returns
-------
list:
a list of PIL images, one for each frame of the original PIL image
"""
pil_image.seek(0)
images = [Image.fromarray(numpy.array(pil_image))]
pil_image.seek(1)
images.append(Image.fromarray(numpy.array(pil_image)))
pil_image.seek(2)
images.append(Image.fromarray(numpy.array(pil_image)))
return images


def save_tif_stripped(pil_image, data, metadata):
"""
Convert the existing pil image into a new TIF picture which can be used for
generating the required data for setting the payload.

Parameters
----------

pil_image:
the PIL image currently handled
data:
the dictionary holding the data for the payload
metadata:
the JSON string holding the enhanced picture metadata

Returns
-------
data:
the updated dictionary holding the data for the payload
"""
buff = io.BytesIO()
new_pil_images = create_new_pil_image(pil_image)
tiffinfo_dir = {TIFFTAG_IMAGEDESCRIPTION: metadata}
new_pil_images[0].save(
buff,
"TIFF",
compression="deflate",
save_all=True,
append_images=[new_pil_images[1], new_pil_images[2]],
tiffinfo=tiffinfo_dir,
)
buff.seek(0)
data["file_data"] = buff.read()
data["format"] = "tif"
buff.close()
return data


def PIL_image_to_data(img, guid=None):
"""
Convert the input image to a dictionary holding the data for the payload.

Parameters
----------
img:
the input picture. It may be bytes or the path to the file to read
guid:
the guid of the image if it is an already available Qt image

Returns
-------
data:
A dictionary holding the data for the payload
"""
imgbytes = None
imghandle = None
if isinstance(img, str):
imghandle = open(img, "rb")
elif isinstance(img, bytes):
imgbytes = img
data = {}
image = None
if imghandle:
image = Image.open(imghandle)
elif imgbytes:
image = Image.open(io.BytesIO(imgbytes))
data["format"] = image.format.lower()
if data["format"] == "tiff":
data["format"] = "tif"
data["width"] = image.width
data["height"] = image.height
metadata = is_enhanced(image)
if metadata:
data = save_tif_stripped(image, data, metadata)
else:
buff = io.BytesIO()
image.save(buff, "PNG")
buff.seek(0)
data["file_data"] = buff.read()
if imghandle:
imghandle.close()
return data


def enve_image_to_data(img, guid=None):
def image_to_data(img):
# Convert enve image object into a dictionary of image data or None
# The dictionary has the keys:
# 'width' = x pixel count
# 'height' = y pixel count
# 'format' = 'tif' or 'png'
# 'file_data' = a byte array of the raw image (same content as disk file)
if has_enve and has_qt: # pragma: no cover
data = None
if has_enve: # pragma: no cover
if isinstance(img, enve.image):
data = dict(width=img.dims[0], height=img.dims[1])
if img.enhanced:
Expand All @@ -80,22 +279,8 @@ def enve_image_to_data(img, guid=None):
return data
except OSError:
return None
else:
# convert to QImage via ppm string I/O
tmpimg = QtGui.QImage.fromData(img.ppm(), "ppm")
# record the guid in the image (watermark it)
# note: the Qt PNG format supports text keys
tmpimg.setText("CEI_REPORTS_GUID", guid)
# save it in PNG format in memory
be = QtCore.QByteArray()
buf = QtCore.QBuffer(be)
buf.open(QtCore.QIODevice.WriteOnly)
tmpimg.save(buf, "png")
buf.close()
data["format"] = "png"
data["file_data"] = buf.data() # returns a bytes() instance
return data
return None
if not data:
return PIL_image_to_data(img)


def enve_arch():
Expand Down
8 changes: 4 additions & 4 deletions tests/test_report_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ def test_encode_decode() -> bool:

@pytest.mark.ado_test
def test_is_enve_image(request) -> bool:
no_img = ru.is_enve_image(return_file_paths(request)[0])
assert no_img is False
img = ru.is_enve_image_or_pil(return_file_paths(request)[0])
assert img is True


@pytest.mark.ado_test
def test_enve_image_to_data(request) -> bool:
no_img = ru.enve_image_to_data(return_file_paths(request)[0])
assert no_img is None
img_data = ru.image_to_data(return_file_paths(request)[0])
assert "file_data" in img_data.keys()


@pytest.mark.ado_test
Expand Down