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
  • Loading branch information
takis committed Jun 24, 2022
1 parent 36cac78 commit 8f54818
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docx/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 = (
Expand All @@ -26,4 +27,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 docx/image/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class MIME_TYPE(object):
JPEG = 'image/jpeg'
PNG = 'image/png'
TIFF = 'image/tiff'
SVG = 'image/svg+xml'


class PNG_CHUNK_TYPE(object):
Expand Down
49 changes: 49 additions & 0 deletions 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 docx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
)
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 docx/oxml/ns.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 = dict((value, key) for key, value in nsmap.items())
Expand Down
17 changes: 13 additions & 4 deletions docx/oxml/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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 @@ -98,7 +99,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 @@ -136,7 +137,7 @@ def new(cls, pic_id, filename, rId, cx, cy):
pic = parse_xml(cls._pic_xml())
pic.nvPicPr.cNvPr.id = pic_id
pic.nvPicPr.cNvPr.name = filename
pic.blipFill.blip.embed = rId
pic.blipFill.blip.extLst.ext.svgBlip.embed = rId
pic.spPr.cx = cx
pic.spPr.cy = cy
return pic
Expand All @@ -150,7 +151,13 @@ def _pic_xml(cls):
' <pic:cNvPicPr/>\n'
' </pic:nvPicPr>\n'
' <pic:blipFill>\n'
' <a:blip/>\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'
Expand All @@ -162,7 +169,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 All @@ -189,6 +196,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 +266,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 8f54818

Please sign in to comment.