-
Notifications
You must be signed in to change notification settings - Fork 24
fix: alinha DOI à direita e confina título do artigo à coluna própria #1330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Rossi-Luciano
wants to merge
2
commits into
scieloorg:master
Choose a base branch
from
Rossi-Luciano:fix/pdf-header-doi-article-title-alignment
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,23 @@ | ||
| from docx.shared import Pt | ||
| from docx.enum.text import WD_ALIGN_PARAGRAPH | ||
| from docx.oxml import OxmlElement | ||
| from docx.oxml.ns import qn | ||
| from docx.shared import Cm, Pt | ||
| from docx.text.paragraph import Paragraph | ||
|
|
||
| from packtools.sps.formats.pdf import enum as pdf_enum | ||
| from packtools.sps.formats.pdf.pipeline import xml as xml_pipe | ||
| from packtools.sps.formats.pdf.renderer import docx as docx_renderer | ||
| from packtools.sps.formats.pdf.utils import xml_utils | ||
|
|
||
| # Share of the first-page header's width given to the journal title column | ||
| # (the rest goes to the DOI). Skewed well past an even 50/50: a DOI URL is | ||
| # short and fairly constant in length (max ~55 chars across the real test | ||
| # corpus), while the journal title uses a much larger masthead font and | ||
| # genuinely needs the room - an even split pushed some real journal titles | ||
| # (e.g. "Urbe. Revista Brasileira de Gestão Urbana") from 2 to 3 lines for | ||
| # no benefit to the DOI, which fits comfortably either way. | ||
| _JOURNAL_TITLE_DOI_SPLIT = 0.65 | ||
|
|
||
|
|
||
| def pipeline_docx(xml_tree, data): | ||
| """ | ||
|
|
@@ -109,18 +122,28 @@ def pipeline_docx(xml_tree, data): | |
|
|
||
| def docx_journal_title_pipe(docx, journal_title_text, style_name='SCL Journal Title Char'): | ||
| """ | ||
| Adds the journal title text to the first page header of the DOCX document, with each word on a new line. | ||
|
|
||
| Adds the journal title text to the first page header of the DOCX document, | ||
| capped at two lines. | ||
|
|
||
| Written into the left cell of a borderless 2-column table shared with | ||
| the DOI (see docx_doi_pipe): giving the DOI its own column, rather than | ||
| tab-appending it after this text in the same paragraph, means a long | ||
| second line here never leaves the DOI without room to reach the right | ||
| margin on that line. | ||
|
|
||
| Args: | ||
| docx (python-docx.Document): The DOCX document object. | ||
| journal_title_text (str): The text of the journal title to be added. | ||
| style_name (str, optional): The name of the style to apply to the journal title text. Defaults to 'SCL Journal Title Char'. | ||
|
|
||
| Returns: | ||
| python-docx.Paragraph: The paragraph object containing the journal title text. | ||
| """ | ||
| first_page_header = docx_renderer.section.get_first_page_header(docx) | ||
| para = docx_renderer.text.get_first_paragraph(first_page_header) | ||
| journal_cell, _doi_cell = _add_two_column_header_table( | ||
| first_page_header, left_ratio=_JOURNAL_TITLE_DOI_SPLIT | ||
| ) | ||
| para = journal_cell.paragraphs[0] | ||
|
|
||
| left_run = para.add_run(_format_journal_title_two_lines(journal_title_text)) | ||
| left_run.style = docx.styles[style_name] | ||
|
|
@@ -129,14 +152,23 @@ def docx_journal_title_pipe(docx, journal_title_text, style_name='SCL Journal Ti | |
|
|
||
| def docx_doi_pipe(docx, doi_code, paragraph=None, style_name='SCL Header Paragraph Char'): | ||
| """ | ||
| Adds the DOI (Digital Object Identifier) code to the first page header of the DOCX document, with the DOI URL formatted as a tab-indented string. | ||
|
|
||
| Adds the DOI (Digital Object Identifier) code to the first page header of the DOCX document. | ||
|
|
||
| Written into the right cell of the borderless 2-column table docx_journal_title_pipe | ||
| creates (or, if that hasn't run yet, a fresh one of its own), right-aligned within | ||
| that cell. Previously the DOI was tab-appended after the journal title in one shared | ||
| paragraph; a tab stop only sets where a run *starts*, not where it wraps, so a long | ||
| second line of the journal title (see _format_journal_title_two_lines) left no room | ||
| on that line for the DOI to reach the right margin, forcing it onto a line of its own | ||
| instead of sitting flush right. A separate column has its own width regardless of how | ||
| much text is in the journal title's cell. | ||
|
|
||
| Args: | ||
| docx (python-docx.Document): The DOCX document object. | ||
| doi_code (str): The DOI code to be added. | ||
| paragraph (python-docx.Paragraph, optional): The paragraph object to add the DOI URL to. If not provided, the first paragraph in the first page header will be used. | ||
| paragraph (python-docx.Paragraph, optional): The paragraph object to add the DOI URL to. If not provided, the DOI cell of the first page header's title table is used (creating that table if docx_journal_title_pipe hasn't run yet). | ||
| style_name (str, optional): The name of the style to apply to the DOI URL. Defaults to 'SCL Header Paragraph Char'. | ||
|
|
||
| Returns: | ||
| None | ||
| """ | ||
|
|
@@ -146,9 +178,19 @@ def docx_doi_pipe(docx, doi_code, paragraph=None, style_name='SCL Header Paragra | |
| para = paragraph | ||
| else: | ||
| first_page_header = docx_renderer.section.get_first_page_header(docx) | ||
| para = docx_renderer.text.get_first_paragraph(first_page_header) | ||
|
|
||
| r = para.add_run(f'\t{doi_url}') | ||
| if first_page_header.tables: | ||
| para = first_page_header.tables[-1].rows[0].cells[1].paragraphs[0] | ||
| else: | ||
| _journal_cell, doi_cell = _add_two_column_header_table( | ||
| first_page_header, left_ratio=_JOURNAL_TITLE_DOI_SPLIT | ||
| ) | ||
| para = doi_cell.paragraphs[0] | ||
|
|
||
| # Right-aligned regardless of which branch supplied `para`: a caller | ||
| # passing its own `paragraph` wants the DOI right-aligned in it too, | ||
| # not just when this function creates the cell itself. | ||
| para.alignment = WD_ALIGN_PARAGRAPH.RIGHT | ||
| r = para.add_run(doi_url) | ||
| r.style = docx.styles[style_name] | ||
|
|
||
| def docx_article_type_and_category_pipe(docx, category, article_type='Original Article', style_name='SCL Article Category'): | ||
|
|
@@ -335,12 +377,12 @@ def docx_cite_as_pipe( | |
| docx_renderer.style.add_run_with_style(para, f'{cite_as_part_two}.', p2_style) | ||
|
|
||
| def docx_second_header_pipe( | ||
| docx, | ||
| journal_title, | ||
| article_title, | ||
| docx, | ||
| journal_title, | ||
| article_title, | ||
| paragraph_header_style_name='SCL Header Paragraph', | ||
| character_header_style_name='SCL Header Paragraph Char', | ||
| paragraph_title_style_name='SCL Journal Title Char' | ||
| paragraph_title_style_name='SCL Header Paragraph Char' | ||
| ): | ||
| """ | ||
| Adds the journal title and article title to the second page header of the DOCX document. | ||
|
|
@@ -352,6 +394,22 @@ def docx_second_header_pipe( | |
| the previous section, and the second section needs a continuous break | ||
| to let body content start on the same page as the front matter. | ||
|
|
||
| Rendered as a borderless 2-column table rather than tab-separated runs | ||
| in one paragraph: a tab stop only positions where a run *starts*, it | ||
| doesn't constrain where a long run *wraps* (a wrapped line returns to | ||
| the paragraph's own left margin, not back to the tab position). A long | ||
| article title would otherwise overflow past the journal title's column | ||
| instead of wrapping under it. Each column gets half of the content | ||
| width; the article title is right-aligned within its own column. | ||
|
|
||
| The journal title is written as plain, unbroken text in the small | ||
| 'SCL Header Paragraph Char' style, not the masthead's | ||
| _format_journal_title_two_lines()/'SCL Journal Title Char' treatment: | ||
| that treatment is sized for the large first-page masthead, and forcing | ||
| it into this running header's column (already narrower, and further | ||
| split with the article title) pushed titles that fit the masthead in | ||
| two lines into three lines here instead. | ||
|
|
||
| Args: | ||
| docx (python-docx.Document): The DOCX document object. | ||
| journal_title (str): The title of the journal to be added. | ||
|
|
@@ -363,13 +421,17 @@ def docx_second_header_pipe( | |
| None | ||
| """ | ||
| header = docx_renderer.section.get_default_header(docx) | ||
| para = header.add_paragraph() | ||
| para.style = docx.styles[paragraph_header_style_name] | ||
| journal_cell, title_cell = _add_two_column_header_table(header) | ||
|
|
||
| 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(journal_title) | ||
| r1.style = docx.styles[paragraph_title_style_name] | ||
|
|
||
| r2 = para.add_run(f'\t{article_title}') | ||
| title_para = title_cell.paragraphs[0] | ||
| title_para.style = docx.styles[paragraph_header_style_name] | ||
| title_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT | ||
| r2 = title_para.add_run(article_title) | ||
| r2.style = docx.styles[character_header_style_name] | ||
|
|
||
| def docx_second_footer_pipe(docx, footer_data, paragraph_style_name='SCL Footer'): | ||
|
|
@@ -530,6 +592,92 @@ def _format_journal_title_two_lines(journal_title_text): | |
| return f"{words[0]}\n{' '.join(words[1:])}" | ||
|
|
||
|
|
||
| def _content_width(): | ||
| """Content width (page width minus left/right margins), from PAGE_ATTRIBUTES.""" | ||
| attrs = pdf_enum.PAGE_ATTRIBUTES | ||
| page_width = attrs.get('page_width', Cm(21.0)) | ||
| left_margin = attrs.get('left_margin', Cm(2.0)) | ||
| right_margin = attrs.get('right_margin', Cm(2.0)) | ||
| return page_width - left_margin - right_margin | ||
|
|
||
|
|
||
| def _add_two_column_header_table(container, left_ratio=0.5): | ||
| """ | ||
| Add a borderless 1x2 table to a header (or footer) container, its two | ||
| columns splitting the content width by left_ratio (right column gets | ||
| the remainder). No style is assigned, so it keeps python-docx's | ||
| default 'Normal Table' style, which has no borders. | ||
|
|
||
| Returns: | ||
| tuple: (left_cell, right_cell) | ||
| """ | ||
| content_width = int(_content_width()) | ||
| left_width = int(content_width * left_ratio) | ||
| right_width = content_width - left_width | ||
|
|
||
| table = container.add_table(rows=1, cols=2, width=content_width) | ||
| table.autofit = False | ||
| table.allow_autofit = False | ||
| _remove_leading_empty_placeholder_paragraph(table) | ||
| _zero_table_cell_margins(table) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A tabela padrão mantém |
||
| left_cell, right_cell = table.rows[0].cells | ||
| for column, width in zip(table.columns, (left_width, right_width)): | ||
| column.width = width | ||
| for cell, width in ((left_cell, left_width), (right_cell, right_width)): | ||
| cell.width = width | ||
|
|
||
| return left_cell, right_cell | ||
|
|
||
|
|
||
| def _remove_leading_empty_placeholder_paragraph(table): | ||
| """ | ||
| Removes the empty paragraph that precedes `table` in its container, if | ||
| and only if that paragraph is truly empty (no text, no runs, so no | ||
| graphic content either, since a drawing lives inside a run). | ||
|
|
||
| python-docx auto-creates one empty paragraph the first time a header or | ||
| footer's body is accessed, before any content is added to it. Because | ||
| `container.add_table()` appends the table after whatever is already | ||
| there, that placeholder paragraph ends up immediately before the table, | ||
| and being a paragraph (even an empty one) it still reserves a line's | ||
| worth of vertical space above it, pushing the table down. | ||
| """ | ||
| previous = table._tbl.getprevious() | ||
| if previous is None or previous.tag != qn('w:p'): | ||
| return | ||
| placeholder = Paragraph(previous, table._parent) | ||
| if placeholder.text or placeholder.runs: | ||
| return | ||
| previous.getparent().remove(previous) | ||
|
|
||
|
|
||
| def _zero_table_cell_margins(table, sides=('left', 'right')): | ||
| """ | ||
| Zero the given cell-margin sides on a table's default cell margins | ||
| (w:tblCellMar in w:tblPr). Without this, a python-docx table keeps the | ||
| OOXML default of 108 twips (5.4pt) on every side, which offsets a | ||
| header/footer table's content from the flush-left/flush-right text used | ||
| everywhere else in the document (a plain paragraph has no such margin). | ||
| """ | ||
| tblPr = table._tbl.tblPr | ||
| tblCellMar = OxmlElement('w:tblCellMar') | ||
| for side in sides: | ||
| margin = OxmlElement(f'w:{side}') | ||
| margin.set(qn('w:w'), '0') | ||
| margin.set(qn('w:type'), 'dxa') | ||
| tblCellMar.append(margin) | ||
|
|
||
| # w:tblCellMar must come after w:tblLayout/w:tblBorders/w:shd (none of | ||
| # which this table sets) and before w:tblLook in the CT_TblPr schema | ||
| # sequence; anchor on tblLook, which python-docx always adds. | ||
| tbl_look = tblPr.find(qn('w:tblLook')) | ||
| if tbl_look is not None: | ||
| tbl_look.addprevious(tblCellMar) | ||
| else: | ||
| tblPr.append(tblCellMar) | ||
|
|
||
|
|
||
| def _body_column_count(): | ||
| """Number of columns configured for the body, from PAGE_ATTRIBUTES.""" | ||
| return max(1, pdf_enum.PAGE_ATTRIBUTES.get('default_column_count', 2)) | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Header/Footerjá 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.