Skip to content

Some hack to include SVG #798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docx/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +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 = (
# class, offset, signature_bytes
Expand All @@ -26,4 +26,5 @@
(Tiff, 0, b'MM\x00*'), # big-endian (Motorola) TIFF
(Tiff, 0, b'II*\x00'), # little-endian (Intel) TIFF
(Bmp, 0, b'BM'),
(Svg, 2, b'xml'),
)
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'


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

from __future__ import absolute_import, division, print_function

from struct import Struct

from .constants import MIME_TYPE
from .image import BaseImageHeader


class Svg(BaseImageHeader):
"""
Image header parser for GIF images. Note that the GIF format does not
support resolution (DPI) information. Both horizontal and vertical DPI
default to 72.
"""
@classmethod
def from_stream(cls, stream):
"""
Return |Gif| instance having header properties parsed from GIF 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` for
SVG images.
"""
return MIME_TYPE.SVG

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

@classmethod
def _dimensions_from_stream(cls, stream):
from xml.etree import ElementTree
stream.seek(0)
text = stream.read()
root = ElementTree.fromstring(text)
x_min, y_min, x_max, y_max = [float(coord) for coord in root.attrib['viewBox'].split()]
return x_max - x_min, y_max - y_min
56 changes: 54 additions & 2 deletions docx/oxml/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class CT_Blip(BaseOxmlElement):
embed = OptionalAttribute('r:embed', ST_RelationshipId)
link = OptionalAttribute('r:link', ST_RelationshipId)


class CT_BlipFillProperties(BaseOxmlElement):
"""
``<pic:blipFill>`` element, specifies picture properties
Expand Down Expand Up @@ -81,7 +80,10 @@ def new_pic_inline(cls, shape_id, rId, filename, cx, cy):
specified by the argument values.
"""
pic_id = 0 # Word doesn't seem to use this, but does not omit it
pic = CT_Picture.new(pic_id, filename, rId, cx, cy)
if filename.endswith('.svg'):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm misreading this - this would only capture SVG is actually provided as a file (that has a filename go with it). There are many use cases where pictures are generated on the fly in in-memory buffers and therefore not provided as files. Any way to account for that?
(For example the PNG equivalent for matplotlib + docx would be something like:

import base64
import io

import docx
from docx.shared import Inches
import matplotlib.figure as figure

fig = figure.Figure()
ax = fig.subplots()
ax.plot([1, 2])
buf = io.BytesIO()
fig.savefig(buf, format='png')

document = docx.Document()
document.add_heading('Document Title', 0)
document.add_picture(buf, width=Inches(1.25))
document.save('demo.docx')

Copy link
Author

@pfernique pfernique Apr 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true that this seems strange but when dispatch is done, a filename image.svg is given for a stream (must be the same for png). So even if you provide in-memory buffers, it's good.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize that - thank you for calling it out. I looked a bit further (which I should have from the beginning):filename is generated here in image.py.

pic = CT_SVGPicture.new(pic_id, filename, rId, cx, cy)
else:
pic = CT_Picture.new(pic_id, filename, rId, cx, cy)
inline = cls.new(cx, cy, shape_id, pic)
inline.graphic.graphicData._insert_pic(pic)
return inline
Expand Down Expand Up @@ -165,6 +167,56 @@ def _pic_xml(cls):
'</pic:pic>' % nsdecls('pic', 'a', 'r')
)

class CT_SVGPicture(CT_Picture):
"""
``<pic:pic>`` element, a DrawingML picture
"""

@classmethod
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.
"""
XML = cls._pic_xml()
XML = XML.replace("${rId}", rId)
pic = parse_xml(XML)
pic.nvPicPr.cNvPr.id = pic_id
pic.nvPicPr.cNvPr.name = filename
pic.spPr.cx = cx
pic.spPr.cy = cy
return pic

@classmethod
def _pic_xml(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 xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="${rId}"/>\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')
)

class CT_PictureNonVisual(BaseOxmlElement):
"""
Expand Down
92 changes: 92 additions & 0 deletions tests/image/test_svg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# encoding: utf-8

"""Unit test suite for docx.image.svg module"""

from __future__ import absolute_import, print_function

import pytest

from docx.compat import BytesIO
from docx.image.constants import MIME_TYPE
from docx.image.svg import Svg

from ..unitutil.mock import ANY, initializer_mock


class DescribeSvg(object):

def it_can_construct_from_a_svg_stream(self, Svg__init__):
cx, cy = 81.56884, 17.054602
bytes_ = b"""\
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg883"
version="1.1"
viewBox="0 0 81.56884 17.054602"
height="17.054602mm"
width="81.56884mm">
<defs
id="defs877" />
<metadata
id="metadata880">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(238.27068,33.733892)"
id="layer1">
<text
id="text843"
y="-16.976948"
x="-238.27068"
style="font-style:normal;font-weight:normal;font-size:22.57777786px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="stroke-width:0.26458332"
y="-16.976948"
x="-238.27068"
id="tspan841">Test 2 !</tspan></text>
<flowRoot
transform="matrix(0.26458333,0,0,0.26458333,-238.27068,-33.392139)"
style="font-style:normal;font-weight:normal;font-size:85.33333588px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
id="flowRoot814"
xml:space="preserve"><flowRegion
id="flowRegion816"><rect
y="-63.976192"
x="-182.85715"
height="248.57143"
width="880"
id="rect818" /></flowRegion><flowPara
id="flowPara820" /></flowRoot> </g>
</svg>"""
stream = BytesIO(bytes_)

svg = Svg.from_stream(stream)

Svg__init__.assert_called_once_with(ANY, cx, cy, 72, 72)
assert isinstance(svg, Svg)

def it_knows_its_content_type(self):
svg = Svg(None, None, None, None)
assert svg.content_type == MIME_TYPE.SVG

def it_knows_its_default_ext(self):
svg = Svg(None, None, None, None)
assert svg.default_ext == 'svg'

# fixture components ---------------------------------------------

@pytest.fixture
def Svg__init__(self, request):
return initializer_mock(request, Svg)