Skip to content

fix: corrige decisão de layout de figuras no gerador de PDF (unidade cm/EMU e DPI de figuras irmãs) - #1294

Open
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/figure-layout-decision-unit-mismatch
Open

fix: corrige decisão de layout de figuras no gerador de PDF (unidade cm/EMU e DPI de figuras irmãs)#1294
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/figure-layout-decision-unit-mismatch

Conversation

@Rossi-Luciano

@Rossi-Luciano Rossi-Luciano commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Corrige dois bugs em cadeia na decisão de layout de figuras do gerador de PDF (decide_figure_layout), que juntos podiam fazer uma figura de uma seção renderizar desproporcionalmente gigante (largura total da página), isolada, enquanto figuras irmãs do mesmo estilo/origem apareciam normais dentro da coluna:

  1. Comparação de unidade cm vs EMU: single_col_width degradava de Cm para um número cru em EMU após subtração/divisão (python-docx não sobrecarrega operadores de Length), tornando o ramo de largura total inatingível na prática para qualquer imagem real.
  2. Seleção de <alternatives> e confiança em DPI de metadado: a lógica de ranqueamento de variantes de imagem (<alternatives>) já existia no código mas nunca rodava, sempre pegando o .tif bruto de produção; e o DPI embutido no arquivo se mostrou inconsistente entre figuras-irmãs do mesmo artigo real (72 DPI vs 300 DPI), inflando o cálculo de largura física por ~4x pra uma delas.

Onde a revisão poderia começar?

packtools/sps/formats/pdf/renderer/docx/figure.py, função decide_figure_layout (e a nova probe_image_dpi). Depois packtools/sps/formats/pdf/pipeline/docx.py, função _flag_dpi_outliers.

Como este poderia ser testado manualmente?

Com o LibreOffice instalado:

python -m packtools.sps.formats.pdf_generator -i tests/fixtures/pdf/a4.xml -l tests/fixtures/pdf/layout.docx -o saida.pdf

Comparar a página com os gráficos ("Graph 1" a "Graph 7", ~página 3-4) contra tests/fixtures/pdf/a4.gt.pdf: antes deste PR, Graph 1 aparecia sozinho, gigante, em página de largura total; depois, os 7 gráficos aparecem com tamanho consistente, fluindo com o texto em 2 colunas. Confirmei também a mesma figura de largura total funcionando corretamente onde é esperada (a1.xml/a3.xml, mapas que devem ocupar a página inteira).

Testes automatizados: python -m unittest discover -s tests/sps/formats/pdf -v (109 testes, 8 novos cobrindo os dois commits deste PR).

Algum cenário de contexto que queira dar?

Encontrado durante um levantamento mais amplo de qualidade do gerador de PDF contra um corpus de artigos reais (tests/fixtures/pdf/), comparando a saída atual contra os PDFs editoriais canônicos (*.gt.pdf). Faz parte do escopo da issue #1278 (layout de página/coluna configurável).

Screenshots

comparison_before_after

Quais são os tickets relevantes?

Refs #1278. Closes #1293.

Referências

N/A


Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Não

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Não aplicável a este PR (justifique): Trivy/SonarQube não estão configurados neste repositório (packtools é biblioteca Python, não serviço containerizado) — ver SECURITY_ADHERENCE.md. Os gates automáticos reais deste repositório (Snyk e GitGuardian) rodam via CI neste PR.

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Não

Este PR expõe novos endpoints, telas ou serviços?

  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado

…de figura

decide_figure_layout comparava a largura natural da imagem (width_in_cm, um
float em centímetros) contra single_col_width - mas esse último degrada de
Cm para um número cru em EMU após subtração/divisão (Length do python-docx
não sobrecarrega operadores aritméticos pra preservar a unidade). A
comparação cm >= EMU tornava o ramo full-width inalcançável na prática para
qualquer imagem real, independente do tamanho. Converte de volta pra cm
antes de comparar.

Confirmado contra a Figura 1 real de tests/fixtures/pdf/a1.xml (mapa
698x493px, ~18.5cm de largura natural): antes sempre "double-column-layout"
(dentro da coluna, ~8.2cm), depois "single-column-layout" (largura total),
batendo com o PDF oficial (a1.pdf) - inclusive movendo a figura pra página 3,
igual ao original.

Refs scieloorg#1278.
decide_figure_layout calcula a largura física da figura via pixels/DPI do
metadado do arquivo para decidir entre largura total e largura de coluna.
Analisando um artigo real (tests/fixtures/pdf/a4.xml, 7 gráficos "Graph 1"
a "Graph 7" na mesma seção) apareceu um caso onde só um deles renderizava
gigante em página de largura total, isolado dos demais - mesmo sendo do
mesmo estilo/origem que os outros 6.

Três causas, encontradas nesta ordem:

1. extract_figure_data (pipeline/xml.py) usava fig_node.find('.//graphic'),
   que casa com o primeiro <graphic> em ordem de documento mesmo dentro de
   <alternatives> - sempre pegava o .tif de produção (primeiro na lista) em
   vez de rodar a lógica de ranqueamento já existente no código, que
   preferiria a variante specific-use="scielo-web". Corrigido para só
   casar com um <graphic> filho direto, deixando <alternatives> cair na
   lógica de ranqueamento.

2. Corrigido (1), as 7 figuras passaram a usar .png sem metadado de DPI
   nenhum - caindo no fallback de 96 DPI (resolução de tela), baixo demais
   pra essas imagens (~700-800px), fazendo as 7 (não mais 1) virarem
   largura total. Fallback subiu de 96 para 300 DPI, batendo com a
   convenção observada nos arquivos corretamente rotulados deste mesmo
   corpus.

3. Mesmo com (1) e (2), o caso original de DPI explicitamente inconsistente
   entre arquivos-irmãos (72 DPI vs 300 DPI no .tif, quando não há
   <alternatives> pra escolher) continua possível. Adicionado
   _flag_dpi_outliers (pipeline/docx.py): compara o DPI entre as figuras de
   uma mesma seção antes de decidir o layout: se uma destoa da mediana do
   grupo por 2x ou mais, decide_figure_layout usa a mediana em vez do DPI
   do próprio arquivo (novo campo layout_dpi_override).

Validado ponta a ponta contra a4.xml: antes, 4 seções DOCX alternando
1/2 colunas (Graph 1 isolado); depois, 2 seções (título + corpo), todos os
7 gráficos com tamanho consistente fluindo com o texto em 2 colunas.
Contagem de página/palavras não regride.

Refs scieloorg#1278, scieloorg#1293.

Baseia-se no fix de comparação de unidade cm/EMU já commitado nesta branch
(decide_figure_layout retornava sempre "double-column-layout" antes disso,
o que mascararia completamente este bug).
Comment on lines +1261 to +1273
def test_alternatives_prefers_scielo_web_over_raw_tif(self):
xml = etree.fromstring(
'<fig xmlns:xlink="http://www.w3.org/1999/xlink" id="F1">'
'<label>Graph 1</label>'
'<alternatives>'
'<graphic xlink:href="raw.tif"/>'
'<graphic xlink:href="web.png" specific-use="scielo-web"/>'
'<graphic xlink:href="thumb.jpg" specific-use="scielo-web" content-type="scielo-267x140"/>'
'</alternatives>'
'</fig>'
)
result = xml_pipe.extract_figure_data(xml)
self.assertEqual(result['href'], 'web.png')

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.

Este teste não comprova que specific-use="scielo-web" tem prioridade. web.png vence raw.tif apenas porque o ranking atual prefere PNG a TIFF e nem considera specific-use. Ao trocar as entradas para raw.png e web.jpg com specific-use="scielo-web", o resultado é raw.png. Sugiro tornar o teste discriminante e, se a regra desejada for realmente priorizar SciELO Web, incluir specific-use explicitamente na chave de ordenação.

Suggested change
def test_alternatives_prefers_scielo_web_over_raw_tif(self):
xml = etree.fromstring(
'<fig xmlns:xlink="http://www.w3.org/1999/xlink" id="F1">'
'<label>Graph 1</label>'
'<alternatives>'
'<graphic xlink:href="raw.tif"/>'
'<graphic xlink:href="web.png" specific-use="scielo-web"/>'
'<graphic xlink:href="thumb.jpg" specific-use="scielo-web" content-type="scielo-267x140"/>'
'</alternatives>'
'</fig>'
)
result = xml_pipe.extract_figure_data(xml)
self.assertEqual(result['href'], 'web.png')
def test_alternatives_prefers_scielo_web_over_raw_graphic(self):
xml = etree.fromstring(
'<fig xmlns:xlink="http://www.w3.org/1999/xlink" id="F1">'
'<label>Graph 1</label>'
'<alternatives>'
'<graphic xlink:href="raw.png"/>'
'<graphic xlink:href="web.jpg" specific-use="scielo-web"/>'
'<graphic xlink:href="thumb.jpg" specific-use="scielo-web" content-type="scielo-267x140"/>'
'</alternatives>'
'</fig>'
)
result = xml_pipe.extract_figure_data(xml)
self.assertEqual(result['href'], 'web.jpg')

alt_text = None

graphic = fig_node.find('.//graphic')
graphic = fig_node.find('graphic')

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.

Comento aqui porque não consigo comentar em código não participante do PR. O comentário seguinte serve para o arquivo packtools/packtools/sps/formats/pdf/pipeline/xml.py, linha 476 (candidates.sort(...)):

Agora que chega a esta ordenação, specific-use continua não participando da prioridade: a chave considera apenas thumbnail, área e extensão. Isso contradiz a intenção descrita no commit de preferir specific-use="scielo-web". Sugiro registrar is_scielo_web em cada candidato e ordenar, por exemplo, por (is_thumbnail, not is_scielo_web, -dims_area, ext_rank), preservando a rejeição de thumbnails antes de priorizar a variante web.


def _render_figures(docx, figures):
"""Render figures, switching to single column when the layout requires it."""
_flag_dpi_outliers(docx, figures)

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.

Sugiro remover a heurística de mediana deste PR. O caso a4 não a exercita nem depende dela: o resultado consistente já é obtido pela seleção das variantes PNG e pelo fallback de 300 DPI. Além disso, pertencer à mesma seção não implica compartilhar DPI ou escala física. No conjunto de XMLs/PDFs ampliado (26 arquivos), a mediana atua em apenas 3 de 116 figuras e prejudica um dos casos. Parece mais seguro usar o DPI próprio quando presente e aplicar 300 DPI somente quando os metadados estiverem ausentes.

@@ -38,7 +38,7 @@ def decide_figure_layout(docx, figure_data, page_attributes=pdf_enum.PAGE_ATTRIB
Decide whether a figure should occupy the full page width (double-column-layout) or a single column (single-column-layout).

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.

A documentação está invertendo os labels usados pela implementação. SINGLE_COLUMN_PAGE_LABEL cria uma seção de uma coluna e permite largura total; DOUBLE_COLUMN_PAGE_LABEL mantém a figura dentro de uma das duas colunas. Além disso, a assinatura usa threshold=1.1, mas a seção Args informa default 0.9. Sugiro corrigir a docstring para evitar que os próximos ajustes e testes sejam escritos com a interpretação oposta.

@pitangainnovare pitangainnovare left a comment

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.

Sugere-se atender aos comentários feitos no código.

Notas para outro PR/Issue:

  • re deveria ser importado no topo do módulo, e não dentro do processamento dos candidatos.

  • extract_figure_data está acumulando extração de legenda, resolução de atributos, seleção de representação, interpretação de content-type e ordenação de candidatos. Cabe refatoração para dividir em funções menores, dedicadas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pdf_generator: figuras da mesma seção renderizam com tamanhos inconsistentes devido a heurística de DPI não confiável

2 participants