Files
Igor20264 b38661f588
Python application / build (push) Has been cancelled
Update 0.4.0
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00

149 lines
5.9 KiB
Python

import logging
from copy import copy
from io import BytesIO
from typing import Generator
from os import environ
import os.path
import requests
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Parented, Length, Mm
from docx.text.paragraph import Paragraph
from .caption import Caption, CaptionInfo
from .renderable import Renderable
from .requires_numbering import RequiresNumbering
from ..docx_svg import attach_svg_blip
from ..layout_tracker import LayoutState
from ..rendered_info import RenderedInfo
from ..sub_renderable import SubRenderable
from ..util import create_element
# Leave room under the figure so «Рисунок N — …» stays on the same page.
_CAPTION_RESERVE = Mm(12)
class Image(Renderable, RequiresNumbering):
def __init__(
self,
parent: Parented,
path: str,
caption_info: CaptionInfo | None = None,
svg_path: str | None = None,
*,
pixel_scale: float = 1.0,
):
super().__init__("Рисунок")
self._parent = parent
self._caption_info = caption_info
self._docx_paragraph = Paragraph(create_element("w:p"), parent)
self._docx_paragraph.paragraph_format.space_before = 0
self._docx_paragraph.paragraph_format.space_after = 0
self._docx_paragraph.paragraph_format.first_line_indent = 0
self._docx_paragraph.paragraph_format.line_spacing = 1
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
self._invalid = False
self._native_width: Length | None = None
self._native_height: Length | None = None
run = self._docx_paragraph.add_run()
if path.startswith("http"):
bytesio = BytesIO()
bytesio.write(requests.get(path).content)
self._image = run.add_picture(bytesio)
else:
try:
path = os.path.expanduser(path)
if not os.path.isabs(path):
path = os.path.join(environ.get("WORKING_DIR", "."), path)
self._image = run.add_picture(path)
except FileNotFoundError:
logging.warning(f"Invalid image path: {path}, skipping...")
self._invalid = True
self._image = None
# High-res PlantUML/Kroki PNG: keep on-page size as if scale were 1.
if not self._invalid and pixel_scale and pixel_scale > 1:
self._image.width = Length(int(self._image.width / pixel_scale))
self._image.height = Length(int(self._image.height / pixel_scale))
if not self._invalid:
self._native_width = Length(int(self._image.width))
self._native_height = Length(int(self._image.height))
if not self._invalid and svg_path:
try:
attach_svg_blip(run, self._image, svg_path)
except Exception as exc:
logging.warning("SVG blip attach failed (%s): %s", svg_path, exc)
self._number = None
self.landscape = bool(caption_info and caption_info.landscape)
if caption_info and caption_info.unique_name:
self.unique_name = caption_info.unique_name
# Keep figure + caption together across Word pagination.
if not self._invalid and caption_info is not None:
self._docx_paragraph.paragraph_format.keep_with_next = True
def set_number(self, number: str):
self._number = number
def _reset_native_size(self) -> None:
if self._image is None or self._native_width is None or self._native_height is None:
return
self._image.width = Length(int(self._native_width))
self._image.height = Length(int(self._native_height))
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
-> Generator[RenderedInfo | SubRenderable, None, None]:
if self._invalid:
yield from []
return
# Re-fit from native size each time (Paragraph may have measured us earlier).
self._reset_native_size()
has_caption = self._caption_info is not None
caption_reserve = _CAPTION_RESERVE if has_caption else Length(0)
max_w = layout_state.max_width
max_h = Length(max(0, int(layout_state.max_height) - int(caption_reserve)))
# limit width
if self._image.width > max_w:
height_by_width = self._image.height / self._image.width
self._image.width = max_w
self._image.height = Length(self._image.width * height_by_width)
# limit height (leave room for caption on the same page)
if self._image.height > max_h:
width_by_height = self._image.width / self._image.height
self._image.height = max_h
self._image.width = Length(self._image.height * width_by_height)
height = self._image.height
need = Length(int(height) + int(caption_reserve))
remaining = layout_state.remaining_page_height
if remaining < need:
if self.landscape:
# Already on a fresh landscape section — shrink instead of soft page-break
# (soft break + section break = empty landscape page before the figure).
avail = Length(max(0, int(remaining) - int(caption_reserve)))
if avail > 0 and self._image.height > avail:
ratio = int(avail) / int(self._image.height)
self._image.height = avail
self._image.width = Length(int(self._image.width * ratio))
height = self._image.height
else:
height = Length(int(height) + int(remaining))
yield (rendered_image := RenderedInfo(self._docx_paragraph, Length(height)))
layout_state.add_height(rendered_image.height)
caption = Caption(self._parent, "Рисунок", self._caption_info, self._number, False)
caption.center()
yield from caption.render(rendered_image, copy(layout_state))