fix: corrige decisão de layout de figuras no gerador de PDF (unidade cm/EMU e DPI de figuras irmãs) - #1294
Conversation
…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).
| 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') |
There was a problem hiding this comment.
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.
| 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') |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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). | |||
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Sugere-se atender aos comentários feitos no código.
Notas para outro PR/Issue:
-
redeveria ser importado no topo do módulo, e não dentro do processamento dos candidatos. -
extract_figure_dataestá acumulando extração de legenda, resolução de atributos, seleção de representação, interpretação decontent-typee ordenação de candidatos. Cabe refatoração para dividir em funções menores, dedicadas.
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:single_col_widthdegradava deCmpara um número cru em EMU após subtração/divisão (python-docx não sobrecarrega operadores deLength), tornando o ramo de largura total inatingível na prática para qualquer imagem real.<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.tifbruto 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çãodecide_figure_layout(e a novaprobe_image_dpi). Depoispacktools/sps/formats/pdf/pipeline/docx.py, função_flag_dpi_outliers.Como este poderia ser testado manualmente?
Com o LibreOffice instalado:
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 1aparecia 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
Quais são os tickets relevantes?
Refs #1278. Closes #1293.
Referências
N/A
Segurança da informação (NSI.04)
Este PR manipula dados sensíveis ou pessoais (LGPD)?
Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?
Este PR introduz, atualiza ou remove dependências de terceiros?
Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?
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?
Este PR expõe novos endpoints, telas ou serviços?
Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?