Skip to content

fix: alinha DOI à direita e confina título do artigo à coluna própria - #1330

Open
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/pdf-header-doi-article-title-alignment
Open

fix: alinha DOI à direita e confina título do artigo à coluna própria#1330
Rossi-Luciano wants to merge 2 commits into
scieloorg:masterfrom
Rossi-Luciano:fix/pdf-header-doi-article-title-alignment

Conversation

@Rossi-Luciano

@Rossi-Luciano Rossi-Luciano commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

O que esse PR faz?

Resolve dois problemas do cabeçalho do PDF: o DOI (página 1) não ficava alinhado à margem direita, e o título do artigo nas páginas internas (>1) aparecia colado ao título do periódico em vez de confinado à própria área.

Os dois compartilhavam a mesma causa: DOI e título do periódico dividiam o mesmo parágrafo, separados só por um caractere de tabulação (\t). Uma tab stop define onde um texto começa, não onde ele quebra linha — uma linha que estoura volta para a margem esquerda do parágrafo, não para a posição da tab. Quando a segunda linha do título do periódico (_format_journal_title_two_lines) já era longa, não sobrava espaço na linha para o DOI alcançar a margem direita, e ele quebrava para uma 3ª linha em vez de ficar alinhado (visto em a30.xml: "URBE. / REVISTA BRASILEIRA DE GESTÃO URBANA" colidindo com o DOI).

A correção usa uma tabela sem borda de 2 colunas no cabeçalho (helper novo _add_two_column_header_table, reaproveitado pelos dois pares): título do periódico + DOI na página 1, título do periódico + título do artigo nas páginas seguintes (esse segundo par já usava uma tabela desde o fix de #1301; o helper foi extraído para os dois casos compartilharem). Cada coluna tem largura própria, então o conteúdo de uma nunca disputa espaço horizontal com o da outra.

O par periódico/DOI usa split 65/35 em vez de 50/50 (usado no par periódico/artigo, sem mudança): testei contra vários títulos longos do corpus real — títulos de 5-6 palavras não são raros (8 dos 26 artigos) — e contra o maior DOI do corpus (54 caracteres). 50/50 empurrava títulos longos para 3 linhas sem necessidade, já que o DOI cabe com folga em bem menos da metade do espaço. Não existe um split único que garanta 1 linha dos dois lados sempre (quebra de texto acontece em pontos discretos, não contínuos); 65/35 foi o que manteve o título do periódico em 2 linhas nos casos testados, aceitando que o DOI às vezes quebre em 2 linhas — mas sempre alinhado à direita, nunca colidindo com o título.

Onde a revisão poderia começar?

packtools/sps/formats/pdf/pipeline/docx.py:

  • _add_two_column_header_table (helper novo, no bloco de helpers privados)
  • docx_journal_title_pipe e docx_doi_pipe (página 1)
  • docx_second_header_pipe (páginas >1, já era tabela, só passou a reusar o helper)

Como este poderia ser testado manualmente?

python -m packtools.sps.formats.pdf_generator \
  -i <artigo-com-titulo-de-periodico-longo>.xml \
  -l layout.docx \
  -o saida.pdf \
  --libreoffice-binary libreoffice

Conferir a página 1 (DOI alinhado à direita, mesmo com título de periódico longo) e uma página interna (título do artigo confinado à direita, sem colidir com o título do periódico).

Algum cenário de contexto que queira dar?

Achado revisando visualmente o corpus de teste de 26 artigos reais (layout_examples_rafael/corpus/). Validado contra o corpus inteiro (as 26 amostras geram sem erro) e visualmente em a30.xml (o caso original do DOI colidindo) e a6.xml (título de artigo mais longo do corpus, 172 caracteres, para estressar a quebra de linha na coluna própria).

Screenshots

itemD_before_after

Quais são os tickets relevantes?

Nenhuma issue aberta associada; achado durante revisão visual do corpus de teste do gerador de PDF.

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)?

  • Sim — descreva os controles de proteção aplicados (criptografia, mascaramento, anonimização, etc.):
  • Não

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

  • Sim — descreva o que mudou e por quê:
  • Não

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

  • Sim — as novas dependências foram verificadas no SBOM/Trivy sem vulnerabilidades críticas/altas em aberto?
    • Verificado e aprovado
    • Pendente / vulnerabilidade aceita com justificativa:
  • Não

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

  • Sim — link do job:
  • Não aplicável a este PR (justifique): mudança isolada de layout de cabeçalho do DOCX (tabela em vez de tab stop), sem I/O externo ou entrada não confiável

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

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não

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

  • Sim — HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio?
  • Não

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

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

🤖 Generated with Claude Code

https://claude.ai/code/session_01X8r2LRJ3PGTT9vLaPtb373

Itens 1 e 5 de revisao visual do corpus: o DOI (pagina 1) e o titulo do
artigo no cabecalho corrido (paginas >1) dividiam o mesmo paragrafo do
titulo do periodico, separados so por um caractere de tabulacao.

Uma tab stop define so onde um run COMECA, nao onde ele quebra linha -
uma linha quebrada volta para a margem esquerda do paragrafo, nao para
a posicao da tab. Isso tinha dois efeitos:

- DOI (docx_doi_pipe): quando a segunda linha do titulo do periodico
  (_format_journal_title_two_lines) ja era longa, nao sobrava espaco
  na linha pra tab+DOI chegar na margem direita, e ele quebrava pra
  uma 3a linha em vez de ficar alinhado a direita (visto em a30.xml:
  "URBE. / REVISTA BRASILEIRA DE GESTAO URBANA" + DOI colidindo).
- Titulo do artigo (docx_second_header_pipe, ja tinha virado tabela no
  fix anterior de scieloorg#1301): esse problema nao se aplicava mais ali, mas
  o DOI da pagina 1 ainda usava o padrao antigo de tab.

Os dois casos agora usam uma tabela sem borda de 2 colunas (helper
compartilhado _add_two_column_header_table): titulo do periodico/DOI
na pagina 1, titulo do periodico/titulo do artigo nas paginas
seguintes. Cada coluna tem largura propria, entao o conteudo de uma
nunca disputa espaco horizontal com o da outra na mesma linha.

Par titulo do periodico/DOI usa split 65/35 em vez de 50/50 (usado no
par titulo do periodico/titulo do artigo, sem mudanca): testado contra
varios titulos longos do corpus real (a30, a13 - titulos de 5-6
palavras nao sao raros, 8 dos 26 artigos do corpus) e o maior DOI do
corpus (54 chars) - 50/50 empurrava titulos assim pra 3 linhas sem
necessidade, ja que o DOI cabe com folga em menos da metade do espaco.
Nao existe split unico que garanta 1 linha nos dois lados sempre (texto
quebra em pontos discretos, nao continuos); 65/35 foi o que manteve o
titulo do periodico em 2 linhas nos casos testados, aceitando que o DOI
as vezes quebra em 2 linhas (ainda alinhado a direita, nunca colidindo).

Validado contra as 26 amostras do corpus (todas geram sem erro) e
visualmente em a30.xml (DOI) e a6.xml (titulo de artigo mais longo do
corpus, 172 chars).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8r2LRJ3PGTT9vLaPtb373
r1 = para.add_run(_format_journal_title_two_lines(journal_title))
journal_para = journal_cell.paragraphs[0]
journal_para.style = docx.styles[paragraph_header_style_name]
r1 = journal_para.add_run(_format_journal_title_two_lines(journal_title))

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.

O cabeçalho corrido não deve reutilizar a formatação do masthead da primeira página. Essa quebra forçada faz urbe. Revista Brasileira de Gestão Urbana ocupar três linhas na página 2 quando combinada com a coluna de 50%. Use journal_title sem _format_journal_title_two_lines() e aplique SCL Header Paragraph Char ao run do periódico.

left_width = int(content_width * left_ratio)
right_width = content_width - left_width

table = container.add_table(rows=1, cols=2, width=content_width)

@pitangainnovare pitangainnovare Sep 5, 2026

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.

Header/Footer já começa com um parágrafo vazio. A tabela é inserida depois dele, deslocando o cabeçalho verticalmente. Sugiro remover os parágrafos de placeholder criados automaticamente pelo python-docx, após confirmar que eles não contém texto, runs ou conteúdo gráfico.

table = container.add_table(rows=1, cols=2, width=content_width)
table.autofit = False
table.allow_autofit = False

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 tabela padrão mantém w:tblCellMar (≈5,4 pt), criando recuo nas duas extremidades e desalinhando o cabeçalho do corpo. Configure w:tblCellMar com top/left/bottom/right = 0.

@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.

Em teste manual com vários XMLs, percebi alguns problemas:

  1. No cabeçalho corrido, o título do periódico continua usando _format_journal_title_two_lines() e o estilo SCL Journal Title Char, que pertence ao masthead da primeira página. Com a nova coluna de 50%,
    isso faz urbe. Revista Brasileira de Gestão Urbana ocupar três linhas. A sugestão é usar diretamente journal_title e aplicar SCL Header Paragraph Char, mantendo o periódico em uma única linha sempre que
    houver espaço.

  2. O Header/Footer criado pelo python-docx contém um parágrafo placeholder vazio. Como add_table() insere a tabela depois desse parágrafo, o cabeçalho da primeira página passa de aproximadamente y=28,3 pt para y=41,0 pt. A sugestão é remover somente esse placeholder automático, verificando que ele realmente não possui texto, runs ou conteúdo gráfico, sem apagar parágrafos vazios intencionais.

  3. As células da tabela mantêm margens internas laterais de aproximadamente 5,4 pt. Na renderização, o cabeçalho começa em x=62,2 pt, enquanto o corpo começa em x=56,8 pt. A sugestão é sobrescrever
    w:tblCellMar, ao menos para left e right, com valor zero. Zerar os quatro lados também é aceitável para esse cabeçalho.

  4. Como ajuste adicional, para.alignment = WD_ALIGN_PARAGRAPH.RIGHT deveria ficar depois do if/else externo de docx_doi_pipe. Atualmente, quando o chamador fornece paragraph, o DOI perde tanto a
    tabulação removida pelo PR quanto o alinhamento à direita. Esse caminho deveria receber um teste específico.

…e zera margem de celula

Atende a revisao de @pitangainnovare no PR scieloorg#1330:

1. docx_second_header_pipe (cabecalho a partir da pagina 2) reutilizava
   _format_journal_title_two_lines() e o estilo SCL Journal Title Char do
   masthead da pagina 1, o que forcava titulos de periodico longos (ex.:
   "urbe. Revista Brasileira de Gestao Urbana") para 3 linhas nessa coluna
   mais estreita. Passa a escrever o titulo sem quebra artificial, com o
   estilo pequeno SCL Header Paragraph Char, que ja usamos no resto do
   cabecalho. Confirmado contra a10.xml/a30.xml do corpus real: titulo
   volta a caber em 1 linha.

2. python-docx cria um paragrafo vazio ao acessar header/footer pela
   primeira vez; add_table() insere a tabela depois dele, deslocando o
   conteudo para baixo. _remove_leading_empty_placeholder_paragraph
   remove esse paragrafo, mas so quando ele nao tem texto nem runs (logo
   nenhum conteudo grafico, que sempre vive dentro de um run).

3. O default OOXML de margem de celula (108 twips = 5.4pt, valor exato
   medido pelo revisor) desalinhava o inicio do texto do cabecalho em
   relacao ao corpo, que nao tem essa margem. _zero_table_cell_margins
   zera left/right em w:tblCellMar.

4. Em docx_doi_pipe, o alinhamento a direita so era aplicado quando a
   funcao criava a propria celula; movido para depois do if/else, entao
   tambem se aplica quando um paragraph= existente e passado (caminho
   sem nenhum teste antes).

Suite completa do pipeline PDF: 198 testes passando (191 + 7 novos).
@Rossi-Luciano

Copy link
Copy Markdown
Contributor Author

Obrigado pela revisão. Os 4 pontos procedem, corrigidos em a09d817:

  1. Cabeçalho corrido (página 2+): parou de reutilizar _format_journal_title_two_lines() e o estilo SCL Journal Title Char do masthead. Agora escreve o título sem quebra artificial, com SCL Header Paragraph Char, como sugerido. Validado no corpus real (a30.xml, journal-title "urbe. Revista Brasileira de Gestão Urbana"): o título que quebrava em 3 linhas na coluna de 50% agora fica em 1 linha só, já que o estilo pequeno cabe com folga mesmo sem a proporção 65/35 usada no cabeçalho da página 1.
  2. Parágrafo placeholder do Header/Footer: confirmei o mecanismo com python-docx (um <w:p/> vazio sempre precede a <w:tbl/> recém-criada) e adicionei _remove_leading_empty_placeholder_paragraph, que remove esse parágrafo só quando ele não tem texto nem runs (logo, sem conteúdo gráfico também, já que isso sempre vive dentro de um run).
  3. Margem interna da tabela: confirmei que o valor de 5,4pt é exatamente o padrão OOXML (108 twips) quando w:tblCellMar não é definido. Adicionada _zero_table_cell_margins, zerando left/right.
  4. Alinhamento do DOI: para.alignment = WD_ALIGN_PARAGRAPH.RIGHT movido para depois do if/else, então passa a valer também quando um paragraph= existente é passado. Adicionado teste cobrindo esse caminho, que antes não tinha nenhum.

Suíte completa do pipeline PDF: 198 testes passando (191 + 7 novos).

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.

2 participants