Skip to content

Commit c4d98b9

Browse files
remove enve dependency (#73)
1 parent 4ba5fb7 commit c4d98b9

File tree

4 files changed

+222
-36
lines changed

4 files changed

+222
-36
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies = [
4646
"requests>=2.28.2",
4747
"twine>=4.0.1",
4848
"urllib3<2.0.0",
49+
"Pillow>=9.3.0"
4950
]
5051

5152
[tool.setuptools.packages.find]

src/ansys/dynamicreporting/core/utils/report_objects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,8 +1278,8 @@ def set_payload_image(self, img):
12781278
if has_qt: # pragma: no cover
12791279
if isinstance(img, QtGui.QImage):
12801280
tmpimg = img
1281-
elif report_utils.is_enve_image(img):
1282-
image_data = report_utils.enve_image_to_data(img, str(self.guid))
1281+
elif report_utils.is_enve_image_or_pil(img):
1282+
image_data = report_utils.image_to_data(img)
12831283
if image_data is not None:
12841284
self.width = image_data["width"]
12851285
self.height = image_data["height"]

src/ansys/dynamicreporting/core/utils/report_utils.py

Lines changed: 215 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import array
22
import base64
33
from html.parser import HTMLParser as BaseHTMLParser
4+
import io
5+
import json
46
import os
57
import os.path
68
import platform
@@ -9,6 +11,8 @@
911
import tempfile
1012
from typing import List, Optional
1113

14+
from PIL import Image
15+
from PIL.TiffTags import TAGS
1216
import requests
1317

1418
try:
@@ -19,20 +23,13 @@
1923
except (ImportError, SystemError):
2024
has_enve = False
2125

22-
try:
23-
from PyQt5 import QtCore, QtGui
24-
25-
has_qt = True
26-
except ImportError:
27-
has_qt = False
28-
2926
try:
3027
import numpy
3128

3229
has_numpy = True
3330
except ImportError:
3431
has_numpy = False
35-
32+
TIFFTAG_IMAGEDESCRIPTION: int = 0x010E
3633
text_type = str
3734
"""@package report_utils
3835
Methods that serve as a shim to the enve and ceiversion modules that may not be present
@@ -51,20 +48,222 @@ def encode_url(s):
5148
return s
5249

5350

54-
def is_enve_image(img):
55-
if has_enve and has_qt: # pragma: no cover
56-
return isinstance(img, enve.image)
57-
return False
51+
def check_if_PIL(img):
52+
"""
53+
Check if the input image can be opened by PIL.
54+
55+
Parameters
56+
----------
57+
img:
58+
filename or bytes representing the picture
59+
60+
Returns
61+
-------
62+
bool:
63+
True if the image can be opened by PIL
64+
"""
65+
# Assume you are getting bytes.
66+
# If string, open it
67+
imghandle = None
68+
imgbytes = None
69+
if isinstance(img, str):
70+
imghandle = open(img, "rb")
71+
elif isinstance(img, bytes):
72+
imgbytes = img
73+
try:
74+
# Check PIL can handle the img opening
75+
if imghandle:
76+
Image.open(imghandle)
77+
elif imgbytes:
78+
Image.open(io.BytesIO(imgbytes))
79+
return True
80+
except Exception:
81+
return False
82+
finally:
83+
if imghandle:
84+
imghandle.close()
85+
86+
87+
def is_enve_image_or_pil(img):
88+
"""
89+
Check if the input image can be handled by enve or PIL.
90+
91+
Parameters
92+
----------
93+
94+
img:
95+
filename or bytes representing the picture
96+
97+
Returns
98+
-------
99+
bool:
100+
True if the image can be opened either by PIL or enve
101+
"""
102+
is_enve = False
103+
if has_enve: # pragma: no cover
104+
is_enve = isinstance(img, enve.image)
105+
is_PIL = check_if_PIL(img)
106+
return is_enve or is_PIL
107+
108+
109+
def is_enhanced(image):
110+
"""
111+
Check if the input PIL image is an enhanced picture.
112+
113+
Parameters
114+
----------
115+
image:
116+
the input PIL image
117+
118+
Returns
119+
-------
120+
str:
121+
The json metadata, if enhanced. None otherwise
122+
"""
123+
if not image.format == "TIFF":
124+
return None
125+
frames = image.n_frames
126+
if frames != 3:
127+
return None
128+
image.seek(0)
129+
first_channel = image.getbands() == ("R", "G", "B")
130+
image.seek(1)
131+
second_channel = image.getbands() == ("R", "G", "B", "A")
132+
image.seek(2)
133+
third_channel = image.getbands() == ("F",)
134+
if not all([first_channel, second_channel, third_channel]):
135+
return None
136+
image.seek(0)
137+
meta_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2}
138+
if not meta_dict.get("ImageDescription"):
139+
return None
140+
json_description = meta_dict["ImageDescription"][0]
141+
description = json.loads(json_description)
142+
if not description.get("parts"):
143+
return None
144+
if not description.get("variables"):
145+
return None
146+
return json_description
147+
148+
149+
def create_new_pil_image(pil_image):
150+
"""
151+
Convert the existing PIL image into a new PIL image for enhanced export. Reading an
152+
enhanced picture with PIL and save it directly does not work, so a new set of
153+
pictures for each frame needs to be generated.
154+
155+
Parameters
156+
----------
157+
pil_image:
158+
the PIL image currently handled
159+
160+
Returns
161+
-------
162+
list:
163+
a list of PIL images, one for each frame of the original PIL image
164+
"""
165+
pil_image.seek(0)
166+
images = [Image.fromarray(numpy.array(pil_image))]
167+
pil_image.seek(1)
168+
images.append(Image.fromarray(numpy.array(pil_image)))
169+
pil_image.seek(2)
170+
images.append(Image.fromarray(numpy.array(pil_image)))
171+
return images
172+
173+
174+
def save_tif_stripped(pil_image, data, metadata):
175+
"""
176+
Convert the existing pil image into a new TIF picture which can be used for
177+
generating the required data for setting the payload.
178+
179+
Parameters
180+
----------
181+
182+
pil_image:
183+
the PIL image currently handled
184+
data:
185+
the dictionary holding the data for the payload
186+
metadata:
187+
the JSON string holding the enhanced picture metadata
188+
189+
Returns
190+
-------
191+
data:
192+
the updated dictionary holding the data for the payload
193+
"""
194+
buff = io.BytesIO()
195+
new_pil_images = create_new_pil_image(pil_image)
196+
tiffinfo_dir = {TIFFTAG_IMAGEDESCRIPTION: metadata}
197+
new_pil_images[0].save(
198+
buff,
199+
"TIFF",
200+
compression="deflate",
201+
save_all=True,
202+
append_images=[new_pil_images[1], new_pil_images[2]],
203+
tiffinfo=tiffinfo_dir,
204+
)
205+
buff.seek(0)
206+
data["file_data"] = buff.read()
207+
data["format"] = "tif"
208+
buff.close()
209+
return data
210+
211+
212+
def PIL_image_to_data(img, guid=None):
213+
"""
214+
Convert the input image to a dictionary holding the data for the payload.
215+
216+
Parameters
217+
----------
218+
img:
219+
the input picture. It may be bytes or the path to the file to read
220+
guid:
221+
the guid of the image if it is an already available Qt image
222+
223+
Returns
224+
-------
225+
data:
226+
A dictionary holding the data for the payload
227+
"""
228+
imgbytes = None
229+
imghandle = None
230+
if isinstance(img, str):
231+
imghandle = open(img, "rb")
232+
elif isinstance(img, bytes):
233+
imgbytes = img
234+
data = {}
235+
image = None
236+
if imghandle:
237+
image = Image.open(imghandle)
238+
elif imgbytes:
239+
image = Image.open(io.BytesIO(imgbytes))
240+
data["format"] = image.format.lower()
241+
if data["format"] == "tiff":
242+
data["format"] = "tif"
243+
data["width"] = image.width
244+
data["height"] = image.height
245+
metadata = is_enhanced(image)
246+
if metadata:
247+
data = save_tif_stripped(image, data, metadata)
248+
else:
249+
buff = io.BytesIO()
250+
image.save(buff, "PNG")
251+
buff.seek(0)
252+
data["file_data"] = buff.read()
253+
if imghandle:
254+
imghandle.close()
255+
return data
58256

59257

60-
def enve_image_to_data(img, guid=None):
258+
def image_to_data(img):
61259
# Convert enve image object into a dictionary of image data or None
62260
# The dictionary has the keys:
63261
# 'width' = x pixel count
64262
# 'height' = y pixel count
65263
# 'format' = 'tif' or 'png'
66264
# 'file_data' = a byte array of the raw image (same content as disk file)
67-
if has_enve and has_qt: # pragma: no cover
265+
data = None
266+
if has_enve: # pragma: no cover
68267
if isinstance(img, enve.image):
69268
data = dict(width=img.dims[0], height=img.dims[1])
70269
if img.enhanced:
@@ -80,22 +279,8 @@ def enve_image_to_data(img, guid=None):
80279
return data
81280
except OSError:
82281
return None
83-
else:
84-
# convert to QImage via ppm string I/O
85-
tmpimg = QtGui.QImage.fromData(img.ppm(), "ppm")
86-
# record the guid in the image (watermark it)
87-
# note: the Qt PNG format supports text keys
88-
tmpimg.setText("CEI_REPORTS_GUID", guid)
89-
# save it in PNG format in memory
90-
be = QtCore.QByteArray()
91-
buf = QtCore.QBuffer(be)
92-
buf.open(QtCore.QIODevice.WriteOnly)
93-
tmpimg.save(buf, "png")
94-
buf.close()
95-
data["format"] = "png"
96-
data["file_data"] = buf.data() # returns a bytes() instance
97-
return data
98-
return None
282+
if not data:
283+
return PIL_image_to_data(img)
99284

100285

101286
def enve_arch():

tests/test_report_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ def test_encode_decode() -> bool:
2727

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

3333

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

3939

4040
@pytest.mark.ado_test

0 commit comments

Comments
 (0)