python-openxml / python-docx

Create and modify Word documents with Python
MIT License
4.54k stars 1.11k forks source link

Inline support for SVG file stream #1399

Open firezym opened 4 months ago

firezym commented 4 months ago

I can directly insert png stream into docx, but failed when I change it to format='svg'. Is there a plan to add such support in the future or is there anything wrong with my code? @takis

PNG stream is supported:

import io
import docx
from docx import Document
from docx.shared import Inches, Cm, Pt, RGBColor
import plotly.io as pio
import plotly.express as px
import pandas as pd
import numpy as np

width, height, scale, padding = 1000, 500, 2, 10
doc_width = Inches(5)
doc = Document()

x = np.linspace(-2*np.pi, 2*np.pi, 100)
df = pd.DataFrame({'x': x, 'sin(x)': np.sin(x), 'cos(x)': np.cos(x)})
fig = px.line(df, x='x', y=['sin(x)', 'cos(x)'], title='Sin and Cos Functions')

image_stream = io.BytesIO()
pio.write_image(fig, file=image_stream, format='png', width=width, scale=scale)

doc.add_picture(image_stream, width=doc_width)

SVG stream is not supported:

import io
import docx
from docx import Document
from docx.shared import Inches, Cm, Pt, RGBColor
import plotly.io as pio
import plotly.express as px
import pandas as pd
import numpy as np

width, height, scale, padding = 1000, 500, 2, 10
doc_width = Inches(5)
doc = Document()

x = np.linspace(-2*np.pi, 2*np.pi, 100)
df = pd.DataFrame({'x': x, 'sin(x)': np.sin(x), 'cos(x)': np.cos(x)})
fig = px.line(df, x='x', y=['sin(x)', 'cos(x)'], title='Sin and Cos Functions')

image_stream = io.BytesIO()
pio.write_image(fig, file=image_stream, format='svg', width=width, scale=scale)

doc.add_picture(image_stream, width=doc_width)

Error as below:

---------------------------------------------------------------------------
UnrecognizedImageError                    Traceback (most recent call last)
Cell In[9], line 22
     19 image_stream = io.BytesIO()
     20 pio.write_image(fig, file=image_stream, format='svg', width=width, scale=scale)
---> 22 doc.add_picture(image_stream, width=doc_width)

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\document.py:88](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/document.py#line=87), in Document.add_picture(self, image_path_or_stream, width, height)
     77 """Return new picture shape added in its own paragraph at end of the document.
     78 
     79 The picture contains the image at `image_path_or_stream`, scaled based on
   (...)
     85 if no value is specified, as is often the case.
     86 """
     87 run = self.add_paragraph().add_run()
---> 88 return run.add_picture(image_path_or_stream, width, height)

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\text\run.py:79](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/text/run.py#line=78), in Run.add_picture(self, image_path_or_stream, width, height)
     59 def add_picture(
     60     self,
     61     image_path_or_stream: str | IO[bytes],
     62     width: int | Length | None = None,
     63     height: int | Length | None = None,
     64 ) -> InlineShape:
     65     """Return |InlineShape| containing image identified by `image_path_or_stream`.
     66 
     67     The picture is added to the end of this run.
   (...)
     77     value is specified, as is often the case.
     78     """
---> 79     inline = self.part.new_pic_inline(image_path_or_stream, width, height)
     80     self._r.add_drawing(inline)
     81     return InlineShape(inline)

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\parts\story.py:71](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/parts/story.py#line=70), in StoryPart.new_pic_inline(self, image_descriptor, width, height)
     60 def new_pic_inline(
     61     self,
     62     image_descriptor: str | IO[bytes],
     63     width: int | Length | None = None,
     64     height: int | Length | None = None,
     65 ) -> CT_Inline:
     66     """Return a newly-created `w:inline` element.
     67 
     68     The element contains the image specified by `image_descriptor` and is scaled
     69     based on the values of `width` and `height`.
     70     """
---> 71     rId, image = self.get_or_add_image(image_descriptor)
     72     cx, cy = image.scaled_dimensions(width, height)
     73     shape_id, filename = self.next_id, image.filename

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\parts\story.py:37](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/parts/story.py#line=36), in StoryPart.get_or_add_image(self, image_descriptor)
     35 package = self._package
     36 assert package is not None
---> 37 image_part = package.get_or_add_image_part(image_descriptor)
     38 rId = self.relate_to(image_part, RT.IMAGE)
     39 return rId, image_part.image

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\package.py:31](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/package.py#line=30), in Package.get_or_add_image_part(self, image_descriptor)
     25 def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart:
     26     """Return |ImagePart| containing image specified by `image_descriptor`.
     27 
     28     The image-part is newly created if a matching one is not already present in the
     29     collection.
     30     """
---> 31     return self.image_parts.get_or_add_image_part(image_descriptor)

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\package.py:74](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/package.py#line=73), in ImageParts.get_or_add_image_part(self, image_descriptor)
     68 def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart:
     69     """Return |ImagePart| object containing image identified by `image_descriptor`.
     70 
     71     The image-part is newly created if a matching one is not present in the
     72     collection.
     73     """
---> 74     image = Image.from_file(image_descriptor)
     75     matching_image_part = self._get_by_sha1(image.sha1)
     76     if matching_image_part is not None:

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\image\image.py:50](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/image/image.py#line=49), in Image.from_file(cls, image_descriptor)
     48     blob = stream.read()
     49     filename = None
---> 50 return cls._from_stream(stream, blob, filename)

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\image\image.py:162](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/image/image.py#line=161), in Image._from_stream(cls, stream, blob, filename)
    153 @classmethod
    154 def _from_stream(
    155     cls,
   (...)
    158     filename: str | None = None,
    159 ) -> Image:
    160     """Return an instance of the |Image| subclass corresponding to the format of the
    161     image in `stream`."""
--> 162     image_header = _ImageHeaderFactory(stream)
    163     if filename is None:
    164         filename = "image.%s" % image_header.default_ext

File [D:\ProgramData\miniconda3\envs\prod\Lib\site-packages\docx\image\image.py:182](file:///D:/ProgramData/miniconda3/envs/prod/Lib/site-packages/docx/image/image.py#line=181), in _ImageHeaderFactory(stream)
    180     if found_bytes == signature_bytes:
    181         return cls.from_stream(stream)
--> 182 raise UnrecognizedImageError

UnrecognizedImageError:
michaelarfreed commented 4 months ago

My understanding is that @takis code has not been merged, and so svg pictures are not compatible yet. I'd really like to see this feature added, too!

takis commented 4 months ago

Hi @firezym

My patch has not been accepted, so if you want to use SVG you'd have to use my fork: https://github.com/takis/python-docx

I've tried your code and it works fine. Here's a screenshot of the resulting file opened in Word on macOS Sonoma.

Screenshot 2024-05-31 at 20 36 45
firezym commented 4 months ago

@takis Thanks a lot! It's awesome. Looking forward to the future merge :)