Skip to content

Introduz LayoutConfig: layout de página/coluna configurável por periódico - #1279

Open
Rossi-Luciano wants to merge 18 commits into
scieloorg:masterfrom
Rossi-Luciano:feat/pdf-layout-config
Open

Introduz LayoutConfig: layout de página/coluna configurável por periódico#1279
Rossi-Luciano wants to merge 18 commits into
scieloorg:masterfrom
Rossi-Luciano:feat/pdf-layout-config

Conversation

@Rossi-Luciano

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

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Resolve o problema descrito na #1278: o gerador de PDF decide layout (1/2 colunas, largura de tabela/figura) através de duas heurísticas isoladas e desconectadas — DPI de imagem em figure.py, contagem de coluna em xml.py — que não retornam largura real nem justificativa, sobre uma geometria de página fixa em A4/2-colunas (enum.py::PAGE_ATTRIBUTES), sem possibilidade de configuração por periódico.

Introduz LayoutConfig/PageProfile/LayoutDecision (packtools/sps/formats/pdf/layout_config.py) como abstração central: tabela e figura passam a consultar o mesmo contexto de layout, cada decisão vira um objeto com largura + justificativa + origem (medido/heurística), e um perfil de página por periódico pode ser calibrado via profiles/{issn_epub}.json — detectado automaticamente a partir do ISSN eletrônico no XML de entrada, sem precisar de flag manual, com fallback para o default atual (A4, 2 colunas) quando o periódico não estiver calibrado.

Calibrar 3 periódicos reais por medição contra PDF publicado (Acta Amazonica, Acta Botanica Brasilica, Anuário Antropológico) revelou e corrigiu, no caminho, dois bugs reais e independentes deste trabalho:

  • _compute_single_column_width/_compute_table_width assumiam sempre 2 colunas (/2 fixo) — só quebra visivelmente num periódico calibrado pra 1 coluna.
  • _try_insert_picture inseria imagem sem largura explícita, deixando o python-docx usar sua própria leitura de DPI, independente da usada pelo resto do código — confirmado por medição que uma imagem sem metadado de DPI diverge ~33% entre as duas leituras (72dpi vs. 96dpi de fallback).

Onde a revisão poderia começar?

  • packtools/sps/formats/pdf/layout_config.py — o módulo novo (PageProfile, LayoutDecision, LayoutConfig, load_profile).
  • packtools/sps/formats/pdf/pipeline/docx.py::pipeline_docx — onde o ISSN é detectado e o perfil carregado automaticamente.
  • packtools/sps/formats/pdf/renderer/docx/figure.py::_try_insert_picture — a correção do bug de inserção de imagem sem largura explícita (achada em revisão, com teste de regressão verificado contra o código sem a correção).

Como este poderia ser testado manualmente?

Com a fixture já presente no repositório:

python -m packtools.sps.formats.pdf_generator \
  -i tests/fixtures/pdf/a1.xml \
  -l tests/fixtures/pdf/layout.docx \
  -o /tmp/a1_novo.pdf \
  --libreoffice-binary libreoffice

O PDF gerado deve ter altura de página 793.7pt (não A4) e a Figura 1 em largura total na página 3 — comparável a tests/fixtures/pdf/a1.pdf.

Os outros 2 periódicos calibrados (Acta Botanica Brasilica, Anuário Antropológico) foram validados com artigos reais baixados do SciELO — XML/PDF não fazem parte das fixtures deste repositório, então não são reproduzíveis diretamente a partir daqui; ver "Screenshots" abaixo para a evidência visual.

A suíte automatizada cobre os 3 casos (1 coluna, 2 colunas, alternância temporária):

python -m pytest tests/sps/formats/pdf/ -v

Algum cenário de contexto que queira dar?

Este PR nasceu de investigação empírica, não de design abstrato: parti da fixture real já existente no repositório (a1.xml/a1.pdf, Acta Amazonica) e rodei o pipeline completo comparando com o PDF oficial publicado pelo periódico. Isso expôs o problema de raiz. Calibrando mais dois periódicos com características deliberadamente diferentes (Acta Botanica Brasilica: 2 colunas mas altura de página diferente da Acta Amazonica; Anuário Antropológico: o único de 1 coluna) os dois bugs adicionais listados acima só apareceram porque saí do caso "sempre 2 colunas" — ficaram invisíveis nos testes anteriores.

Uma limitação foi encontrada e deliberadamente não resolvida neste PR: o dimensionamento de largura de figura dentro do teto disponível (quando não precisa de largura total) não tem regra geral confirmada — uma hipótese de DPI de metadado não confiável bateu para a Acta Amazonica e foi contrariada pela Acta Botanica Brasilica (onde o alvo real usa ~2.2x o tamanho nativo confiável da imagem). Documentado como limitação conhecida em profiles/1677-941X.json e na própria #1278, não escondido.

Screenshots

Acta Amazonica — altura de página e quebra/retorno de coluna ao redor da Figura 1 batendo com o PDF oficial:

aa_fig1_final

Acta Botanica Brasilica — Tabela 1 convergindo bem; Figura 1 mostrando a limitação conhecida (destacada, não escondida):

abb_fig1_final abb_fig3_final abb_tabela1_final

Anuário Antropológico — único periódico de 1 coluna calibrado; corpo de texto e Figura 2 (largura total) batendo com o PDF oficial:

aantropol_corpo_final aantropol_fig2_final aantropol_fig4_final

Quais são os tickets relevantes?

Refs #1278 — não fecha a issue: ficam em aberto lá a seleção manual de 1/2 colunas via API/CLI, ligar largura real medida na decisão de tabela, e calibrar mais periódicos além dos 3 iniciais.

Referências


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

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 — usa apenas dependências já existentes no projeto (python-docx, Pillow, lxml).

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

  • Não aplicável a este PR — repositório não tem pipeline de Sonar/Trivy configurado; validação feita via suíte de testes local (tests/sps/formats/pdf/, 143 testes) e regeneração ponta a ponta via CLI real.

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

  • Não — mudanças restritas a cálculo de geometria de layout e leitura de arquivos JSON de configuração locais (profiles/*.json, empacotados com o próprio pacote).

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.

…ura + justificativa)

Propósito:
Nenhuma abstração reutilizável existe hoje pra decisão de layout (1/2
colunas, largura de tabela/figura) — cada ponto do pipeline resolve isso
sozinho, sem retornar largura real nem justificativa.

Solução técnica:
- PageProfile: geometria de página/coluna/margem, com default igual ao
  comportamento atual (A4, 2 colunas) e campos calibráveis por periódico.
- LayoutDecision: largura + justificativa + origem (medido/heurística),
  não só um rótulo de string.
- LayoutConfig: decide_table_layout/decide_figure_layout unificam a
  decisão de largura; full_width() abstrai quebra/restauração temporária
  de coluna.
- load_profile(issn_epub): carrega profiles/{issn}.json, com fallback pro
  default quando o periódico não estiver calibrado.

Refs scieloorg#1278
Propósito:
Fornecer o primeiro perfil real de periódico para LayoutConfig, calibrado
por medição direta contra PDF publicado.

Solução técnica:
- profiles/1809-4392.json: altura de página 793.7pt (não é A4) e 2
  colunas, medidos contra tests/fixtures/pdf/a1.pdf.

Refs scieloorg#1278
Propósito:
Segundo periódico calibrado, usado para validar que LayoutConfig
generaliza além do primeiro caso.

Solução técnica:
- profiles/1677-941X.json: altura de página 793.7pt e 2 colunas.
- Documenta em known_limitations que o dimensionamento de largura de
  figura dentro do teto não segue regra geral descoberta (Figura 1 do
  PDF real usa ~2.2x o tamanho nativo confiável) — deliberadamente não
  resolvido aqui, ver scieloorg#1278.

Refs scieloorg#1278
Propósito:
Terceiro periódico calibrado — o único de 1 coluna entre os três,
usado para expor bugs que só aparecem fora do caso 2-colunas.

Solução técnica:
- profiles/2357-738X.json: altura de página A4 (841.89pt) e 1 coluna.

Refs scieloorg#1278
Propósito:
Cobrir o módulo novo antes de qualquer código do pipeline passar a
depender dele.

Solução técnica:
- TestPageProfile: aritmética de largura de conteúdo/coluna, roundtrip
  to_dict/from_dict, to_page_attributes.
- TestLayoutConfigColumns: column_count default vs. explícito,
  full_width() com restauração (inclusive aninhado e sob exceção).
- TestDecideTableLayout/TestDecideFigureLayout: fallback heurístico vs.
  largura medida, threshold configurável.
- TestLoadProfile: perfil ausente cai no default; os 3 perfis reais
  calibrados carregam com os valores esperados.

Refs scieloorg#1278
…trônico

Propósito:
A decisão de largura de tabela era uma heurística isolada (">4 colunas"),
sem acesso à geometria real de coluna do periódico; e não havia como
descobrir automaticamente qual periódico um XML pertence.

Solução técnica:
- determine_table_layout/extract_table_data/extract_body_data recebem
  layout_config opcional; comportamento anterior preservado quando
  omitido.
- extract_issn_epub(xml_tree): lê <issn pub-type="epub">, o mesmo
  identificador que o SciELO já usa como chave em
  minio.scielo.br/documentstore/ — permite carregar o perfil do
  periódico direto do XML de entrada, sem flag manual.

Refs scieloorg#1278
Propósito:
Cobrir as mudanças em xml.py, incluindo o caso limite descoberto durante
a escrita do teste.

Solução técnica:
- TestExtractIssnEpub: ISSN presente/ausente.
- TestDetermineTableLayout: comportamento sem layout_config (heurística
  legada preservada) e com layout_config. Documenta explicitamente uma
  limitação encontrada ao escrever o teste: determine_table_layout não
  tem largura real por coluna disponível nesse ponto do pipeline, então
  mesmo com layout_config a decisão ainda cai no fallback heurístico de
  contagem de coluna — column_count por si só não muda esse resultado
  ainda (ver subtarefa aberta na scieloorg#1278).

Refs scieloorg#1278
…rrige inserção de imagem sem largura explícita

Propósito:
A decisão de largura de figura era uma heurística de DPI isolada, sem
acesso à geometria real de coluna do periódico; e _compute_single_column_width
assumia sempre 2 colunas, o que só quebra visivelmente num periódico de
1 coluna. Revisando o diff antes do commit, achado um terceiro problema:
_try_insert_picture inseria a imagem sem largura explícita, deixando o
python-docx usar sua própria leitura de DPI — independente da usada pelo
resto do código (_infer_image_dpi) — e silenciosamente ignorando a
largura calculada sempre que essa leitura própria desse um valor maior.

Solução técnica:
- decide_figure_layout/add_figure/_compute_single_column_width recebem
  layout_config opcional; comportamento anterior preservado quando
  omitido.
- _compute_single_column_width, com layout_config, usa
  layout_config.column_width_pt em vez da fórmula fixa "sempre 2
  colunas".
- _natural_width_capped: tamanho natural da imagem (via DPI) limitado
  pelo teto disponível, sem nunca esticar além do tamanho nativo.
- _try_insert_picture passa a chamar add_picture(img_path,
  width=content_width) explicitamente, em vez de deixar o python-docx
  inferir sozinho — confirmado por medição que uma imagem sem metadado
  de DPI diverge ~33% entre as duas leituras (72dpi vs. 96dpi de
  fallback).

Refs scieloorg#1278
Propósito:
Primeira cobertura de teste pra renderer/docx/ (inexistente até então,
gap já catalogado na issue scieloorg#1275). Inclui teste de regressão pro bug
achado em revisão no _try_insert_picture.

Solução técnica:
- TestComputeSingleColumnWidth: fórmula legada vs. layout_config em
  periódico de 1 e 2 colunas.
- TestNaturalWidthCapped: fallback pro teto quando a imagem não existe.
- TestTryInsertPictureUsesExplicitWidth: confirma que a largura inserida
  é exatamente a largura pedida, tanto menor quanto maior que a leitura
  própria do python-docx — este último caso é o que expõe o bug; testado
  contra uma cópia temporária do código sem a correção pra confirmar que
  falha (3810000 != 5000000) antes de aceitar o teste como válido.

Refs scieloorg#1278
Propósito:
Mesmo bug de _compute_single_column_width (figure.py), agora em tabela:
a largura "cabe na coluna" assumia sempre 2 colunas, dividindo o
conteúdo por 2 mesmo quando o periódico só tem 1.

Solução técnica:
- add_table/_compute_table_width recebem layout_config opcional;
  comportamento anterior preservado quando omitido.
- Com layout_config, usa layout_config.column_width_pt em vez da fórmula
  fixa.

Refs scieloorg#1278
Propósito:
Cobrir _compute_table_width, incluindo o bug de largura fixa "sempre 2
colunas" corrigido no commit anterior.

Solução técnica:
- Layout de largura total ignora layout_config (já usa content_width
  inteiro, correto por definição).
- Layout "cabe na coluna" testado sem layout_config (fórmula legada) e
  com layout_config de 1 e 2 colunas.

Refs scieloorg#1278
…icamente

Propósito:
Amarra o trabalho dos commits anteriores: sem isso, LayoutConfig existia
mas nada no pipeline principal o construía nem o propagava.

Solução técnica:
- pipeline_docx detecta o ISSN eletrônico do XML de entrada
  (xml.py::extract_issn_epub) e carrega o perfil calibrado
  (layout_config.py::load_profile), com fallback pro default (A4, 2
  colunas) quando o periódico não estiver calibrado. Um
  data['layout_config'] explícito tem precedência sobre a detecção
  automática.
- docx_body_pipe, _setup_two_column_body_section, _add_two_column_section,
  _render_tables, _render_figures, _figure_layout propagam layout_config
  pro resto da árvore de chamada.
- docx_setup_sections passa a receber page_attributes derivado de
  layout_config.profile.to_page_attributes(), aplicando a altura de
  página calibrada em vez do A4 hardcoded.

Confirmado localmente:
143 testes na suíte de packtools/sps/formats/pdf (0 regressão).
Reconfirmado ponta a ponta via CLI real
(packtools.sps.formats.pdf_generator, sem monkeypatch) nos 3 periódicos
calibrados — mesmos resultados de página/coluna já validados antes desta
migração pro repositório.

Refs scieloorg#1278
…erride

Propósito:
O campo existia no schema desde a migração inicial mas nunca tinha sido
lido em lugar nenhum do código — a docstring também não definia o que
ele deveria escalar. Achado revisando o PR: a comparação visual da Acta
Amazonica mostrou a Figura 1 bem mais larga que o alvo real, e o campo
que deveria calibrar isso estava vazio E sem uso.

Solução técnica:
- Docstring de PageProfile atualizada: figure_width_scale_override
  escala o teto de largura (não o tamanho natural da imagem), então só
  afeta figura que já estaria sendo limitada pelo teto — não achata uma
  figura pequena que já cabe no espaço disponível.
- Documenta o contra-exemplo já conhecido (Acta Botanica Brasilica,
  profiles/1677-941X.json): um fator único por periódico não serve pra
  esse caso, porque a Figura 1 de lá precisa ser MAIOR, não menor, que o
  tamanho nativo confiável — direção oposta.

Refs scieloorg#1278
Propósito:
Sem isso, o campo documentado no commit anterior continuava sem efeito
nenhum no PDF gerado — o teto de largura calculado nunca era ajustado
por ele, então nenhum periódico conseguia de fato ser calibrado.

Solução técnica:
- add_figure multiplica ceiling_width por
  layout_config.profile.figure_width_scale_override quando definido,
  antes de passar pra _natural_width_capped. None (default) mantém o
  comportamento atual, sem ajuste.

Confirmado localmente:
Com profiles/1809-4392.json calibrado (ver próximo commit), a Figura 1
da Acta Amazonica passou a sair em 348.7pt no PDF gerado — antes 481.9pt
— batendo com a medição real do PDF publicado (348.7pt), conferido por
correspondência de hash de imagem, não só posição de página.

Refs scieloorg#1278
…Amazonica

Propósito:
Fechar o gap encontrado na revisão visual do PR: a Figura 1 gerada
saía ~38% mais larga que o PDF oficial (481.9pt vs. 348.7pt medidos).

Solução técnica:
- figure_width_scale_override: 0.7236 = 348.7pt (Figura 1 medida no PDF
  real) / 481.9pt (teto de largura total computado pelo packtools).
  Calibrado a partir de um único dado (Figura 1, a única figura deste
  fixture) — pode não generalizar pra outras figuras deste periódico se
  mais forem calibradas no futuro.

Refs scieloorg#1278
… Anuário Antropológico

Propósito:
Registrar um achado da revisão visual do PR antes que se perca: ao
verificar por hash de imagem 7 das 11 figuras do artigo contra o PDF
real, um padrão diferente do da Acta Amazonica apareceu.

Solução técnica:
- known_limitations documenta: Figuras 1/9/10/11 (tamanho natural já
  cabe no teto) batem quase exatamente com o alvo (~357-358pt nos dois
  lados); Figuras 3/4/5 saem ~30% mais largas no gerado (ex.: Figura 4,
  357pt no alvo vs. ~468pt gerado), mesmo tendo tamanho natural
  provavelmente diferente entre si. Isso sugere que o periódico pode
  usar uma largura FIXA (~357pt) pra figura dentro do texto,
  independente do tamanho natural de cada imagem — um tipo de parâmetro
  diferente do figure_width_scale_override (que escala o teto, não
  fixa um valor). Deliberadamente não implementado: precisaria de um
  campo novo (ex.: figure_width_fixed_pt) e confirmação em mais de 7
  figuras amostradas antes de virar regra confiável.

Refs scieloorg#1278
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.

3 participants