Skip to content

Commit

Permalink
Add basic support for adding SVG pictures to docx files
Browse files Browse the repository at this point in the history
See issues #351, #651, #659.
  • Loading branch information
takis committed Feb 22, 2024
1 parent 57d3b9e commit 78767ef
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/docx/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from docx.image.jpeg import Exif, Jfif
from docx.image.png import Png
from docx.image.tiff import Tiff
from docx.image.svg import Svg

SIGNATURES = (
# class, offset, signature_bytes
Expand All @@ -20,4 +21,5 @@
(Tiff, 0, b"MM\x00*"), # big-endian (Motorola) TIFF
(Tiff, 0, b"II*\x00"), # little-endian (Intel) TIFF
(Bmp, 0, b"BM"),
(Svg, 0, b"<svg "),
)
1 change: 1 addition & 0 deletions src/docx/image/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class MIME_TYPE:
JPEG = "image/jpeg"
PNG = "image/png"
TIFF = "image/tiff"
SVG = "image/svg+xml"


class PNG_CHUNK_TYPE:
Expand Down
49 changes: 49 additions & 0 deletions src/docx/image/svg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# encoding: utf-8

from __future__ import absolute_import, division, print_function

import xml.etree.ElementTree as ET

from .constants import MIME_TYPE
from .image import BaseImageHeader


class Svg(BaseImageHeader):
"""
Image header parser for SVG images.
"""

@classmethod
def from_stream(cls, stream):
"""
Return |Svg| instance having header properties parsed from SVG image
in *stream*.
"""
px_width, px_height = cls._dimensions_from_stream(stream)
return cls(px_width, px_height, 72, 72)

@property
def content_type(self):
"""
MIME content type for this image, unconditionally `image/svg+xml` for
SVG images.
"""
return MIME_TYPE.SVG

@property
def default_ext(self):
"""
Default filename extension, always 'svg' for SVG images.
"""
return "svg"

@classmethod
def _dimensions_from_stream(cls, stream):
stream.seek(0)
data = stream.read()
root = ET.fromstring(data)
# FIXME: The width could be expressed as '4cm'
# See https://www.w3.org/TR/SVG11/struct.html#NewDocument
width = int(root.attrib["width"])
height = int(root.attrib["height"])
return width, height
2 changes: 2 additions & 0 deletions src/docx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

register_element_cls("a:blip", CT_Blip)
register_element_cls("a:ext", CT_PositiveSize2D)
register_element_cls("a:extLst", CT_Transform2D)
register_element_cls("asvg:svgBlip", CT_Transform2D)
register_element_cls("a:graphic", CT_GraphicalObject)
register_element_cls("a:graphicData", CT_GraphicalObjectData)
register_element_cls("a:off", CT_Point2D)
Expand Down
1 change: 1 addition & 0 deletions src/docx/oxml/ns.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xml": "http://www.w3.org/XML/1998/namespace",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"asvg": "http://schemas.microsoft.com/office/drawing/2016/SVG/main",
}

pfxmap = {value: key for key, value in nsmap.items()}
Expand Down
45 changes: 41 additions & 4 deletions src/docx/oxml/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CT_Blip(BaseOxmlElement):

embed = OptionalAttribute("r:embed", ST_RelationshipId)
link = OptionalAttribute("r:link", ST_RelationshipId)
extLst = ZeroOrOne("a:extLst")


class CT_BlipFillProperties(BaseOxmlElement):
Expand Down Expand Up @@ -105,7 +106,7 @@ def _inline_xml(cls):
" <a:graphic>\n"
' <a:graphicData uri="URI not set"/>\n'
" </a:graphic>\n"
"</wp:inline>" % nsdecls("wp", "a", "pic", "r")
"</wp:inline>" % nsdecls("wp", "a", "pic", "r", "asvg")
)


Expand Down Expand Up @@ -135,14 +136,48 @@ def new(cls, pic_id, filename, rId, cx, cy):
"""Return a new ``<pic:pic>`` element populated with the minimal contents
required to define a viable picture element, based on the values passed as
parameters."""
pic = parse_xml(cls._pic_xml())
if filename.endswith(".svg"):
pic = parse_xml(cls._pic_xml_svg())
pic.blipFill.blip.extLst.ext.svgBlip.embed = rId
else:
pic = parse_xml(cls._pic_xml())
pic.blipFill.blip.embed = rId
pic.nvPicPr.cNvPr.id = pic_id
pic.nvPicPr.cNvPr.name = filename
pic.blipFill.blip.embed = rId
pic.spPr.cx = cx
pic.spPr.cy = cy
return pic

@classmethod
def _pic_xml_svg(cls):
return (
"<pic:pic %s>\n"
" <pic:nvPicPr>\n"
' <pic:cNvPr id="666" name="unnamed"/>\n'
" <pic:cNvPicPr/>\n"
" </pic:nvPicPr>\n"
" <pic:blipFill>\n"
" <a:blip>\n"
" <a:extLst>\n"
' <a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">\n'
" <asvg:svgBlip/>\n"
" </a:ext>\n"
" </a:extLst>\n"
" </a:blip>\n"
" <a:stretch>\n"
" <a:fillRect/>\n"
" </a:stretch>\n"
" </pic:blipFill>\n"
" <pic:spPr>\n"
" <a:xfrm>\n"
' <a:off x="0" y="0"/>\n'
' <a:ext cx="914400" cy="914400"/>\n'
" </a:xfrm>\n"
' <a:prstGeom prst="rect"/>\n'
" </pic:spPr>\n"
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
)

@classmethod
def _pic_xml(cls):
return (
Expand All @@ -164,7 +199,7 @@ def _pic_xml(cls):
" </a:xfrm>\n"
' <a:prstGeom prst="rect"/>\n'
" </pic:spPr>\n"
"</pic:pic>" % nsdecls("pic", "a", "r")
"</pic:pic>" % nsdecls("pic", "a", "r", "asvg")
)


Expand Down Expand Up @@ -192,6 +227,7 @@ class CT_PositiveSize2D(BaseOxmlElement):

cx = RequiredAttribute("cx", ST_PositiveCoordinate)
cy = RequiredAttribute("cy", ST_PositiveCoordinate)
svgBlip = ZeroOrOne("asvg:svgBlip")


class CT_PresetGeometry2D(BaseOxmlElement):
Expand Down Expand Up @@ -258,6 +294,7 @@ class CT_Transform2D(BaseOxmlElement):

off = ZeroOrOne("a:off", successors=("a:ext",))
ext = ZeroOrOne("a:ext", successors=())
embed = OptionalAttribute("r:embed", ST_RelationshipId)

@property
def cx(self):
Expand Down

0 comments on commit 78767ef

Please sign in to comment.