Skip to content

Commit c7792d6

Browse files
committed
Add basic support for adding SVG pictures to docx files
See issues python-openxml#351, python-openxml#651, python-openxml#659.
1 parent 0cf6d71 commit c7792d6

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
@@ -40,6 +40,7 @@ class CT_Blip(BaseOxmlElement):
4040
link: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
4141
"r:link", ST_RelationshipId
4242
)
43+
extLst = ZeroOrOne("a:extLst")
4344

4445

4546
class CT_BlipFillProperties(BaseOxmlElement):
@@ -115,7 +116,7 @@ def _inline_xml(cls):
115116
" <a:graphic>\n"
116117
' <a:graphicData uri="URI not set"/>\n'
117118
" </a:graphic>\n"
118-
"</wp:inline>" % nsdecls("wp", "a", "pic", "r")
119+
"</wp:inline>" % nsdecls("wp", "a", "pic", "r", "asvg")
119120
)
120121

121122

@@ -149,14 +150,48 @@ def new(cls, pic_id, filename, rId, cx, cy):
149150
"""Return a new ``<pic:pic>`` element populated with the minimal contents
150151
required to define a viable picture element, based on the values passed as
151152
parameters."""
152-
pic = parse_xml(cls._pic_xml())
153+
if filename.endswith(".svg"):
154+
pic = parse_xml(cls._pic_xml_svg())
155+
pic.blipFill.blip.extLst.ext.svgBlip.embed = rId
156+
else:
157+
pic = parse_xml(cls._pic_xml())
158+
pic.blipFill.blip.embed = rId
153159
pic.nvPicPr.cNvPr.id = pic_id
154160
pic.nvPicPr.cNvPr.name = filename
155-
pic.blipFill.blip.embed = rId
156161
pic.spPr.cx = cx
157162
pic.spPr.cy = cy
158163
return pic
159164

165+
@classmethod
166+
def _pic_xml_svg(cls):
167+
return (
168+
"<pic:pic %s>\n"
169+
" <pic:nvPicPr>\n"
170+
' <pic:cNvPr id="666" name="unnamed"/>\n'
171+
" <pic:cNvPicPr/>\n"
172+
" </pic:nvPicPr>\n"
173+
" <pic:blipFill>\n"
174+
" <a:blip>\n"
175+
" <a:extLst>\n"
176+
' <a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">\n'
177+
" <asvg:svgBlip/>\n"
178+
" </a:ext>\n"
179+
" </a:extLst>\n"
180+
" </a:blip>\n"
181+
" <a:stretch>\n"
182+
" <a:fillRect/>\n"
183+
" </a:stretch>\n"
184+
" </pic:blipFill>\n"
185+
" <pic:spPr>\n"
186+
" <a:xfrm>\n"
187+
' <a:off x="0" y="0"/>\n'
188+
' <a:ext cx="914400" cy="914400"/>\n'
189+
" </a:xfrm>\n"
190+
' <a:prstGeom prst="rect"/>\n'
191+
" </pic:spPr>\n"
192+
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
193+
)
194+
160195
@classmethod
161196
def _pic_xml(cls):
162197
return (
@@ -178,7 +213,7 @@ def _pic_xml(cls):
178213
" </a:xfrm>\n"
179214
' <a:prstGeom prst="rect"/>\n'
180215
" </pic:spPr>\n"
181-
"</pic:pic>" % nsdecls("pic", "a", "r")
216+
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
182217
)
183218

184219

@@ -210,6 +245,7 @@ class CT_PositiveSize2D(BaseOxmlElement):
210245
cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
211246
"cy", ST_PositiveCoordinate
212247
)
248+
svgBlip = ZeroOrOne("asvg:svgBlip")
213249

214250

215251
class CT_PresetGeometry2D(BaseOxmlElement):
@@ -276,6 +312,7 @@ class CT_Transform2D(BaseOxmlElement):
276312

277313
off = ZeroOrOne("a:off", successors=("a:ext",))
278314
ext = ZeroOrOne("a:ext", successors=())
315+
embed = OptionalAttribute("r:embed", ST_RelationshipId)
279316

280317
@property
281318
def cx(self):

0 commit comments

Comments
 (0)