11import array
22import base64
33from html .parser import HTMLParser as BaseHTMLParser
4+ import io
5+ import json
46import os
57import os .path
68import platform
911import tempfile
1012from typing import List , Optional
1113
14+ from PIL import Image
15+ from PIL .TiffTags import TAGS
1216import requests
1317
1418try :
1923except (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-
2926try :
3027 import numpy
3128
3229 has_numpy = True
3330except ImportError :
3431 has_numpy = False
35-
32+ TIFFTAG_IMAGEDESCRIPTION : int = 0x010E
3633text_type = str
3734"""@package report_utils
3835Methods 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
101286def enve_arch ():
0 commit comments