Skip to content

Commit 9ca490f

Browse files
author
Steve Canny
committed
img: extract Image to sub-package
1 parent 60d5156 commit 9ca490f

File tree

5 files changed

+275
-245
lines changed

5 files changed

+275
-245
lines changed

docx/image/__init__.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Provides objects that can characterize image streams as to content type and
5+
size, as a required step in including them in a document.
6+
"""
7+
8+
from __future__ import (
9+
absolute_import, division, print_function, unicode_literals
10+
)
11+
12+
import hashlib
13+
import os
14+
15+
try:
16+
from PIL import Image as PIL_Image
17+
except ImportError:
18+
import Image as PIL_Image
19+
20+
from docx.compat import BytesIO, is_string
21+
from docx.opc.constants import CONTENT_TYPE as CT
22+
from docx.shared import lazyproperty
23+
24+
25+
class Image(object):
26+
"""
27+
A helper object that knows how to analyze an image file.
28+
"""
29+
def __init__(
30+
self, blob, filename, content_type, px_width, px_height,
31+
horz_dpi, vert_dpi):
32+
super(Image, self).__init__()
33+
self._blob = blob
34+
self._filename = filename
35+
self._content_type = content_type
36+
self._px_width = px_width
37+
self._px_height = px_height
38+
self._horz_dpi = horz_dpi
39+
self._vert_dpi = vert_dpi
40+
41+
@property
42+
def blob(self):
43+
"""
44+
The bytes of the image 'file'
45+
"""
46+
return self._blob
47+
48+
@property
49+
def content_type(self):
50+
"""
51+
The MIME type of the image, e.g. 'image/png'.
52+
"""
53+
return self._content_type
54+
55+
@lazyproperty
56+
def ext(self):
57+
"""
58+
The file extension for the image. If an actual one is available from
59+
a load filename it is used. Otherwise a canonical extension is
60+
assigned based on the content type.
61+
"""
62+
return os.path.splitext(self._filename)[1]
63+
64+
@property
65+
def filename(self):
66+
"""
67+
Original image file name, if loaded from disk, or a generic filename
68+
if loaded from an anonymous stream.
69+
"""
70+
return self._filename
71+
72+
@classmethod
73+
def from_blob(cls, blob):
74+
stream = BytesIO(blob)
75+
return cls._from_stream(stream, blob)
76+
77+
@classmethod
78+
def from_file(cls, image_descriptor):
79+
"""
80+
Return a new |Image| instance loaded from the image file identified
81+
by *image_descriptor*, a path or file-like object.
82+
"""
83+
if is_string(image_descriptor):
84+
path = image_descriptor
85+
with open(path, 'rb') as f:
86+
blob = f.read()
87+
stream = BytesIO(blob)
88+
filename = os.path.basename(path)
89+
else:
90+
stream = image_descriptor
91+
stream.seek(0)
92+
blob = stream.read()
93+
filename = None
94+
return cls._from_stream(stream, blob, filename)
95+
96+
@property
97+
def horz_dpi(self):
98+
"""
99+
The horizontal dots per inch (dpi) of the image, defaults to 72 when
100+
no dpi information is stored in the image, as is often the case.
101+
"""
102+
return self._horz_dpi
103+
104+
@property
105+
def px_width(self):
106+
"""
107+
The horizontal pixel dimension of the image
108+
"""
109+
return self._px_width
110+
111+
@property
112+
def px_height(self):
113+
"""
114+
The vertical pixel dimension of the image
115+
"""
116+
return self._px_height
117+
118+
@lazyproperty
119+
def sha1(self):
120+
"""
121+
SHA1 hash digest of the image blob
122+
"""
123+
return hashlib.sha1(self._blob).hexdigest()
124+
125+
@property
126+
def vert_dpi(self):
127+
"""
128+
The vertical dots per inch (dpi) of the image, defaults to 72 when no
129+
dpi information is stored in the image.
130+
"""
131+
return self._vert_dpi
132+
133+
@classmethod
134+
def _analyze_image(cls, stream):
135+
pil_image = cls._open_pillow_image(stream)
136+
content_type = cls._format_content_type(pil_image.format)
137+
px_width, px_height = pil_image.size
138+
try:
139+
horz_dpi, vert_dpi = pil_image.info.get('dpi')
140+
except:
141+
horz_dpi, vert_dpi = (72, 72)
142+
return content_type, px_width, px_height, horz_dpi, vert_dpi
143+
144+
@classmethod
145+
def _def_mime_ext(cls, mime_type):
146+
"""
147+
Return the default file extension, e.g. ``'.png'``, corresponding to
148+
*mime_type*. Raises |KeyError| for unsupported image types.
149+
"""
150+
content_type_extensions = {
151+
CT.BMP: '.bmp', CT.GIF: '.gif', CT.JPEG: '.jpg', CT.PNG: '.png',
152+
CT.TIFF: '.tiff', CT.X_WMF: '.wmf'
153+
}
154+
return content_type_extensions[mime_type]
155+
156+
@classmethod
157+
def _format_content_type(cls, format):
158+
"""
159+
Return the content type string (MIME type for images) corresponding
160+
to the Pillow image format string *format*.
161+
"""
162+
format_content_types = {
163+
'BMP': CT.BMP, 'GIF': CT.GIF, 'JPEG': CT.JPEG, 'PNG': CT.PNG,
164+
'TIFF': CT.TIFF, 'WMF': CT.X_WMF
165+
}
166+
return format_content_types[format]
167+
168+
@classmethod
169+
def _from_stream(cls, stream, blob, filename=None):
170+
content_type, px_width, px_height, horz_dpi, vert_dpi = (
171+
cls._analyze_image(stream)
172+
)
173+
if filename is None:
174+
filename = 'image%s' % cls._def_mime_ext(content_type)
175+
return cls(
176+
blob, filename, content_type, px_width, px_height, horz_dpi,
177+
vert_dpi
178+
)
179+
180+
@classmethod
181+
def _open_pillow_image(cls, stream):
182+
"""
183+
Return a Pillow ``Image`` instance loaded from the image file-like
184+
object *stream*. The image is validated to confirm it is a supported
185+
image type.
186+
"""
187+
stream.seek(0)
188+
pil_image = PIL_Image.open(stream)
189+
try:
190+
cls._format_content_type(pil_image.format)
191+
except KeyError:
192+
tmpl = "unsupported image format '%s'"
193+
raise ValueError(tmpl % (pil_image.format))
194+
return pil_image

docx/parts/image.py

Lines changed: 2 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -9,189 +9,10 @@
99
)
1010

1111
import hashlib
12-
import os
1312

14-
try:
15-
from PIL import Image as PIL_Image
16-
except ImportError:
17-
import Image as PIL_Image
18-
19-
from docx.compat import BytesIO, is_string
20-
from docx.opc.constants import CONTENT_TYPE as CT
13+
from docx.image import Image
2114
from docx.opc.package import Part
22-
from docx.shared import Emu, Inches, lazyproperty
23-
24-
25-
class Image(object):
26-
"""
27-
A helper object that knows how to analyze an image file.
28-
"""
29-
def __init__(
30-
self, blob, filename, content_type, px_width, px_height,
31-
horz_dpi, vert_dpi):
32-
super(Image, self).__init__()
33-
self._blob = blob
34-
self._filename = filename
35-
self._content_type = content_type
36-
self._px_width = px_width
37-
self._px_height = px_height
38-
self._horz_dpi = horz_dpi
39-
self._vert_dpi = vert_dpi
40-
41-
@property
42-
def blob(self):
43-
"""
44-
The bytes of the image 'file'
45-
"""
46-
return self._blob
47-
48-
@property
49-
def content_type(self):
50-
"""
51-
The MIME type of the image, e.g. 'image/png'.
52-
"""
53-
return self._content_type
54-
55-
@lazyproperty
56-
def ext(self):
57-
"""
58-
The file extension for the image. If an actual one is available from
59-
a load filename it is used. Otherwise a canonical extension is
60-
assigned based on the content type.
61-
"""
62-
return os.path.splitext(self._filename)[1]
63-
64-
@property
65-
def filename(self):
66-
"""
67-
Original image file name, if loaded from disk, or a generic filename
68-
if loaded from an anonymous stream.
69-
"""
70-
return self._filename
71-
72-
@classmethod
73-
def from_blob(cls, blob):
74-
stream = BytesIO(blob)
75-
return cls._from_stream(stream, blob)
76-
77-
@classmethod
78-
def from_file(cls, image_descriptor):
79-
"""
80-
Return a new |Image| instance loaded from the image file identified
81-
by *image_descriptor*, a path or file-like object.
82-
"""
83-
if is_string(image_descriptor):
84-
path = image_descriptor
85-
with open(path, 'rb') as f:
86-
blob = f.read()
87-
stream = BytesIO(blob)
88-
filename = os.path.basename(path)
89-
else:
90-
stream = image_descriptor
91-
stream.seek(0)
92-
blob = stream.read()
93-
filename = None
94-
return cls._from_stream(stream, blob, filename)
95-
96-
@property
97-
def horz_dpi(self):
98-
"""
99-
The horizontal dots per inch (dpi) of the image, defaults to 72 when
100-
no dpi information is stored in the image, as is often the case.
101-
"""
102-
return self._horz_dpi
103-
104-
@property
105-
def px_width(self):
106-
"""
107-
The horizontal pixel dimension of the image
108-
"""
109-
return self._px_width
110-
111-
@property
112-
def px_height(self):
113-
"""
114-
The vertical pixel dimension of the image
115-
"""
116-
return self._px_height
117-
118-
@lazyproperty
119-
def sha1(self):
120-
"""
121-
SHA1 hash digest of the image blob
122-
"""
123-
return hashlib.sha1(self._blob).hexdigest()
124-
125-
@property
126-
def vert_dpi(self):
127-
"""
128-
The vertical dots per inch (dpi) of the image, defaults to 72 when no
129-
dpi information is stored in the image.
130-
"""
131-
return self._vert_dpi
132-
133-
@classmethod
134-
def _analyze_image(cls, stream):
135-
pil_image = cls._open_pillow_image(stream)
136-
content_type = cls._format_content_type(pil_image.format)
137-
px_width, px_height = pil_image.size
138-
try:
139-
horz_dpi, vert_dpi = pil_image.info.get('dpi')
140-
except:
141-
horz_dpi, vert_dpi = (72, 72)
142-
return content_type, px_width, px_height, horz_dpi, vert_dpi
143-
144-
@classmethod
145-
def _def_mime_ext(cls, mime_type):
146-
"""
147-
Return the default file extension, e.g. ``'.png'``, corresponding to
148-
*mime_type*. Raises |KeyError| for unsupported image types.
149-
"""
150-
content_type_extensions = {
151-
CT.BMP: '.bmp', CT.GIF: '.gif', CT.JPEG: '.jpg', CT.PNG: '.png',
152-
CT.TIFF: '.tiff', CT.X_WMF: '.wmf'
153-
}
154-
return content_type_extensions[mime_type]
155-
156-
@classmethod
157-
def _format_content_type(cls, format):
158-
"""
159-
Return the content type string (MIME type for images) corresponding
160-
to the Pillow image format string *format*.
161-
"""
162-
format_content_types = {
163-
'BMP': CT.BMP, 'GIF': CT.GIF, 'JPEG': CT.JPEG, 'PNG': CT.PNG,
164-
'TIFF': CT.TIFF, 'WMF': CT.X_WMF
165-
}
166-
return format_content_types[format]
167-
168-
@classmethod
169-
def _from_stream(cls, stream, blob, filename=None):
170-
content_type, px_width, px_height, horz_dpi, vert_dpi = (
171-
cls._analyze_image(stream)
172-
)
173-
if filename is None:
174-
filename = 'image%s' % cls._def_mime_ext(content_type)
175-
return cls(
176-
blob, filename, content_type, px_width, px_height, horz_dpi,
177-
vert_dpi
178-
)
179-
180-
@classmethod
181-
def _open_pillow_image(cls, stream):
182-
"""
183-
Return a Pillow ``Image`` instance loaded from the image file-like
184-
object *stream*. The image is validated to confirm it is a supported
185-
image type.
186-
"""
187-
stream.seek(0)
188-
pil_image = PIL_Image.open(stream)
189-
try:
190-
cls._format_content_type(pil_image.format)
191-
except KeyError:
192-
tmpl = "unsupported image format '%s'"
193-
raise ValueError(tmpl % (pil_image.format))
194-
return pil_image
15+
from docx.shared import Emu, Inches
19516

19617

19718
class ImagePart(Part):

tests/image/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)