99import hashlib
1010import os
1111
12+ try :
13+ from PIL import Image as PIL_Image
14+ except ImportError :
15+ import Image as PIL_Image
16+
17+ from docx .opc .constants import CONTENT_TYPE as CT
1218from docx .opc .package import Part
1319from docx .shared import lazyproperty
1420
@@ -17,10 +23,17 @@ class Image(object):
1723 """
1824 A helper object that knows how to analyze an image file.
1925 """
20- def __init__ (self , blob , filename ):
26+ def __init__ (
27+ self , blob , filename , content_type , px_width , px_height ,
28+ horz_dpi , vert_dpi ):
2129 super (Image , self ).__init__ ()
2230 self ._blob = blob
2331 self ._filename = filename
32+ self ._content_type = content_type
33+ self ._px_width = px_width
34+ self ._px_height = px_height
35+ self ._horz_dpi = horz_dpi
36+ self ._vert_dpi = vert_dpi
2437
2538 @property
2639 def blob (self ):
@@ -34,16 +47,16 @@ def content_type(self):
3447 """
3548 The MIME type of the image, e.g. 'image/png'.
3649 """
37- raise NotImplementedError
50+ return self . _content_type
3851
39- @property
52+ @lazyproperty
4053 def ext (self ):
4154 """
4255 The file extension for the image. If an actual one is available from
4356 a load filename it is used. Otherwise a canonical extension is
4457 assigned based on the content type.
4558 """
46- raise NotImplementedError
59+ return os . path . splitext ( self . _filename )[ 1 ]
4760
4861 @property
4962 def filename (self ):
@@ -53,23 +66,37 @@ def filename(self):
5366 """
5467 return self ._filename
5568
69+ @property
70+ def horz_dpi (self ):
71+ """
72+ The horizontal dots per inch (dpi) of the image, defaults to 72 when
73+ no dpi information is stored in the image, as is often the case.
74+ """
75+ return self ._horz_dpi
76+
5677 @classmethod
5778 def load (cls , image_descriptor ):
5879 """
5980 Return a new |Image| instance loaded from the image file identified
6081 by *image_descriptor*, a path or file-like object.
6182 """
6283 if isinstance (image_descriptor , basestring ):
63- path = image_descriptor
64- with open (path , 'rb' ) as f :
65- blob = f .read ()
66- filename = os .path .basename (path )
67- else :
68- stream = image_descriptor
69- stream .seek (0 )
70- blob = stream .read ()
71- filename = None
72- return cls (blob , filename )
84+ return cls ._load_from_path (image_descriptor )
85+ return cls ._load_from_stream (image_descriptor )
86+
87+ @property
88+ def px_width (self ):
89+ """
90+ The horizontal pixel dimension of the image
91+ """
92+ return self ._px_width
93+
94+ @property
95+ def px_height (self ):
96+ """
97+ The vertical pixel dimension of the image
98+ """
99+ return self ._px_height
73100
74101 @lazyproperty
75102 def sha1 (self ):
@@ -78,6 +105,91 @@ def sha1(self):
78105 """
79106 return hashlib .sha1 (self ._blob ).hexdigest ()
80107
108+ @property
109+ def vert_dpi (self ):
110+ """
111+ The vertical dots per inch (dpi) of the image, defaults to 72 when no
112+ dpi information is stored in the image.
113+ """
114+ return self ._vert_dpi
115+
116+ @classmethod
117+ def _analyze_image (cls , stream ):
118+ pil_image = cls ._open_pillow_image (stream )
119+ content_type = cls ._format_content_type (pil_image .format )
120+ px_width , px_height = pil_image .size
121+ try :
122+ horz_dpi , vert_dpi = pil_image .info .get ('dpi' )
123+ except :
124+ horz_dpi , vert_dpi = (72 , 72 )
125+ return content_type , px_width , px_height , horz_dpi , vert_dpi
126+
127+ @classmethod
128+ def _def_mime_ext (cls , mime_type ):
129+ """
130+ Return the default file extension, e.g. ``'.png'``, corresponding to
131+ *mime_type*. Raises |KeyError| for unsupported image types.
132+ """
133+ content_type_extensions = {
134+ CT .BMP : '.bmp' , CT .GIF : '.gif' , CT .JPEG : '.jpg' , CT .PNG : '.png' ,
135+ CT .TIFF : '.tiff' , CT .X_WMF : '.wmf'
136+ }
137+ return content_type_extensions [mime_type ]
138+
139+ @classmethod
140+ def _format_content_type (cls , format ):
141+ """
142+ Return the content type string (MIME type for images) corresponding
143+ to the Pillow image format string *format*.
144+ """
145+ format_content_types = {
146+ 'BMP' : CT .BMP , 'GIF' : CT .GIF , 'JPEG' : CT .JPEG , 'PNG' : CT .PNG ,
147+ 'TIFF' : CT .TIFF , 'WMF' : CT .X_WMF
148+ }
149+ return format_content_types [format ]
150+
151+ @classmethod
152+ def _load_from_path (cls , path ):
153+ with open (path , 'rb' ) as f :
154+ blob = f .read ()
155+ content_type , px_width , px_height , horz_dpi , vert_dpi = (
156+ cls ._analyze_image (f )
157+ )
158+ filename = os .path .basename (path )
159+ return cls (
160+ blob , filename , content_type , px_width , px_height , horz_dpi ,
161+ vert_dpi
162+ )
163+
164+ @classmethod
165+ def _load_from_stream (cls , stream ):
166+ stream .seek (0 )
167+ blob = stream .read ()
168+ content_type , px_width , px_height , horz_dpi , vert_dpi = (
169+ cls ._analyze_image (stream )
170+ )
171+ filename = 'image%s' % cls ._def_mime_ext (content_type )
172+ return cls (
173+ blob , filename , content_type , px_width , px_height , horz_dpi ,
174+ vert_dpi
175+ )
176+
177+ @classmethod
178+ def _open_pillow_image (cls , stream ):
179+ """
180+ Return a Pillow ``Image`` instance loaded from the image file-like
181+ object *stream*. The image is validated to confirm it is a supported
182+ image type.
183+ """
184+ stream .seek (0 )
185+ pil_image = PIL_Image .open (stream )
186+ try :
187+ cls ._format_content_type (pil_image .format )
188+ except KeyError :
189+ tmpl = "unsupported image format '%s'"
190+ raise ValueError (tmpl % (pil_image .format ))
191+ return pil_image
192+
81193
82194class ImagePart (Part ):
83195 """
0 commit comments