Skip to content

Commit 78767ef

Browse files
committed
Add basic support for adding SVG pictures to docx files
See issues #351, #651, #659.
1 parent 57d3b9e commit 78767ef

File tree

6 files changed

+96
-4
lines changed

6 files changed

+96
-4
lines changed

src/docx/image/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from docx.image.jpeg import Exif, Jfif
1010
from docx.image.png import Png
1111
from docx.image.tiff import Tiff
12+
from docx.image.svg import Svg
1213

1314
SIGNATURES = (
1415
# class, offset, signature_bytes
@@ -20,4 +21,5 @@
2021
(Tiff, 0, b"MM\x00*"), # big-endian (Motorola) TIFF
2122
(Tiff, 0, b"II*\x00"), # little-endian (Intel) TIFF
2223
(Bmp, 0, b"BM"),
24+
(Svg, 0, b"<svg "),
2325
)

src/docx/image/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class MIME_TYPE:
105105
JPEG = "image/jpeg"
106106
PNG = "image/png"
107107
TIFF = "image/tiff"
108+
SVG = "image/svg+xml"
108109

109110

110111
class PNG_CHUNK_TYPE:

src/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

src/docx/oxml/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444

4545
register_element_cls("a:blip", CT_Blip)
4646
register_element_cls("a:ext", CT_PositiveSize2D)
47+
register_element_cls("a:extLst", CT_Transform2D)
48+
register_element_cls("asvg:svgBlip", CT_Transform2D)
4749
register_element_cls("a:graphic", CT_GraphicalObject)
4850
register_element_cls("a:graphicData", CT_GraphicalObjectData)
4951
register_element_cls("a:off", CT_Point2D)

src/docx/oxml/ns.py

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

2627
pfxmap = {value: key for key, value in nsmap.items()}

src/docx/oxml/shape.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class CT_Blip(BaseOxmlElement):
3636

3737
embed = OptionalAttribute("r:embed", ST_RelationshipId)
3838
link = OptionalAttribute("r:link", ST_RelationshipId)
39+
extLst = ZeroOrOne("a:extLst")
3940

4041

4142
class CT_BlipFillProperties(BaseOxmlElement):
@@ -105,7 +106,7 @@ def _inline_xml(cls):
105106
" <a:graphic>\n"
106107
' <a:graphicData uri="URI not set"/>\n'
107108
" </a:graphic>\n"
108-
"</wp:inline>" % nsdecls("wp", "a", "pic", "r")
109+
"</wp:inline>" % nsdecls("wp", "a", "pic", "r", "asvg")
109110
)
110111

111112

@@ -135,14 +136,48 @@ def new(cls, pic_id, filename, rId, cx, cy):
135136
"""Return a new ``<pic:pic>`` element populated with the minimal contents
136137
required to define a viable picture element, based on the values passed as
137138
parameters."""
138-
pic = parse_xml(cls._pic_xml())
139+
if filename.endswith(".svg"):
140+
pic = parse_xml(cls._pic_xml_svg())
141+
pic.blipFill.blip.extLst.ext.svgBlip.embed = rId
142+
else:
143+
pic = parse_xml(cls._pic_xml())
144+
pic.blipFill.blip.embed = rId
139145
pic.nvPicPr.cNvPr.id = pic_id
140146
pic.nvPicPr.cNvPr.name = filename
141-
pic.blipFill.blip.embed = rId
142147
pic.spPr.cx = cx
143148
pic.spPr.cy = cy
144149
return pic
145150

151+
@classmethod
152+
def _pic_xml_svg(cls):
153+
return (
154+
"<pic:pic %s>\n"
155+
" <pic:nvPicPr>\n"
156+
' <pic:cNvPr id="666" name="unnamed"/>\n'
157+
" <pic:cNvPicPr/>\n"
158+
" </pic:nvPicPr>\n"
159+
" <pic:blipFill>\n"
160+
" <a:blip>\n"
161+
" <a:extLst>\n"
162+
' <a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">\n'
163+
" <asvg:svgBlip/>\n"
164+
" </a:ext>\n"
165+
" </a:extLst>\n"
166+
" </a:blip>\n"
167+
" <a:stretch>\n"
168+
" <a:fillRect/>\n"
169+
" </a:stretch>\n"
170+
" </pic:blipFill>\n"
171+
" <pic:spPr>\n"
172+
" <a:xfrm>\n"
173+
' <a:off x="0" y="0"/>\n'
174+
' <a:ext cx="914400" cy="914400"/>\n'
175+
" </a:xfrm>\n"
176+
' <a:prstGeom prst="rect"/>\n'
177+
" </pic:spPr>\n"
178+
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
179+
)
180+
146181
@classmethod
147182
def _pic_xml(cls):
148183
return (
@@ -164,7 +199,7 @@ def _pic_xml(cls):
164199
" </a:xfrm>\n"
165200
' <a:prstGeom prst="rect"/>\n'
166201
" </pic:spPr>\n"
167-
"</pic:pic>" % nsdecls("pic", "a", "r")
202+
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
168203
)
169204

170205

@@ -192,6 +227,7 @@ class CT_PositiveSize2D(BaseOxmlElement):
192227

193228
cx = RequiredAttribute("cx", ST_PositiveCoordinate)
194229
cy = RequiredAttribute("cy", ST_PositiveCoordinate)
230+
svgBlip = ZeroOrOne("asvg:svgBlip")
195231

196232

197233
class CT_PresetGeometry2D(BaseOxmlElement):
@@ -258,6 +294,7 @@ class CT_Transform2D(BaseOxmlElement):
258294

259295
off = ZeroOrOne("a:off", successors=("a:ext",))
260296
ext = ZeroOrOne("a:ext", successors=())
297+
embed = OptionalAttribute("r:embed", ST_RelationshipId)
261298

262299
@property
263300
def cx(self):

0 commit comments

Comments
 (0)