无法使用 Python 将 EMF 插入 Word

Unable to insert EMF into Word using Python

我有将 SVG 文件插入 Word 的要求。因为,我们不能直接这样做,我计划将 SVG 转换为 EMF 并插入它。使用 inkscape 从 SVG 转换为 EMF 效果很好。但是,我无法想出将其插入 Word 的正确代码。 我按照 this post. Have shown the steps followed in the attached file -

中的人员 Alvaro 解释的步骤进行操作

这是我的代码 -

但是,当我 运行 附件中显示的代码时 - 它仍然抛出 docx.image.exceptions.UnrecognizedImageError。 github 上的库的贡献者声称该库解决了这个问题。如果是这样,如果我遗漏了什么,请告诉我。

我能够手动成功插入 EMF 文件。通过插入 EMF 附加文档。此 EMF 从 the internet 下载用于测试。

模块 docx 似乎不适用于 EMF 个文件。

我的意思是解决方法在这里:

import shutil
import zipfile

temp_dir = "_temp"

old_docx = "doc.docx"
new_docx = "doc_new.docx"

old_emf = temp_dir + "/word/media/image1.emf"
new_emf = "new_image.emf"


# unpack content of the docx file into the temp folder

with zipfile.ZipFile(old_docx, "r") as z:
    files = z.namelist()
    for f in files: z.extract(f, temp_dir)


# replace the image

shutil.copyfile(new_emf, old_emf)


# pack all files from temp folder back into the new docx file

with zipfile.ZipFile(new_docx, "a") as z:
    for f in files: z.write(temp_dir + "/" + f, f)


# remove the temp folder

shutil.rmtree(temp_dir)

docx 文件的典型结构:

doc.docx
│
├─ [Content_Types].xml
│
├─ _rels
│  └─ .rels
│
├─ docProps
│  ├─ app.xml
│  └─ docProps
│
└─ word
   ├─ document.xml    <-- text is here
   ├─ fontTable.xml
   ├─ settings.xml
   ├─ webSettings.xml
   ├─ styles.xml
   │
   ├─ _rels
   │  └─ document.xml.rels
   │
   ├─ theme
   │  └─ theme1.xml
   │
   └─ media
      └─ image1.emf   <-- your image is here

它解压临时文件夹 _temp 中的 doc 文件 doc.docx 的内容,然后用当前目录中的另一个文件 new_image.emf 替换临时目录中的文件 image1.emf目录。然后它将临时文件夹的内容打包回 doc_new.docx 文件并删除临时目录。

注意:新图像在 new_doc.docx 中的大小与旧图像相同。

因此工作流程可以是这样的:制作模板 docx 文件,手动放置模板 emf 图片并保存 docx 文件。然后获取新的 emf 图像,将图像放在 docx 文件和 运行 脚本旁边。这样你就可以得到一个带有新 emf 图像的新 docx 文件。

我想你有很多 emf 图像,所以在这个脚本中添加几行是有意义的,它能够拍摄多张图像并制作几个 docx 文件。

如果所有的 emf 图像具有相同的大小,它将工作正常。如果它们的大小不同,则需要更多编码来处理 xml 数据。

更新

我已经知道如何获取 emf 图像的大小了。所以这是完整的解决方案:

from docx import Document
import shutil
import zipfile

temp_dir = "_temp"
old_docx = "doc.docx"
new_docx = "doc_new.docx"
old_emf  = temp_dir + "/word/media/image1.emf" # don't change this line
new_emf  = "img5.emf"

# unpack content of the docx file into temp folder
with zipfile.ZipFile(old_docx, "r") as z:
    files = z.namelist()
    for f in files: z.extract(f, temp_dir)

# replace the image
shutil.copyfile(new_emf, old_emf)

# pack all files from temp folder back into the new docx file
with zipfile.ZipFile(new_docx, "a") as z:
    for f in files: z.write(temp_dir + "/" + f, f)

# remove temp folder
shutil.rmtree(temp_dir)

# get sizes of the emf image
with open(new_emf, "rb") as f:
    f.read(16)
    w1, w2 = f.read(1).hex(), f.read(1).hex()
    f.read(2)
    h1, h2 = f.read(1).hex(), f.read(1).hex()

width  = int(str(w2) + str(w1), 16) * 762
height = int(str(h2) + str(h1), 16) * 762

# open the new docx file and set the sizes for the image
doc = Document(new_docx)
img = doc.inline_shapes[0]  # suppose the first image is the image
img.width  = width
img.height = height

doc.save(new_docx)

这是另一个基于 win32com 模块和 MS Word API 的解决方案:

from pathlib import Path
import win32com.client

cur_dir  = Path.cwd()                                   # get current folder
pictures = list((cur_dir / "pictures").glob("*.emf"))   # get a list of pictures
word_app = win32com.client.Dispatch("Word.Application") # run Word
doc      = word_app.Documents.Add()                     # create a new docx file

for pict in pictures:                                   # insert all pictures
    doc.InlineShapes.AddPicture(pict)

doc.SaveAs(str(cur_dir / "pictures.docx"))              # save the docx file
doc.Close()                                             # close docx
word_app.Quit()                                         # close Word

将您的 EMF 图像放入子文件夹 pictures 和 运行 此脚本。之后,您会在当前文件夹中获得包含所有这些 EMF 图像的文件 pictures.docx

SVG 可以直接添加到 Word - 只需在 Word (2016) 中手动尝试即可。 我已经为您的用例创建了一个 example Java project 作为 POC。 无需调用 inkscape,因为后备 PNG 是通过 Batik 即时创建的。

当然,OP 要求 Python 解决方案 - 但如果 python-openxml 缺少某些功能,可能需要付出更多努力才能实现

=26=] 通过 python 与调用 java 运行 时间。

关于通过 EMF 的变通解决方案 - 请注意,有多种方法可以确定边界 - 在我在 POI 中实现的 EMF 渲染器中,我扫描 Window 和视口记录默认情况下,如果我找不到其他任何东西或者如果通过配置选项省略了扫描,则仅使用 EMF header 边界。这通常会给我带来更好的结果。

示例工程的相关代码片段如下:

public class AddSvgToDocument {
    public static void main(String[] args) throws IOException, InvalidFormatException {
        File tmplDocx = new File(args[0]);
        File svgFile = new File(args[1]);
        File outDocx = new File(args[2]);

        try (FileInputStream fis = new FileInputStream(tmplDocx);
             XWPFDocument doc = new XWPFDocument(fis)) {

            SVGImageRenderer rnd = new SVGImageRenderer();
            try (FileInputStream fis2 = new FileInputStream(svgFile)) {
                rnd.loadImage(fis2, PictureData.PictureType.SVG.contentType);
            }

            Rectangle2D nativeDim = rnd.getNativeBounds();
            double widthPx = 500;
            double heightPx = widthPx * nativeDim.getHeight() / nativeDim.getWidth();

            BufferedImage bi = rnd.getImage(new Dimension2DDouble(widthPx, heightPx));
            ByteArrayOutputStream bos = new ByteArrayOutputStream(100_000);
            ImageIO.write(bi, "PNG", bos);

            XWPFRun run = doc.createParagraph().createRun();

            int widthEmu = Units.pixelToEMU((int)widthPx);
            int heightEmu = Units.pixelToEMU((int)heightPx);
            XWPFPicture pic = run.addPicture(new ByteArrayInputStream(bos.toByteArray()), PictureData.PictureType.PNG.ooxmlId, "image.png", widthEmu, heightEmu);
            CTOfficeArtExtensionList extLst = pic.getCTPicture().getBlipFill().getBlip().addNewExtLst();
            addExt(extLst, "{28A0092B-C50C-407E-A947-70E740481C1C}"
                , "http://schemas.microsoft.com/office/drawing/2010/main", "a14:useLocalDpi"
                , "val", "0");

            addExt(extLst, "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
                , "http://schemas.microsoft.com/office/drawing/2016/SVG/main", "asvg:svgBlip"
                , "r:embed", addSVG(doc, svgFile));

            try (FileOutputStream fos = new FileOutputStream(outDocx)) {
                doc.write(fos);
            }
        }
    }



    private static void addExt(CTOfficeArtExtensionList extLst, String uri, String namespace, String name, String attribute, String value) {
        CTOfficeArtExtension ext = extLst.addNewExt();
        ext.setUri(uri);
        XmlCursor cur = ext.newCursor();
        cur.toEndToken();
        String[] prefixName = name.split(":");
        cur.beginElement(new QName(namespace, prefixName[1], prefixName[0]));
        cur.insertNamespace(prefixName[0], namespace);
        if (attribute.contains(":")) {
            prefixName = attribute.split(":");
            String prefix = prefixName[0];
            String attrNamespace = DEFAULT_XML_OPTIONS
                .getSaveSuggestedPrefixes().entrySet().stream()
                .filter(me -> prefix.equals(me.getValue()))
                .map(Map.Entry::getKey)
                .findFirst().orElse(null);
            cur.insertAttributeWithValue(new QName(attrNamespace, prefixName[1], prefix), value);
        } else {
            cur.insertAttributeWithValue(attribute, value);
        }
        cur.dispose();
    }

    private static String addSVG(XWPFDocument doc, File svgFile) throws InvalidFormatException, IOException {
        // SVG is not thoroughly supported as of POI 5.0.0, hence we need to go the long way instead of adding a picture
        OPCPackage pkg = doc.getPackage();
        String svgNameTmpl = "/word/media/image#.svg";
        int svgImageIdx = pkg.getUnusedPartIndex(svgNameTmpl);
        PackagePartName svgPPName = PackagingURIHelper.createPartName(svgNameTmpl.replace("#", Integer.toString(svgImageIdx)));
        PackagePart svgPart = pkg.createPart(svgPPName, PictureData.PictureType.SVG.contentType);

        try (FileInputStream fis = new FileInputStream(svgFile);
             OutputStream os = svgPart.getOutputStream()) {
            IOUtils.copy(fis, os);
        }
        PackageRelationship svgRel = doc.getPackagePart().addRelationship(svgPPName, TargetMode.INTERNAL, IMAGE_PART);
        return svgRel.getId();
    }
}