Skip to content
Merged
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
38 changes: 34 additions & 4 deletions packtools/sps/formats/pdf/renderer/docx/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ def add_figure(docx, figure_data, header_style_name='SCL Table Heading', page_at

single_col_width = _compute_single_column_width(page_attributes)

target_width = content_width if layout == pdf_enum.SINGLE_COLUMN_PAGE_LABEL else single_col_width
ceiling_width = content_width if layout == pdf_enum.SINGLE_COLUMN_PAGE_LABEL else single_col_width
context = _get_docx_context(docx)
href, alt = _extract_figure_meta(figure_data)
img_path = _resolve_image_path(href, context)

target_width = _natural_width_capped(img_path, ceiling_width)
picture_added = _try_insert_picture(docx, img_path, target_width, page_attributes)
if not picture_added:
_add_alt_paragraph(docx, alt)
Expand Down Expand Up @@ -214,15 +215,44 @@ def _resolve_image_path(href, context):

return img_path

def _natural_width_capped(img_path, ceiling_width):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note que:

  • decide_figure_layout escolhe entre largura total e largura de coluna.
  • _natural_width_capped determina a largura efetiva de inserção, limitada ao teto.

Nelas há um trecho duplicado que se refere ao cálculo da largura natural:

px_w = im.width
dpi = _infer_image_dpi(im)
natural_width = (px_w / max(1.0, dpi)) * 2.54

Ainda,

  • decide_figure_layout mantém o resultado como float em centímetros;
  • _natural_width_capped converte o resultado com Cm(...), produzindo um objeto Length representado internamente em EMU.

As funções têm responsabilidades diferentes, mas ambas abrem a imagem e calculam sua largura natural a partir de pixels e DPI. Além da duplicação, atualmente elas produzem unidades diferentes: decide_figure_layout mantém um float em centímetros, enquanto _natural_width_capped retorna Cm/EMU.

Podemos extrair uma operação única que devolva a largura natural em Cm e reutilizá-la tanto na decisão quanto no limite de inserção? Isso mantém interpretação de DPI e unidade consistentes nos dois fluxos.

"""
Compute the image's natural width from its DPI metadata, capped at
ceiling_width - only ever shrunk to fit, never enlarged.
"""
if not (img_path and os.path.exists(img_path)):
return ceiling_width
try:
from PIL import Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mover este import para o topo, removendo desta linha e de outra neste arquivo.


with Image.open(img_path) as im:
px_w = im.width
dpi = _infer_image_dpi(im)
natural_width = Cm((px_w / max(1.0, dpi)) * 2.54)
return natural_width if natural_width < ceiling_width else ceiling_width
except Exception:
return ceiling_width

def _try_insert_picture(docx, img_path, content_width, page_attributes):
"""Try to insert a picture into the document, scaling it to fit content width. Returns True if successful."""
"""
Insert a picture at content_width explicitly, then apply
_scale_picture_to_fit as a height safety net. Returns True if successful.

content_width is passed to add_picture(width=...) instead of left for
python-docx to infer: python-docx reads DPI metadata independently of
_infer_image_dpi (used above to compute content_width in the first
place), and the two can disagree - e.g. a PNG without DPI metadata makes
python-docx default to 72 DPI while _infer_image_dpi defaults to 96 DPI,
a ~33% size difference. Passing the width explicitly makes content_width
the actual inserted size.
"""
if not (img_path and os.path.exists(img_path)):
return False
try:
pic_para = docx.add_paragraph()
run = pic_para.add_run()
picture = run.add_picture(img_path)
picture = run.add_picture(img_path, width=content_width)

_scale_picture_to_fit(picture, content_width, page_attributes)
pic_para.alignment = WD_ALIGN_PARAGRAPH.CENTER

Expand Down
80 changes: 79 additions & 1 deletion tests/sps/formats/pdf/renderer/docx/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
import unittest

from docx import Document
from docx.shared import Cm
from PIL import Image

from packtools.sps.formats.pdf.renderer.docx.figure import decide_figure_layout
from packtools.sps.formats.pdf.renderer.docx.figure import (
_infer_image_dpi,
_natural_width_capped,
add_figure,
decide_figure_layout,
)
from packtools.sps.formats.pdf import enum as pdf_enum


Expand Down Expand Up @@ -73,5 +79,77 @@ def test_layout_dpi_override_replaces_embedded_dpi(self):
)


class TestNaturalWidthCapped(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmpdir.cleanup)

def _make_png(self, px_width=200, px_height=100, dpi=None):
path = os.path.join(self.tmpdir.name, "img.png")
im = Image.new("RGB", (px_width, px_height), color="white")
if dpi:
im.save(path, format="PNG", dpi=(dpi, dpi))
else:
im.save(path, format="PNG")
return path

def test_natural_width_shrinks_to_ceiling_when_larger(self):
img_path = self._make_png(px_width=4000, dpi=96)
ceiling = Cm(10)
result = _natural_width_capped(img_path, ceiling)
self.assertEqual(result, ceiling)

def test_natural_width_kept_when_smaller_than_ceiling(self):
img_path = self._make_png(px_width=200, dpi=96)
ceiling = Cm(10)
result = _natural_width_capped(img_path, ceiling)
expected = Cm((200 / 96) * 2.54)
# PNG's pHYs chunk stores pixels-per-meter (integer), so the DPI read
# back after a save/load round-trip is a close approximation, not
# bit-exact - allow a small tolerance instead of exact EMU equality.
self.assertAlmostEqual(int(result), int(expected), delta=5000)
self.assertLess(result, ceiling)

def test_missing_file_falls_back_to_ceiling(self):
ceiling = Cm(10)
result = _natural_width_capped("/no/such/file.png", ceiling)
self.assertEqual(result, ceiling)


class TestAddFigureInsertedWidth(unittest.TestCase):
"""
Regression test for the DPI mismatch between _infer_image_dpi (used to
decide the figure's width) and python-docx's own DPI reading (used when
add_picture() is called without an explicit width, defaulting to 72 DPI
vs _infer_image_dpi's 96 DPI fallback - a ~33% size difference).
"""

def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmpdir.cleanup)

def test_inserted_picture_width_matches_infer_image_dpi_not_python_docx_default(self):
img_path = os.path.join(self.tmpdir.name, "no_dpi.png")
Image.new("RGB", (200, 100), color="white").save(img_path, format="PNG")

docx = Document()
figure_data = {"href": img_path, "label": "Figure 1", "caption": "test"}
add_figure(docx, figure_data)

self.assertEqual(len(docx.inline_shapes), 1)
inserted_width = docx.inline_shapes[0].width

with Image.open(img_path) as im:
dpi = _infer_image_dpi(im)
expected_width = int(Cm((200 / dpi) * 2.54))

self.assertEqual(inserted_width, expected_width)

# python-docx's own (unfixed) default would have produced 96/72 = 1.333x
# this width instead - assert we are NOT that value.
python_docx_default_width = int(Cm((200 / 72) * 2.54))
self.assertNotEqual(inserted_width, python_docx_default_width)


if __name__ == "__main__":
unittest.main()