Skip to content
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

Refactor number of pages alias (fix #1090) #1203

Merged
merged 14 commits into from
Oct 30, 2024
Merged
73 changes: 48 additions & 25 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from numbers import Number
from os.path import splitext
from pathlib import Path
from typing import Callable, Iterator, NamedTuple, Optional, Union
from typing import Callable, Dict, Iterator, NamedTuple, Optional, Union

try:
from endesive import signer
Expand Down Expand Up @@ -101,7 +101,7 @@ class Image:
preload_image,
)
from .linearization import LinearizedOutputProducer
from .line_break import Fragment, MultiLineBreak, TextLine
from .line_break import Fragment, MultiLineBreak, TextLine, TotalPagesAliasFragment
from .outline import OutlineSection
from .output import (
OutputProducer,
Expand Down Expand Up @@ -279,7 +279,9 @@ def __init__(
but is less compatible with the PDF spec.
"""
self.page = 0 # current page number
self.pages = {} # array of PDFPage objects starting at index 1
self.pages: Dict[int, PDFPage] = (
{}
) # array of PDFPage objects starting at index 1
andersonhc marked this conversation as resolved.
Show resolved Hide resolved
self.fonts = {} # map font string keys to an instance of CoreFont or TTFFont
# map page numbers to a set of font indices:
self.fonts_used_per_page_number = defaultdict(set)
Expand Down Expand Up @@ -3129,6 +3131,8 @@ def _render_styled_text_line(
f"{(self.h - self.y - 0.5 * h - 0.3 * max_font_size) * k:.2f} Td"
)
for i, frag in enumerate(fragments):
if isinstance(frag, TotalPagesAliasFragment):
self.pages[self.page].add_alias(frag)
if frag.graphics_state["text_color"] != last_used_color:
# allow to change color within the line of text.
last_used_color = frag.graphics_state["text_color"]
Expand Down Expand Up @@ -3381,6 +3385,22 @@ def get_fallback_font(self, char, style=""):
def _parse_chars(self, text: str, markdown: bool) -> Iterator[Fragment]:
"Split text into fragments"
if not markdown and not self.is_ttf_font:
if self.str_alias_nb_pages:
for seq, fragment_text in enumerate(
text.split(self.str_alias_nb_pages)
):
if seq > 0:
yield TotalPagesAliasFragment(
self.str_alias_nb_pages,
self._get_current_graphics_state(),
self.k,
)
if fragment_text:
yield Fragment(
fragment_text, self._get_current_graphics_state(), self.k
)
return

yield Fragment(text, self._get_current_graphics_state(), self.k)
return
txt_frag, in_bold, in_italics, in_underline = (
Expand Down Expand Up @@ -3439,6 +3459,23 @@ def frag():
yield frag()
current_text_script = text_script

if self.str_alias_nb_pages:
if text[: len(self.str_alias_nb_pages)] == self.str_alias_nb_pages:
if txt_frag:
yield frag()
gstate = self._get_current_graphics_state()
gstate["font_style"] = ("B" if in_bold else "") + (
"I" if in_italics else ""
)
gstate["underline"] = in_underline
yield TotalPagesAliasFragment(
self.str_alias_nb_pages,
gstate,
self.k,
)
text = text[len(self.str_alias_nb_pages) :]
continue

# Check that previous & next characters are not identical to the marker:
if markdown:
if (
Expand Down Expand Up @@ -4585,26 +4622,6 @@ def sign(
)
self.pages[self.page].annots.append(annotation)

def _substitute_page_number(self):
substituted = False
# Replace number of pages in fonts using subsets (unicode)
alias = self.str_alias_nb_pages.encode("utf-16-be")
encoded_nb = str(self.pages_count).encode("utf-16-be")
for page in self.pages.values():
substituted |= alias in page.contents
page.contents = page.contents.replace(alias, encoded_nb)
# Now repeat for no pages in non-subset fonts
alias = self.str_alias_nb_pages.encode("latin-1")
encoded_nb = str(self.pages_count).encode("latin-1")
for page in self.pages.values():
substituted |= alias in page.contents
page.contents = page.contents.replace(alias, encoded_nb)
if substituted:
LOGGER.debug(
"Substitution of '%s' was performed in the document",
self.str_alias_nb_pages,
)

def _insert_table_of_contents(self):
# Doc has been closed but we want to write to self.pages[self.page] instead of self.buffer:
tocp = self._toc_placeholder
Expand Down Expand Up @@ -5158,8 +5175,14 @@ def output(
# Generating .buffer based on .pages:
if self._toc_placeholder:
self._insert_table_of_contents()
if self.str_alias_nb_pages:
andersonhc marked this conversation as resolved.
Show resolved Hide resolved
self._substitute_page_number()
for page in self.pages.values():
for alias in page.get_aliases():
page.contents = page.contents.replace(
alias.get_alias_string().encode("latin-1"),
alias.render_alias_substitution(str(self.pages_count)).encode(
"latin-1"
),
)
if linearize:
output_producer_class = LinearizedOutputProducer
output_producer = output_producer_class(self)
Expand Down
20 changes: 20 additions & 0 deletions fpdf/line_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from typing import NamedTuple, Any, List, Optional, Union, Sequence
from numbers import Number
from uuid import uuid4

from .enums import Align, CharVPos, TextDirection, WrapMode
from .errors import FPDFException
Expand Down Expand Up @@ -332,6 +333,25 @@ def render_pdf_text_core(self, frag_ws, current_ws):
return ret


class TotalPagesAliasFragment(Fragment):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.uuid = uuid4()

def get_alias_string(self):
return f"::alias:{self.uuid}::"

def render_pdf_text(self, *args, **kwargs):
self._render_args = args
self._render_kwargs = kwargs
andersonhc marked this conversation as resolved.
Show resolved Hide resolved
return self.get_alias_string()

def render_alias_substitution(self, replacement_text: str):
self.characters = list(replacement_text)
return super().render_pdf_text(*self._render_args, **self._render_kwargs)


class TextLine(NamedTuple):
fragments: tuple
text_width: float
Expand Down
10 changes: 10 additions & 0 deletions fpdf/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
from contextlib import contextmanager
from io import BytesIO


from .annotations import PDFAnnotation
from .enums import SignatureFlag
from .errors import FPDFException
from .line_break import TotalPagesAliasFragment
from .image_datastructures import RasterImageInfo
from .outline import build_outline_objs
from .sign import Signature, sign_content
Expand Down Expand Up @@ -243,6 +245,7 @@ class PDFPage(PDFObject):
"_index",
"_width_pt",
"_height_pt",
"_aliases",
)

def __init__(
Expand All @@ -265,6 +268,7 @@ def __init__(
self.parent = None # must always be set before calling .serialize()
self._index = index
self._width_pt, self._height_pt = None, None
self._aliases: list[TotalPagesAliasFragment] = []

def index(self):
return self._index
Expand All @@ -277,6 +281,12 @@ def set_dimensions(self, width_pt, height_pt):
"Accepts a pair (width, height) in the unit specified to FPDF constructor"
self._width_pt, self._height_pt = width_pt, height_pt

def get_aliases(self):
return self._aliases

def add_alias(self, alias):
self._aliases.append(alias)


class PDFPagesRoot(PDFObject):
def __init__(self, count, media_box):
Expand Down
Binary file modified test/alias_nb_pages.pdf
Binary file not shown.
Binary file modified test/outline/toc_with_nb_and_footer.pdf
Binary file not shown.
Loading