Skip to content

Commit 8f54818

Browse files
committed
Add basic support for adding SVG pictures to docx files
See issues #351, #651, #659.
1 parent 36cac78 commit 8f54818

File tree

6 files changed

+68
-4
lines changed

6 files changed

+68
-4
lines changed

docx/image/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from docx.image.jpeg import Exif, Jfif
1515
from docx.image.png import Png
1616
from docx.image.tiff import Tiff
17+
from docx.image.svg import Svg
1718

1819

1920
SIGNATURES = (
@@ -26,4 +27,5 @@
2627
(Tiff, 0, b'MM\x00*'), # big-endian (Motorola) TIFF
2728
(Tiff, 0, b'II*\x00'), # little-endian (Intel) TIFF
2829
(Bmp, 0, b'BM'),
30+
(Svg, 0, b'<svg '),
2931
)

docx/image/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class MIME_TYPE(object):
102102
JPEG = 'image/jpeg'
103103
PNG = 'image/png'
104104
TIFF = 'image/tiff'
105+
SVG = 'image/svg+xml'
105106

106107

107108
class PNG_CHUNK_TYPE(object):

docx/image/svg.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# encoding: utf-8
2+
3+
from __future__ import absolute_import, division, print_function
4+
5+
import xml.etree.ElementTree as ET
6+
7+
from .constants import MIME_TYPE
8+
from .image import BaseImageHeader
9+
10+
11+
class Svg(BaseImageHeader):
12+
"""
13+
Image header parser for SVG images.
14+
"""
15+
16+
@classmethod
17+
def from_stream(cls, stream):
18+
"""
19+
Return |Svg| instance having header properties parsed from SVG image
20+
in *stream*.
21+
"""
22+
px_width, px_height = cls._dimensions_from_stream(stream)
23+
return cls(px_width, px_height, 72, 72)
24+
25+
@property
26+
def content_type(self):
27+
"""
28+
MIME content type for this image, unconditionally `image/svg+xml` for
29+
SVG images.
30+
"""
31+
return MIME_TYPE.SVG
32+
33+
@property
34+
def default_ext(self):
35+
"""
36+
Default filename extension, always 'svg' for SVG images.
37+
"""
38+
return "svg"
39+
40+
@classmethod
41+
def _dimensions_from_stream(cls, stream):
42+
stream.seek(0)
43+
data = stream.read()
44+
root = ET.fromstring(data)
45+
# FIXME: The width could be expressed as '4cm'
46+
# See https://www.w3.org/TR/SVG11/struct.html#NewDocument
47+
width = int(root.attrib["width"])
48+
height = int(root.attrib["height"])
49+
return width, height

docx/oxml/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
122122
)
123123
register_element_cls('a:blip', CT_Blip)
124124
register_element_cls('a:ext', CT_PositiveSize2D)
125+
register_element_cls('a:extLst', CT_Transform2D)
126+
register_element_cls('asvg:svgBlip', CT_Transform2D)
125127
register_element_cls('a:graphic', CT_GraphicalObject)
126128
register_element_cls('a:graphicData', CT_GraphicalObjectData)
127129
register_element_cls('a:off', CT_Point2D)

docx/oxml/ns.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
2525
"xml": "http://www.w3.org/XML/1998/namespace",
2626
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
27+
"asvg": "http://schemas.microsoft.com/office/drawing/2016/SVG/main",
2728
}
2829

2930
pfxmap = dict((value, key) for key, value in nsmap.items())

docx/oxml/shape.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class CT_Blip(BaseOxmlElement):
2323
"""
2424
embed = OptionalAttribute('r:embed', ST_RelationshipId)
2525
link = OptionalAttribute('r:link', ST_RelationshipId)
26+
extLst = ZeroOrOne('a:extLst')
2627

2728

2829
class CT_BlipFillProperties(BaseOxmlElement):
@@ -98,7 +99,7 @@ def _inline_xml(cls):
9899
' <a:graphic>\n'
99100
' <a:graphicData uri="URI not set"/>\n'
100101
' </a:graphic>\n'
101-
'</wp:inline>' % nsdecls('wp', 'a', 'pic', 'r')
102+
'</wp:inline>' % nsdecls('wp', 'a', 'pic', 'r', 'asvg')
102103
)
103104

104105

@@ -136,7 +137,7 @@ def new(cls, pic_id, filename, rId, cx, cy):
136137
pic = parse_xml(cls._pic_xml())
137138
pic.nvPicPr.cNvPr.id = pic_id
138139
pic.nvPicPr.cNvPr.name = filename
139-
pic.blipFill.blip.embed = rId
140+
pic.blipFill.blip.extLst.ext.svgBlip.embed = rId
140141
pic.spPr.cx = cx
141142
pic.spPr.cy = cy
142143
return pic
@@ -150,7 +151,13 @@ def _pic_xml(cls):
150151
' <pic:cNvPicPr/>\n'
151152
' </pic:nvPicPr>\n'
152153
' <pic:blipFill>\n'
153-
' <a:blip/>\n'
154+
' <a:blip>\n'
155+
' <a:extLst>\n'
156+
' <a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">\n'
157+
' <asvg:svgBlip/>\n'
158+
' </a:ext>\n'
159+
' </a:extLst>\n'
160+
' </a:blip>\n'
154161
' <a:stretch>\n'
155162
' <a:fillRect/>\n'
156163
' </a:stretch>\n'
@@ -162,7 +169,7 @@ def _pic_xml(cls):
162169
' </a:xfrm>\n'
163170
' <a:prstGeom prst="rect"/>\n'
164171
' </pic:spPr>\n'
165-
'</pic:pic>' % nsdecls('pic', 'a', 'r')
172+
'</pic:pic>' % nsdecls('pic', 'a', 'r', 'asvg')
166173
)
167174

168175

@@ -189,6 +196,7 @@ class CT_PositiveSize2D(BaseOxmlElement):
189196
"""
190197
cx = RequiredAttribute('cx', ST_PositiveCoordinate)
191198
cy = RequiredAttribute('cy', ST_PositiveCoordinate)
199+
svgBlip = ZeroOrOne('asvg:svgBlip')
192200

193201

194202
class CT_PresetGeometry2D(BaseOxmlElement):
@@ -258,6 +266,7 @@ class CT_Transform2D(BaseOxmlElement):
258266
"""
259267
off = ZeroOrOne('a:off', successors=('a:ext',))
260268
ext = ZeroOrOne('a:ext', successors=())
269+
embed = OptionalAttribute('r:embed', ST_RelationshipId)
261270

262271
@property
263272
def cx(self):

0 commit comments

Comments
 (0)