Сообщить об ошибке.

Работа с рисунками, модуль python-docx

Добавление/извлечение изображений из документа docx

По сути, документы Word состоят из двух слоев: текстового и слой с картинками. В текстовом слое, текстовые объекты перемещаются слева направо и сверху вниз, начиная новую страницу после заполнения предыдущей. В слое изображений, объекты рисунка, называемые фигурами, размещаются в произвольных местах. Иногда их называют плавающими фигурами.

Изображение - это фигура, которая может вставляться либо в текстовый, либо в графический слой. Если она добавлена в текстовой слой, то она называется встроенной формой или, более конкретно, встроенным изображением.

Содержание:


Добавление встроенного изображения в DOCX.

Пока модуль python-docx поддерживает добавление ТОЛЬКО встроенных изображений. Добавить указанное изображение отдельным абзацем в конец документа можно методом Document.add_picture(). Метод возвращает не объект абзаца, а объект вставленной картинки Document.inline_shapes.

По умолчанию, изображение добавляется с исходными размерами, что часто не устраивает пользователя. Собственный размер рассчитывается как px/dpi. Таким образом, изображение размером 300x300 пикселей с разрешением 300 точек на дюйм появляется в квадрате размером один дюйм. Проблема в том, что большинство изображений не содержат свойства dpi, и по умолчанию оно приравнивается к 72 dpi. Следовательно, то же изображение будет иметь одну сторону, размером 4,167 дюйма, что означает половину страницы.

Чтобы получить изображение нужного размера, необходимо указывать его ширину или высоту в удобных единицах измерения, например, в миллиметрах или сантиметрах:

from docx import Document
from docx.shared import Mm

# создаем документ
doc = Document()
# Добавляем картинку как отдельный абзац
img = doc.add_picture('/path/to/image.jpg', width=Mm(25))
# возвращает объект `Document.inline_shapes`
print(img.type, img.width.mm, img.height.mm)
doc.save('test.docx')

Картинку можно еще добавить как отдельный прогон Run.add_picture() Встроенные таким образом изображения, обрабатываются как большой текстовый символ (глиф символа). Высота строки увеличивается на высоту изображения, при этом изображение уменьшится по ширине, что бы уместиться точно так же, как текст. Вставка текста перед ним приведет к его перемещению вправо. Часто изображение помещается в абзац отдельно, но это не обязательно. До и после него в абзаце, в котором он размещен, может быть текст.

Пример:

from docx import Document
from docx.shared import Mm
from docx.enum.text import WD_ALIGN_PARAGRAPH

# создаем документ
doc = Document()
# Добавляем пустой абзац
p = doc.add_paragraph()
# Добавляем пустой прогон
run = p.add_run()
# теперь в прогон вставляем картинку
run.add_picture('/path/to/image.jpg', width=Mm(25))
# выравниваем картинку посередине страницы
p.alignment =  WD_ALIGN_PARAGRAPH.CENTER

# на второй странице расположим 
# текст до и после картинки
doc.add_page_break()
p = doc.add_paragraph()
run = p.add_run('Текст до картинки')
run.add_picture('/path/to/image.jpg', width=Mm(25))
run.add_text('текст после картинки')
doc.save('test.docx')

Добавление плавающего изображения в DOCX.

Вставка в документ DOCX плавающего изображения еще не поддерживается модулем python-docx. Но, основываясь на реализации создания встроенной картинки, можно создать обходной путь.

Если посмотреть на структуру XML, созданную DOCX, то можно увидеть различия между встроенной и плавающей картинкой:

  • встроенное изображение - это узел <wp:inline> под <w:drawing>;
  • плавающее изображение - это узел <wp:anchor> под <w:drawing>;
  • помимо всех подузлов встроенного изображения, плавающее изображение содержит также <wp:positionH> и <wp:positionV> для определения фиксированной позиции.

Идея состоит в том, чтобы вместо узла <wp:inline> добавлять узел <wp:anchor>, а затем дополнительно к уже имеющимся методам inline (встроенной) картинки добавить подузлы <wp:positionH> и <wp:positionV>.

Смотрим:

from docx.oxml import parse_xml, register_element_cls
from docx.oxml.ns import nsdecls
from docx.oxml.shape import CT_Picture
from docx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne

# смотрите: docx.oxml.shape.CT_Inline
class CT_Anchor(BaseOxmlElement):
    """
    Элемент `<w:anchor>`, контейнер для плавающего изображения.
    """
    extent = OneAndOnlyOne('wp:extent')
    docPr = OneAndOnlyOne('wp:docPr')
    graphic = OneAndOnlyOne('a:graphic')

    @classmethod
    def _anchor_xml(cls, pos_x, pos_y):
        """
        Стиль переноса текста: `<wp:anchor behindDoc="0">`;
        Положение изображения: `<wp:positionH relativeFrom="page">`;
        Обтекание текста: `<wp:wrapSquare wrapText="largest"/>`.
        """
        return (
            '<wp:anchor behindDoc="0" distT="0" distB="0" distL="0" distR="0"'
            ' simplePos="0" layoutInCell="1" allowOverlap="1" relativeHeight="2"'
            f' {nsdecls("wp", "a", "pic", "r")}>'
            '  <wp:simplePos x="0" y="0"/>'
            '  <wp:positionH relativeFrom="page">'
            f'    <wp:posOffset>{int(pos_x)}</wp:posOffset>'
            '  </wp:positionH>'
            '  <wp:positionV relativeFrom="page">'
            f'    <wp:posOffset>{int(pos_y)}</wp:posOffset>'
            '  </wp:positionV>'
            '  <wp:extent />'
            '  <wp:wrapSquare wrapText="largest"/>'
            '  <wp:docPr />'
            '  <wp:cNvGraphicFramePr>'
            '    <a:graphicFrameLocks noChangeAspect="1"/>'
            '  </wp:cNvGraphicFramePr>'
            '  <a:graphic>'
            '    <a:graphicData>'
            '    </a:graphicData>'
            '  </a:graphic>'
            '</wp:anchor>'
        )

    @classmethod
    def new(cls, cx, cy, shape_id, pic, pos_x, pos_y):
        """
        Возвращает новый элемент `<wp:anchor>`, заполненный 
        переданными значениями в качестве параметров.
        """
        anchor = parse_xml(cls._anchor_xml(pos_x, pos_y))
        anchor.extent.cx = cx
        anchor.extent.cy = cy
        anchor.docPr.id = shape_id
        anchor.docPr.name = f'Picture {shape_id}'
        anchor.graphic.graphicData.uri = (
                'http://schemas.openxmlformats.org/drawingml/2006/picture')
        anchor.graphic.graphicData._insert_pic(pic)
        return anchor

    @classmethod
    def new_pic_anchor(cls, shape_id, rId, filename, cx, cy, pos_x, pos_y):
        """
        Возвращает новый элемент `wp:anchor`, содержащий элемент 
        `pic:pic` задается значениями аргументов.
        """
        pic_id = 0  # Word, похоже, не использует это, но и не опускает его
        pic = CT_Picture.new(pic_id, filename, rId, cx, cy)
        anchor = cls.new(cx, cy, shape_id, pic, pos_x, pos_y)
        anchor.graphic.graphicData._insert_pic(pic)
        return anchor


# смотрите: docx.parts.story.BaseStoryPart.new_pic_inline
def new_pic_anchor(part, image_descriptor, width, height, pos_x, pos_y):
    """
    Возвращает вновь созданный элемент `w:anchor`.
    Элемент содержит изображение, указанное в *image_descriptor*,
    и масштабируется на основе значений *width* и *height*.
    """
    rId, image = part.get_or_add_image(image_descriptor)
    cx, cy = image.scaled_dimensions(width, height)
    shape_id, filename = part.next_id, image.filename    
    return CT_Anchor.new_pic_anchor(shape_id, rId, filename, cx, cy, pos_x, pos_y)

# смотрите: docx.text.run.add_picture
def add_float_picture(p, image_path_or_stream, width=None, height=None, pos_x=0, pos_y=0):
    """
    Добавляет плавающее изображение в фиксированном 
    положении "pos_x" и "pos_y", отсчет - левый верхний угол.
    """
    run = p.add_run()
    anchor = new_pic_anchor(run.part, image_path_or_stream, width, height, pos_x, pos_y)
    run._r.add_drawing(anchor)

# смотрите: docx.oxml.shape.__init__.py
register_element_cls('wp:anchor', CT_Anchor)


if __name__ == '__main__':

    from docx import Document
    from docx.shared import Mm

    doc = Document()
    # добавим плавающее изображение
    p = doc.add_paragraph()
    add_float_picture(p, '/path/to/image.jpg', width=Mm(25), pos_x=Mm(30), pos_y=Mm(30))
    # добавим текст
    p.add_run('текст документа. ' * 50)
    doc.save('test.docx')

Извлечение картинок из документа DOCX.

Так как файл документа MS Word с расширением .docx представляет собой простой zip-архив, то извлечение картинок сводится к распоковки zip-архива, поиска картинок и извлечение их в определенную папку.

Во время извлечения картинок, их можно дополнительно фильтровать по имени ZipInfo.filename, расширению, размеру ZipInfo.file_size и т.д.

import zipfile, pathlib

# укажите файл
docx = 'test.docx'
# директория для извлечения
ex_dir = pathlib.Path(f'pic_{docx}')
if not ex_dir.is_dir():
    ex_dir.mkdir()

with zipfile.ZipFile(docx) as zf:
    for name in zf.infolist():
        if name.filename.startswith('word/media/'):
            # здесь можно задать другие параметры фильтрации, 
            # например отобрать картинки с определенном именем, 
            # расширением, размером `name.file_size` и т.д. 
            zf.extract(name, ex_dir)