From e4a09d89b38b1ebb5d16c1c63d9f799d30d51dcc Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:52:21 +0100 Subject: [PATCH] Supporting tags from SVG files * introduce TextRendererMixin --- fpdf/__init__.py | 2 +- fpdf/drawing.py | 54 +- fpdf/font_type_3.py | 20 +- fpdf/fonts.py | 4 +- fpdf/fpdf.py | 798 +----------------- fpdf/image_parsing.py | 21 +- fpdf/svg.py | 113 ++- fpdf/text_region.py | 2 +- fpdf/text_renderer.py | 791 +++++++++++++++++ test/outline/toc_no_reset_page_indices.pdf | Bin 126372 -> 126126 bytes test/outline/toc_with_extra_page_0.pdf | Bin 67029 -> 67016 bytes test/outline/toc_with_extra_page_1.pdf | Bin 77757 -> 77744 bytes test/outline/toc_with_extra_page_2.pdf | Bin 68032 -> 68019 bytes test/svg/parameters.py | 3 + .../svg_sources/embedded-raster-images.svg | 2 +- test/svg/svg_sources/text-samples.svg | 17 + test/text/clip_text_modes.pdf | Bin 20634 -> 22474 bytes test/text/test_text_mode.py | 4 +- 18 files changed, 990 insertions(+), 841 deletions(-) create mode 100644 fpdf/text_renderer.py create mode 100644 test/svg/svg_sources/text-samples.svg diff --git a/fpdf/__init__.py b/fpdf/__init__.py index b1f2a092d..13bacb918 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -24,12 +24,12 @@ from .fpdf import ( FPDF, TitleStyle, - FPDF_FONT_DIR as _FPDF_FONT_DIR, FPDF_VERSION as _FPDF_VERSION, ) from .html import HTMLMixin, HTML2FPDF from .prefs import ViewerPreferences from .template import Template, FlexTemplate +from .text_renderer import FPDF_FONT_DIR as _FPDF_FONT_DIR from .util import get_scale_factor try: diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 6bee8b7dc..9931bb15c 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -2800,14 +2800,14 @@ def __init__(self): def add_item( self, item: Union["GraphicsContext", "PaintedPath", "PaintComposite"], - _copy: bool = True, + clone: bool = True, ) -> None: """ Append an item to this drawing context Args: item (GraphicsContext, PaintedPath): the item to be appended. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. @@ -2816,7 +2816,7 @@ def add_item( if not isinstance(item, (GraphicsContext, PaintedPath, PaintComposite)): raise TypeError(f"{item} doesn't belong in a DrawingContext") - if _copy: + if clone: item = deepcopy(item) self._subitems.append(item) @@ -3097,24 +3097,24 @@ def transform_group(self, transform): ctxt.transform = transform yield self - def add_path_element(self, item, _copy=True): + def add_path_element(self, item, clone=True): """ Add the given element as a path item of this path. Args: item: the item to add to this path. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if self._starter_move is not None: self._closed = False - self._graphics_context.add_item(self._starter_move, _copy=False) + self._graphics_context.add_item(self._starter_move, clone=False) self._close_context = self._graphics_context self._starter_move = None - self._graphics_context.add_item(item, _copy=_copy) + self._graphics_context.add_item(item, clone=clone) def remove_last_path_element(self): self._graphics_context.remove_last_item() @@ -3144,7 +3144,7 @@ def rectangle(self, x, y, w, h, rx=0, ry=0): self._insert_implicit_close_if_open() self.add_path_element( - RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), _copy=False + RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), clone=False ) self._closed = True self.move_to(x, y) @@ -3179,7 +3179,7 @@ def ellipse(self, cx, cy, rx, ry): The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() - self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), _copy=False) + self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), clone=False) self._closed = True self.move_to(cx, cy) @@ -3223,7 +3223,7 @@ def move_relative(self, x, y): self._insert_implicit_close_if_open() if self._starter_move is not None: self._closed = False - self._graphics_context.add_item(self._starter_move, _copy=False) + self._graphics_context.add_item(self._starter_move, clone=False) self._close_context = self._graphics_context self._starter_move = RelativeMove(Point(x, y)) return self @@ -3239,7 +3239,7 @@ def line_to(self, x, y): Returns: The path, to allow chaining method calls. """ - self.add_path_element(Line(Point(x, y)), _copy=False) + self.add_path_element(Line(Point(x, y)), clone=False) return self def line_relative(self, dx, dy): @@ -3256,7 +3256,7 @@ def line_relative(self, dx, dy): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeLine(Point(dx, dy)), _copy=False) + self.add_path_element(RelativeLine(Point(dx, dy)), clone=False) return self def horizontal_line_to(self, x): @@ -3270,7 +3270,7 @@ def horizontal_line_to(self, x): Returns: The path, to allow chaining method calls. """ - self.add_path_element(HorizontalLine(x), _copy=False) + self.add_path_element(HorizontalLine(x), clone=False) return self def horizontal_line_relative(self, dx): @@ -3286,7 +3286,7 @@ def horizontal_line_relative(self, dx): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeHorizontalLine(dx), _copy=False) + self.add_path_element(RelativeHorizontalLine(dx), clone=False) return self def vertical_line_to(self, y): @@ -3300,7 +3300,7 @@ def vertical_line_to(self, y): Returns: The path, to allow chaining method calls. """ - self.add_path_element(VerticalLine(y), _copy=False) + self.add_path_element(VerticalLine(y), clone=False) return self def vertical_line_relative(self, dy): @@ -3316,7 +3316,7 @@ def vertical_line_relative(self, dy): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeVerticalLine(dy), _copy=False) + self.add_path_element(RelativeVerticalLine(dy), clone=False) return self def curve_to(self, x1, y1, x2, y2, x3, y3): @@ -3338,7 +3338,7 @@ def curve_to(self, x1, y1, x2, y2, x3, y3): ctrl2 = Point(x2, y2) end = Point(x3, y3) - self.add_path_element(BezierCurve(ctrl1, ctrl2, end), _copy=False) + self.add_path_element(BezierCurve(ctrl1, ctrl2, end), clone=False) return self def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): @@ -3372,7 +3372,7 @@ def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): c2d = Point(dx2, dy2) end = Point(dx3, dy3) - self.add_path_element(RelativeBezierCurve(c1d, c2d, end), _copy=False) + self.add_path_element(RelativeBezierCurve(c1d, c2d, end), clone=False) return self def quadratic_curve_to(self, x1, y1, x2, y2): @@ -3390,7 +3390,7 @@ def quadratic_curve_to(self, x1, y1, x2, y2): """ ctrl = Point(x1, y1) end = Point(x2, y2) - self.add_path_element(QuadraticBezierCurve(ctrl, end), _copy=False) + self.add_path_element(QuadraticBezierCurve(ctrl, end), clone=False) return self def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): @@ -3412,7 +3412,7 @@ def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): """ ctrl = Point(dx1, dy1) end = Point(dx2, dy2) - self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), _copy=False) + self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), clone=False) return self def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): @@ -3459,7 +3459,7 @@ def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): end = Point(x, y) self.add_path_element( - Arc(radii, rotation, large_arc, positive_sweep, end), _copy=False + Arc(radii, rotation, large_arc, positive_sweep, end), clone=False ) return self @@ -3508,7 +3508,7 @@ def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy): end = Point(dx, dy) self.add_path_element( - RelativeArc(radii, rotation, large_arc, positive_sweep, end), _copy=False + RelativeArc(radii, rotation, large_arc, positive_sweep, end), clone=False ) return self @@ -3516,13 +3516,13 @@ def close(self): """ Explicitly close the current (sub)path. """ - self.add_path_element(Close(), _copy=False) + self.add_path_element(Close(), clone=False) self._closed = True self.move_relative(0, 0) def _insert_implicit_close_if_open(self): if not self._closed: - self._close_context.add_item(ImplicitClose(), _copy=False) + self._close_context.add_item(ImplicitClose(), clone=False) self._close_context = self._graphics_context self._closed = True @@ -3744,19 +3744,19 @@ def clipping_path(self) -> Optional[ClippingPath]: def clipping_path(self, new_clipath: ClippingPath) -> None: self._clipping_path = new_clipath - def add_item(self, item: Renderable, _copy: bool = True) -> None: + def add_item(self, item: Renderable, clone: bool = True): """ Add a path element to this graphics context. Args: item: the path element to add. May be a primitive element or another `GraphicsContext` or a `PaintedPath`. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ - if _copy: + if clone: item = deepcopy(item) self.path_items.append(item) diff --git a/fpdf/font_type_3.py b/fpdf/font_type_3.py index a8d70ff9d..880262a87 100644 --- a/fpdf/font_type_3.py +++ b/fpdf/font_type_3.py @@ -283,7 +283,7 @@ def draw_glyph_colrv0(self, layers): glyph.draw(pen) path.style.fill_color = self.get_color(layer.colorID) path.style.stroke_color = self.get_color(layer.colorID) - gc.add_item(item=path, _copy=False) + gc.add_item(item=path, clone=False) return gc def draw_glyph_colrv1(self, glyph_name): @@ -318,7 +318,7 @@ def draw_colrv1_paint( parent=group, ctm=ctm, ) - parent.add_item(item=group, _copy=False) + parent.add_item(item=group, clone=False) return parent, target_path if paint.Format in ( @@ -403,8 +403,8 @@ def draw_colrv1_paint( ctm=Transform.identity(), ) if surface_path is not None: - group.add_item(item=surface_path, _copy=False) - parent.add_item(item=group, _copy=False) + group.add_item(item=surface_path, clone=False) + parent.add_item(item=group, clone=False) return parent, None if paint.Format == PaintFormat.PaintColrGlyph: @@ -419,7 +419,7 @@ def draw_colrv1_paint( group = GraphicsContext() self.draw_colrv1_paint(paint=rec.Paint, parent=group, ctm=ctm) - parent.add_item(item=group, _copy=False) + parent.add_item(item=group, clone=False) return parent, target_path if paint.Format in ( @@ -465,7 +465,7 @@ def draw_colrv1_paint( ctm=ctm, ) if backdrop_path is not None: - backdrop_node.add_item(item=backdrop_path, _copy=False) + backdrop_node.add_item(item=backdrop_path, clone=False) source_node = GraphicsContext() _, source_path = self.draw_colrv1_paint( @@ -474,20 +474,20 @@ def draw_colrv1_paint( ctm=ctm, ) if source_path is not None: - source_node.add_item(item=source_path, _copy=False) + source_node.add_item(item=source_path, clone=False) composite_type, composite_mode = self.get_composite_mode( paint.CompositeMode ) if composite_type == "Blend": source_node.style.blend_mode = composite_mode - parent.add_item(item=backdrop_node, _copy=False) - parent.add_item(item=source_node, _copy=False) + parent.add_item(item=backdrop_node, clone=False) + parent.add_item(item=source_node, clone=False) elif composite_type == "Compositing": composite_node = PaintComposite( backdrop=backdrop_node, source=source_node, operation=composite_mode ) - parent.add_item(item=composite_node, _copy=False) + parent.add_item(item=composite_node, clone=False) else: raise ValueError(""" Composite operation not supported """) return parent, None diff --git a/fpdf/fonts.py b/fpdf/fonts.py index ed634d9bb..d703897f4 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -224,8 +224,8 @@ class CoreFont: "emphasis", ) - def __init__(self, fpdf, fontkey, style): - self.i = len(fpdf.fonts) + 1 + def __init__(self, i, fontkey, style): + self.i = i self.type = "core" self.name = CORE_FONTS[fontkey] self.sp = 250 # strikethrough horizontal position diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 2fe0db878..415e655fc 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -21,16 +21,13 @@ from contextlib import contextmanager from datetime import datetime, timezone from functools import wraps -from math import isclose from numbers import Number -from os.path import splitext from pathlib import Path from typing import ( Any, Callable, ContextManager, Dict, - Iterator, NamedTuple, Optional, Union, @@ -61,7 +58,6 @@ class Image: PDFAnnotation, PDFEmbeddedFile, ) -from .bidi import BidiParagraph, auto_detect_base_direction from .deprecation import ( WarnOnDeprecatedModuleAttributes, get_stack_level, @@ -93,16 +89,14 @@ class Image: PathPaintRule, PDFResourceType, RenderStyle, - TextDirection, - TextEmphasis, TextMarkupType, TextMode, WrapMode, XPos, YPos, ) -from .errors import FPDFException, FPDFPageFormatException, FPDFUnicodeEncodingException -from .fonts import CORE_FONTS, CoreFont, FontFace, TextStyle, TitleStyle, TTFFont +from .errors import FPDFException, FPDFPageFormatException +from .fonts import TextStyle, TitleStyle from .graphics_state import GraphicsStateMixin from .html import HTML2FPDF from .image_datastructures import ( @@ -118,7 +112,6 @@ class Image: preload_image, ) from .line_break import ( - Fragment, MultiLineBreak, TextLine, TotalPagesSubstitutionFragment, @@ -141,9 +134,10 @@ class Image: from .svg import Percent, SVGObject from .syntax import DestinationXYZ, PDFArray, PDFDate from .table import Table, draw_box_borders +from .text_renderer import TextRendererMixin from .text_region import TextColumns, TextRegionMixin from .transitions import Transition -from .unicode_script import UnicodeScript, get_unicode_script +from .unicode_script import get_unicode_script from .util import Padding, get_scale_factor # Public global variables: @@ -159,8 +153,6 @@ class Image: # Private global variables: LOGGER = logging.getLogger(__name__) -HERE = Path(__file__).resolve().parent -FPDF_FONT_DIR = HERE / "font" LAYOUT_ALIASES = { "default": None, "single": PageLayout.SINGLE_PAGE, @@ -228,7 +220,7 @@ def wrapper(self, *args, **kwargs): return wrapper -class FPDF(GraphicsStateMixin, TextRegionMixin): +class FPDF(GraphicsStateMixin, TextRendererMixin, TextRegionMixin): "PDF Generation class" MARKDOWN_BOLD_MARKER = "**" @@ -286,8 +278,6 @@ def __init__( """ # array of PDFPage objects starting at index 1: self.pages: Dict[int, PDFPage] = {} - self.fonts = {} # map font string keys to an instance of CoreFont or TTFFont - # map page numbers to a set of font indices: self.links = {} # array of Destination objects starting at index 1 self.embedded_files = [] # array of PDFEmbeddedFile self.image_cache = ImageCache() @@ -322,32 +312,11 @@ def __init__( self.title = None self.section_title_styles = {} # level -> TextStyle - self.core_fonts_encoding = "latin-1" - "Font encoding, Latin-1 by default" - # Replace these fonts with these core fonts - self.font_aliases = { - "arial": "helvetica", - "couriernew": "courier", - "timesnewroman": "times", - } # Scale factor self.k = get_scale_factor(unit) - # Graphics state variables defined as properties by GraphicsStateMixin. - # We set their default values here. - self.font_family = "" # current font family - # current font style (BOLD/ITALICS - does not handle UNDERLINE nor STRIKETHROUGH): - self.font_style = "" - self.underline = False - self.strikethrough = False - self.font_size_pt = 12 # current font size in points - self.font_stretching = 100 # current font stretching - self.char_spacing = 0 # current character spacing - self.current_font = None # None or an instance of CoreFont or TTFFont - self.current_font_is_set_on_page = False # current font and size are already added to current page contents with _out self.draw_color = self.DEFAULT_DRAW_COLOR self.fill_color = self.DEFAULT_FILL_COLOR - self.text_color = self.DEFAULT_TEXT_COLOR self.page_background = None self.dash_pattern = dict(dash=0, gap=0, phase=0) self.line_width = 0.567 / self.k # line width (0.2 mm) @@ -373,12 +342,8 @@ def __init__( self.pdf_version = "1.3" # Set default PDF version No. self.creation_date = datetime.now(timezone.utc) self._security_handler = None - self._fallback_font_ids = [] - self._fallback_font_exact_match = False - self.render_color_fonts = True self._current_draw_context = None - # map page numbers to a set of GraphicsState names: self._record_text_quad_points = False self._resource_catalog = ResourceCatalog() @@ -454,20 +419,6 @@ def write_html(self, text, *args, **kwargs): def _set_min_pdf_version(self, version): self.pdf_version = max(self.pdf_version, version) - @property - def emphasis(self) -> TextEmphasis: - "The current text emphasis: bold, italics, underline and/or strikethrough." - font_style = self.font_style - if self.strikethrough: - font_style += "S" - if self.underline: - font_style += "U" - return TextEmphasis.coerce(font_style) - - @property - def is_ttf_font(self) -> bool: - return self.current_font and self.current_font.type == "TTF" - @property def page_mode(self) -> PageMode: return self._page_mode @@ -672,85 +623,6 @@ def set_display_mode(self, zoom, layout="continuous"): raise FPDFException(f"Incorrect zoom display mode: {zoom}") self.page_layout = LAYOUT_ALIASES.get(layout, layout) - def set_text_shaping( - self, - use_shaping_engine: bool = True, - features: dict = None, - direction: Union[str, TextDirection] = None, - script: str = None, - language: str = None, - ): - """ - Enable or disable text shaping engine when rendering text. - If features, direction, script or language are not specified the shaping engine will try - to guess the values based on the input text. - - Args: - use_shaping_engine: enable or disable the use of the shaping engine to process the text - features: a dictionary containing 4 digit OpenType features and whether each feature - should be enabled or disabled - example: features={"kern": False, "liga": False} - direction: the direction the text should be rendered, either "ltr" (left to right) - or "rtl" (right to left). - script: a valid OpenType script tag like "arab" or "latn" - language: a valid OpenType language tag like "eng" or "fra" - """ - if not use_shaping_engine: - self.text_shaping = None - return - - try: - # pylint: disable=import-outside-toplevel, unused-import - import uharfbuzz - except ImportError as exc: - raise FPDFException( - "The uharfbuzz package could not be imported, but is required for text shaping. Try: pip install uharfbuzz" - ) from exc - - # - # Features must be a dictionary containing opentype features and a boolean flag - # stating whether the feature should be enabled or disabled. - # - # e.g. features={"liga": True, "kern": False} - # - # https://harfbuzz.github.io/shaping-opentype-features.html - # - - if features and not isinstance(features, dict): - raise FPDFException( - "Features must be a dictionary. See text shaping documentation" - ) - if not features: - features = {} - - # Buffer properties (direction, script and language) - # if the properties are not provided, Harfbuzz "guessing" logic is used. - # https://harfbuzz.github.io/setting-buffer-properties.html - # Valid harfbuzz directions are ltr (left to right), rtl (right to left), - # ttb (top to bottom) or btt (bottom to top) - - text_direction = None - if direction: - text_direction = ( - direction - if isinstance(direction, TextDirection) - else TextDirection.coerce(direction) - ) - if text_direction not in [TextDirection.LTR, TextDirection.RTL]: - raise FPDFException( - "FPDF2 only accept ltr (left to right) or rtl (right to left) directions for now." - ) - - self.text_shaping = { - "use_shaping_engine": True, - "features": features, - "direction": text_direction, - "script": script, - "language": language, - "fragment_direction": None, - "paragraph_direction": None, - } - @property def page_layout(self): return self._page_layout @@ -866,30 +738,6 @@ def set_xmp_metadata(self, xmp_metadata): if xmp_metadata: self._set_min_pdf_version("1.4") - def set_doc_option(self, opt, value): - """ - Defines a document option. - - Args: - opt (str): name of the option to set - value (str) option value - - .. deprecated:: 2.4.0 - Simply set the `FPDF.core_fonts_encoding` property as a replacement. - """ - warnings.warn( - ( - "set_doc_option() is deprecated since v2.4.0 " - "and will be removed in a future release. " - "Simply set the `.core_fonts_encoding` property as a replacement." - ), - DeprecationWarning, - stacklevel=get_stack_level(), - ) - if opt != "core_fonts_encoding": - raise FPDFException(f'Unknown document option "{opt}"') - self.core_fonts_encoding = value - def set_image_filter(self, image_filter): """ Args: @@ -1020,8 +868,6 @@ def add_page( "A page cannot be added on a closed document, after calling output()" ) - self.current_font_is_set_on_page = False - family = self.font_family emphasis = self.emphasis size = self.font_size_pt @@ -1155,9 +1001,7 @@ def _beginpage( page = self.pages[self.page] self.x = self.l_margin self.y = self.t_margin - self.font_family = "" - self.font_stretching = 100 - self.char_spacing = 0 + self.set_new_page_font_settings() if same: if orientation or format: raise ValueError( @@ -1246,41 +1090,6 @@ def set_fill_color(self, r, g=-1, b=-1): if self.page > 0: self._out(self.fill_color.serialize().lower()) - def set_text_color(self, r, g=-1, b=-1): - """ - Defines the color used for text. - Accepts either a single greyscale value, 3 values as RGB components, a single `#abc` or `#abcdef` hexadecimal color string, - or an instance of `fpdf.drawing.DeviceCMYK`, `fpdf.drawing.DeviceRGB` or `fpdf.drawing.DeviceGray`. - The method can be called before the first page is created and the value is retained from page to page. - - Args: - r (int, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): if `g` and `b` are given, this indicates the red component. - Else, this indicates the grey level. The value must be between 0 and 255. - g (int): green component (between 0 and 255) - b (int): blue component (between 0 and 255) - """ - self.text_color = convert_to_device_color(r, g, b) - - def get_string_width(self, s, normalized=False, markdown=False): - """ - Returns the length of a string in user unit. A font must be selected. - The value is calculated with stretching and spacing. - - Note that the width of a cell has some extra padding added to this width, - on the left & right sides, equal to the .c_margin property. - - Args: - s (str): the string whose length is to be computed. - normalized (bool): whether normalization needs to be performed on the input string. - markdown (bool): indicates if basic markdown support is enabled - """ - # normalized is parameter for internal use - s = s if normalized else self.normalize_text(s) - w = 0 - for frag in self._preload_bidirectional_text(s, markdown): - w += frag.get_width() - return w - def set_line_width(self, width): """ Defines the line width of all stroking operations (lines, rectangles and cell borders). @@ -1362,8 +1171,6 @@ def drawing_context(self, debug_stream=None): # Let the catalog scan & register resources used by this drawing: self._resource_catalog.index_stream_resources(rendered, self.page) - # Once we handle text-rendering SVG tags (cf. PR #1029), - # we should also detect fonts used and add them to the resource catalog self._out(rendered) # The drawing API makes use of features (notably transparency and blending modes) that were introduced in PDF 1.4: @@ -2155,247 +1962,6 @@ def bezier(self, point_list, closed=False, style=None): ctxt.add_item(path) - def add_font(self, family=None, style="", fname=None, uni="DEPRECATED"): - """ - Imports a TrueType or OpenType font and makes it available - for later calls to the `FPDF.set_font()` method. - - You will find more information on the "Unicode" documentation page. - - Args: - family (str): optional name of the font family. Used as a reference for `FPDF.set_font()`. - If not provided, use the base name of the `fname` font path, without extension. - style (str): font style. "" for regular, include 'B' for bold, and/or 'I' for italic. - fname (str): font file name. You can specify a relative or full path. - If the file is not found, it will be searched in `FPDF_FONT_DIR`. - uni (bool): [**DEPRECATED since 2.5.1**] unused - """ - if not fname: - raise ValueError('"fname" parameter is required') - - ext = splitext(str(fname))[1].lower() - if ext not in (".otf", ".otc", ".ttf", ".ttc"): - raise ValueError( - f"Unsupported font file extension: {ext}." - " add_font() used to accept .pkl file as input, but for security reasons" - " this feature is deprecated since v2.5.1 and has been removed in v2.5.3." - ) - - if uni != "DEPRECATED": - warnings.warn( - ( - '"uni" parameter is deprecated since v2.5.1, ' - "unused and will soon be removed" - ), - DeprecationWarning, - stacklevel=get_stack_level(), - ) - - style = "".join(sorted(style.upper())) - if any(letter not in "BI" for letter in style): - raise ValueError( - f"Unknown style provided (only B & I letters are allowed): {style}" - ) - - for parent in (".", FPDF_FONT_DIR): - if not parent: - continue - - if (Path(parent) / fname).exists(): - font_file_path = Path(parent) / fname - break - else: - raise FileNotFoundError(f"TTF Font file not found: {fname}") - - if family is None: - family = font_file_path.stem - - fontkey = f"{family.lower()}{style}" - # Check if font already added or one of the core fonts - if fontkey in self.fonts or fontkey in CORE_FONTS: - warnings.warn( - f"Core font or font already added '{fontkey}': doing nothing", - stacklevel=get_stack_level(), - ) - return - - self.fonts[fontkey] = TTFFont(self, font_file_path, fontkey, style) - - def set_font(self, family=None, style: Union[str, TextEmphasis] = "", size=0): - """ - Sets the font used to print character strings. - It is mandatory to call this method at least once before printing text. - - Default encoding is not specified, but all text writing methods accept only - unicode for external fonts and one byte encoding for standard. - - Standard fonts use `Latin-1` encoding by default, but Windows - encoding `cp1252` (Western Europe) can be used with - `self.core_fonts_encoding = encoding`. - - The font specified is retained from page to page. - The method can be called before the first page is created. - - Args: - family (str): name of a font added with `FPDF.add_font`, - or name of one of the 14 standard "PostScript" fonts: - Courier (fixed-width), Helvetica (sans serif), Times (serif), - Symbol (symbolic) or ZapfDingbats (symbolic) - If an empty string is provided, the current family is retained. - style (str, fpdf.enums.TextEmphasis): empty string (by default) or a combination - of one or several letters among B (bold), I (italic), S (strikethrough) and U (underline). - Bold and italic styles do not apply to Symbol and ZapfDingbats fonts. - size (float): in points. The default value is the current size. - """ - if not family: - family = self.font_family - - family = family.lower() - if isinstance(style, TextEmphasis): - style = style.style - style = "".join(sorted(style.upper())) - if any(letter not in "BISU" for letter in style): - raise ValueError( - f"Unknown style provided (only B/I/S/U letters are allowed): {style}" - ) - if "U" in style: - self.underline = True - style = style.replace("U", "") - else: - self.underline = False - if "S" in style: - self.strikethrough = True - style = style.replace("S", "") - else: - self.strikethrough = False - - if family in self.font_aliases and family + style not in self.fonts: - warnings.warn( - f"Substituting font {family} by core font {self.font_aliases[family]}" - " - This is deprecated since v2.7.8, and will soon be removed", - DeprecationWarning, - stacklevel=get_stack_level(), - ) - family = self.font_aliases[family] - elif family in ("symbol", "zapfdingbats") and style: - warnings.warn( - f"Built-in font {family} only has a single 'style' " - "and can't be bold or italic", - stacklevel=get_stack_level(), - ) - style = "" - - if not size: - size = self.font_size_pt - - # Test if font is already selected - if ( - self.font_family == family - and self.font_style == style - and isclose(self.font_size_pt, size) - ): - return - - # Test if used for the first time - fontkey = family + style - if fontkey not in self.fonts: - if fontkey not in CORE_FONTS: - raise FPDFException( - f"Undefined font: {fontkey} - " - f"Use built-in fonts or FPDF.add_font() beforehand" - ) - # If it's one of the core fonts, add it to self.fonts - self.fonts[fontkey] = CoreFont(self, fontkey, style) - - # Select it - self.font_family = family - self.font_style = style - self.font_size_pt = size - self.current_font = self.fonts[fontkey] - self.current_font_is_set_on_page = False - - def set_font_size(self, size): - """ - Configure the font size in points - - Args: - size (float): font size in points - """ - if isclose(self.font_size_pt, size): - return - self.font_size_pt = size - self.current_font_is_set_on_page = False - - def _set_font_for_page(self, font, font_size_pt, wrap_in_text_object=True): - """ - Set font and size for current page. - This step is needed before adding text into page and not needed in set_font and set_font_size. - """ - sl = f"/F{font.i} {font_size_pt:.2f} Tf" - if wrap_in_text_object: - sl = f"BT {sl} ET" - self._resource_catalog.add(PDFResourceType.FONT, font.i, self.page) - self.current_font_is_set_on_page = True - return sl - - def set_char_spacing(self, spacing): - """ - Sets horizontal character spacing. - A positive value increases the space between characters, a negative value - reduces it (which may result in glyph overlap). - By default, no spacing is set (which is equivalent to a value of 0). - - Args: - spacing (float): horizontal spacing in document units - """ - if self.char_spacing == spacing: - return - self.char_spacing = spacing - if self.page > 0: - self._out(f"BT {spacing:.2f} Tc ET") - - def set_stretching(self, stretching): - """ - Sets horizontal font stretching. - By default, no stretching is set (which is equivalent to a value of 100). - - Args: - stretching (float): horizontal stretching (scaling) in percents. - """ - if self.font_stretching == stretching: - return - self.font_stretching = stretching - if self.page > 0: - self._out(f"BT {stretching:.2f} Tz ET") - - def set_fallback_fonts(self, fallback_fonts, exact_match=True): - """ - Allows you to specify a list of fonts to be used if any character is not available on the font currently set. - Detailed documentation: https://py-pdf.github.io/fpdf2/Unicode.html#fallback-fonts - - Args: - fallback_fonts: sequence of fallback font IDs - exact_match (bool): when a glyph cannot be rendered uing the current font, - fpdf2 will look for a fallback font matching the current character emphasis (bold/italics). - If it does not find such matching font, and `exact_match` is True, no fallback font will be used. - If it does not find such matching font, and `exact_match` is False, a fallback font will still be used. - To get even more control over this logic, you can also override `FPDF.get_fallback_font()` - """ - fallback_font_ids = [] - for fallback_font in fallback_fonts: - found = False - for fontkey in self.fonts: - # will add all font styles on the same family - if fontkey.replace("B", "").replace("I", "") == fallback_font.lower(): - fallback_font_ids.append(fontkey) - found = True - if not found: - raise FPDFException( - f"Undefined fallback font: {fallback_font} - Use FPDF.add_font() beforehand" - ) - self._fallback_font_ids = tuple(fallback_font_ids) - self._fallback_font_exact_match = exact_match - def add_link(self, y=0, x=0, page=-1, zoom="null"): """ Creates a new internal link and returns its identifier. @@ -3329,6 +2895,8 @@ def cell( prevent_font_change=markdown, ) + # pylint: disable=fixme + # TODO: extract part of this in TextRendererMixin, as well as _do_underline & _do_strikethrough def _render_styled_text_line( self, text_line: TextLine, @@ -3690,264 +3258,6 @@ def _add_quad_points(self, x, y, w, h): ] ) - def _preload_bidirectional_text(self, text, markdown): - """ " - Break the text into bidirectional segments and preload font styles for each fragment - """ - if not self.text_shaping: - return self._preload_font_styles(text, markdown) - paragraph_direction = ( - self.text_shaping["direction"] - if self.text_shaping["direction"] - else auto_detect_base_direction(text) - ) - - paragraph = BidiParagraph(text=text, base_direction=paragraph_direction) - directional_segments = paragraph.get_bidi_fragments() - self.text_shaping["paragraph_direction"] = paragraph.base_direction - - fragments = [] - for bidi_text, bidi_direction in directional_segments: - self.text_shaping["fragment_direction"] = bidi_direction - fragments += self._preload_font_styles(bidi_text, markdown) - return tuple(fragments) - - def _preload_font_styles(self, text, markdown): - """ - When Markdown styling is enabled, we require secondary fonts - to ender text in bold & italics. - This function ensure that those fonts are available. - It needs to perform Markdown parsing, - so we return the resulting `styled_txt_frags` tuple - to avoid repeating this processing later on. - """ - if not text: - return tuple() - prev_font_style = self.font_style - if self.underline: - prev_font_style += "U" - if self.strikethrough: - prev_font_style += "S" - styled_txt_frags = tuple(self._parse_chars(text, markdown)) - if markdown: - page = self.page - # We set the current to page to zero so that - # set_font() does not produce any text object on the stream buffer: - self.page = 0 - if any(frag.font_style == "B" for frag in styled_txt_frags): - # Ensuring bold font is supported: - self.set_font(style="B") - if any(frag.font_style == "I" for frag in styled_txt_frags): - # Ensuring italics font is supported: - self.set_font(style="I") - if any(frag.font_style == "BI" for frag in styled_txt_frags): - # Ensuring bold italics font is supported: - self.set_font(style="BI") - if any(frag.font_style == "" for frag in styled_txt_frags): - # Ensuring base font is supported: - self.set_font(style="") - for frag in styled_txt_frags: - frag.font = self.fonts[frag.font_family + frag.font_style] - # Restoring initial style: - self.set_font(style=prev_font_style) - self.page = page - return styled_txt_frags - - def get_fallback_font(self, char, style=""): - """ - Returns which fallback font has the requested glyph. - This method can be overridden to provide more control than the `select_mode` parameter - of `FPDF.set_fallback_fonts()` provides. - """ - emphasis = TextEmphasis.coerce(style) - fonts_with_char = [ - font_id - for font_id in self._fallback_font_ids - if ord(char) in self.fonts[font_id].cmap - ] - if not fonts_with_char: - return None - font_with_matching_emphasis = next( - (font for font in fonts_with_char if self.fonts[font].emphasis == emphasis), - None, - ) - if font_with_matching_emphasis: - return font_with_matching_emphasis - if self._fallback_font_exact_match: - return None - return fonts_with_char[0] - - def _parse_chars(self, text: str, markdown: bool) -> Iterator[Fragment]: - "Split text into fragments" - if not markdown and not self.text_shaping and not self._fallback_font_ids: - if self.str_alias_nb_pages: - for seq, fragment_text in enumerate( - text.split(self.str_alias_nb_pages) - ): - if seq > 0: - yield TotalPagesSubstitutionFragment( - 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_strikethrough, in_underline = ( - [], - "B" in self.font_style, - "I" in self.font_style, - bool(self.strikethrough), - bool(self.underline), - ) - current_fallback_font = None - current_text_script = None - - def frag(): - nonlocal txt_frag, current_fallback_font, current_text_script - gstate = self._get_current_graphics_state() - gstate["font_style"] = ("B" if in_bold else "") + ( - "I" if in_italics else "" - ) - gstate["strikethrough"] = in_strikethrough - gstate["underline"] = in_underline - if current_fallback_font: - style = "".join(c for c in current_fallback_font if c in ("BI")) - family = current_fallback_font.replace("B", "").replace("I", "") - gstate["font_family"] = family - gstate["font_style"] = style - gstate["current_font"] = self.fonts[current_fallback_font] - current_fallback_font = None - current_text_script = None - fragment = Fragment( - txt_frag, - gstate, - self.k, - ) - txt_frag = [] - return fragment - - if self.is_ttf_font: - font_glyphs = self.current_font.cmap - else: - font_glyphs = [] - num_escape_chars = 0 - - while text: - is_marker = text[:2] in ( - self.MARKDOWN_BOLD_MARKER, - self.MARKDOWN_ITALICS_MARKER, - self.MARKDOWN_STRIKETHROUGH_MARKER, - self.MARKDOWN_UNDERLINE_MARKER, - ) - half_marker = text[0] - text_script = get_unicode_script(text[0]) - if text_script not in ( - UnicodeScript.COMMON, - UnicodeScript.UNKNOWN, - current_text_script, - ): - if txt_frag and current_text_script: - 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["strikethrough"] = in_strikethrough - gstate["underline"] = in_underline - yield TotalPagesSubstitutionFragment( - 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 ( - is_marker - and (not txt_frag or txt_frag[-1] != half_marker) - and (len(text) < 3 or text[2] != half_marker) - ): - txt_frag = ( - txt_frag[: -((num_escape_chars + 1) // 2)] - if num_escape_chars > 0 - else txt_frag - ) - if num_escape_chars % 2 == 0: - if txt_frag: - yield frag() - if text[:2] == self.MARKDOWN_BOLD_MARKER: - in_bold = not in_bold - if text[:2] == self.MARKDOWN_ITALICS_MARKER: - in_italics = not in_italics - if text[:2] == self.MARKDOWN_STRIKETHROUGH_MARKER: - in_strikethrough = not in_strikethrough - if text[:2] == self.MARKDOWN_UNDERLINE_MARKER: - in_underline = not in_underline - text = text[2:] - continue - num_escape_chars = ( - num_escape_chars + 1 - if text[0] == self.MARKDOWN_ESCAPE_CHARACTER - else 0 - ) - is_link = self.MARKDOWN_LINK_REGEX.match(text) - if is_link: - link_text, link_dest, text = is_link.groups() - if txt_frag: - yield frag() - gstate = self._get_current_graphics_state() - gstate["underline"] = self.MARKDOWN_LINK_UNDERLINE - if self.MARKDOWN_LINK_COLOR: - gstate["text_color"] = self.MARKDOWN_LINK_COLOR - try: - page = int(link_dest) - link_dest = self.add_link(page=page) - except ValueError: - pass - yield Fragment( - list(link_text), - gstate, - self.k, - link=link_dest, - ) - continue - if self.is_ttf_font and text[0] != "\n" and not ord(text[0]) in font_glyphs: - style = ("B" if in_bold else "") + ("I" if in_italics else "") - fallback_font = self.get_fallback_font(text[0], style) - if fallback_font: - if fallback_font == current_fallback_font: - txt_frag.append(text[0]) - text = text[1:] - continue - if txt_frag: - yield frag() - current_fallback_font = fallback_font - txt_frag.append(text[0]) - text = text[1:] - continue - if current_fallback_font: - if txt_frag: - yield frag() - current_fallback_font = None - txt_frag.append(text[0]) - text = text[1:] - if txt_frag: - yield frag() - def will_page_break(self, height): """ Let you know if adding an element will trigger a page break, @@ -4588,7 +3898,9 @@ def image( stacklevel=get_stack_level(), ) - name, img, info = preload_image(self.image_cache, name, dims) + name, img, info = preload_image( + name, image_cache=self.image_cache, dims=dims, font_mgr=self + ) if isinstance(info, VectorImageInfo): return self._vector_image( name, img, info, x, y, w, h, link, title, alt_text, keep_aspect_ratio @@ -4638,7 +3950,7 @@ def _raster_image( x = self.x if not isinstance(x, Number): - x = self.x_by_align(x, w, h, info, keep_aspect_ratio) + x = self._x_by_align(x, w, h, info, keep_aspect_ratio) if keep_aspect_ratio: x, y, w, h = info.scale_inside_box(x, y, w, h) if self.oversized_images and info["usages"] == 1 and not dims: @@ -4659,7 +3971,7 @@ def _raster_image( self._resource_catalog.add(PDFResourceType.X_OBJECT, info["i"], self.page) return RasterImageInfo(**info, rendered_width=w, rendered_height=h) - def x_by_align(self, x, w, h, img_info, keep_aspect_ratio): + def _x_by_align(self, x, w, h, img_info, keep_aspect_ratio): if keep_aspect_ratio: _, _, w, h = img_info.scale_inside_box(0, 0, w, h) x = Align.coerce(x) @@ -4735,7 +4047,7 @@ def _vector_image( if keep_aspect_ratio: x, y, w, h = info.scale_inside_box(x, y, w, h) if not isinstance(x, Number): - x = self.x_by_align(x, w, h, info, keep_aspect_ratio) + x = self._x_by_align(x, w, h, info, keep_aspect_ratio) _, _, path = svg.transform_to_rect_viewport( scale=1, width=w, height=h, ignore_svg_top_attrs=True @@ -4855,7 +4167,9 @@ def preload_image(self, name, dims=None): DeprecationWarning, stacklevel=get_stack_level(), ) - return preload_image(self.image_cache, name, dims) + return preload_image( + name, image_cache=self.image_cache, dims=dims, font_mgr=self + ) def preload_glyph_image(self, glyph_image_bytes): return preload_image( @@ -4957,21 +4271,6 @@ def set_xy(self, x, y): self.set_y(y) self.set_x(x) - def normalize_text(self, text): - """Check that text input is in the correct format/encoding""" - # - for TTF unicode fonts: unicode object (utf8 encoding) - # - for built-in fonts: string instances (encoding: latin-1, cp1252) - if not self.is_ttf_font and self.core_fonts_encoding: - try: - return text.encode(self.core_fonts_encoding).decode("latin-1") - except UnicodeEncodeError as error: - raise FPDFUnicodeEncodingException( - text_index=error.start, - character=text[error.start], - font_name=self.font_family + self.font_style, - ) from error - return text - def sign_pkcs12( self, pkcs_filepath, @@ -5588,69 +4887,6 @@ def start_section(self, name, level=0, strict=True): OutlineSection(name, level, self.page, dest, outline_struct_elem) ) - @contextmanager - def use_text_style(self, text_style: TextStyle): - prev_l_margin = None - if text_style: - if text_style.t_margin: - self.ln(text_style.t_margin) - if text_style.l_margin: - if isinstance(text_style.l_margin, (float, int)): - prev_l_margin = self.l_margin - self.l_margin = text_style.l_margin - self.x = self.l_margin - else: - LOGGER.debug( - "Unsupported '%s' value provided as l_margin to .use_text_style()", - text_style.l_margin, - ) - with self.use_font_face(text_style): - yield - if text_style and text_style.b_margin: - self.ln(text_style.b_margin) - if prev_l_margin is not None: - self.l_margin = prev_l_margin - self.x = self.l_margin - - @contextmanager - def use_font_face(self, font_face: FontFace): - """ - Sets the provided `fpdf.fonts.FontFace` in a local context, - then restore font settings back to they were initially. - This method must be used as a context manager using `with`: - - with pdf.use_font_face(FontFace(emphasis="BOLD", color=255, size_pt=42)): - put_some_text() - - Known limitation: in case of a page jump in this local context, - the temporary style may "leak" in the header() & footer(). - """ - if not font_face: - yield - return - prev_font = (self.font_family, self.font_style, self.font_size_pt) - self.set_font( - font_face.family or self.font_family, - ( - font_face.emphasis.style - if font_face.emphasis is not None - else self.font_style - ), - font_face.size_pt or self.font_size_pt, - ) - self.current_font_is_set_on_page = False - prev_text_color = self.text_color - if font_face.color is not None and font_face.color != self.text_color: - self.set_text_color(font_face.color) - prev_fill_color = self.fill_color - if font_face.fill_color is not None: - self.set_fill_color(font_face.fill_color) - yield - if font_face.fill_color is not None: - self.set_fill_color(prev_fill_color) - self.text_color = prev_text_color - self.set_font(*prev_font) - @check_page @contextmanager def table(self, *args: Any, **kwargs: Any) -> ContextManager[Table]: diff --git a/fpdf/image_parsing.py b/fpdf/image_parsing.py index 7541c97ab..5b4dce19a 100644 --- a/fpdf/image_parsing.py +++ b/fpdf/image_parsing.py @@ -28,6 +28,7 @@ from .errors import FPDFException from .image_datastructures import ImageCache, RasterImageInfo, VectorImageInfo from .svg import SVGObject +from .text_renderer import TextRendererMixin @dataclass @@ -77,7 +78,9 @@ class ImageSettings: LZW_MAX_BITS_PER_CODE = 12 # Maximum code bit width -def preload_image(image_cache: ImageCache, name, dims=None): +def preload_image( + name, image_cache: ImageCache, dims=None, font_mgr: TextRendererMixin = None +): """ Read an image and load it into memory. @@ -99,13 +102,19 @@ def preload_image(image_cache: ImageCache, name, dims=None): # Identify and load SVG data: if str(name).endswith(".svg"): try: - return get_svg_info(name, load_image(str(name)), image_cache=image_cache) + return get_svg_info( + name, load_image(str(name)), font_mgr=font_mgr, image_cache=image_cache + ) except Exception as error: raise ValueError(f"Could not parse file: {name}") from error if isinstance(name, bytes) and _is_svg(name.strip()): - return get_svg_info(name, io.BytesIO(name), image_cache=image_cache) + return get_svg_info( + name, io.BytesIO(name), font_mgr=font_mgr, image_cache=image_cache + ) if isinstance(name, io.BytesIO) and _is_svg(name.getvalue().strip()): - return get_svg_info("vector_image", name, image_cache=image_cache) + return get_svg_info( + "vector_image", name, font_mgr=font_mgr, image_cache=image_cache + ) # Load raster data. if isinstance(name, str): @@ -203,8 +212,8 @@ def is_iccp_valid(iccp, filename): return True -def get_svg_info(filename, img, image_cache): - svg = SVGObject(img.getvalue(), image_cache=image_cache) +def get_svg_info(filename, img, font_mgr: TextRendererMixin, image_cache: ImageCache): + svg = SVGObject(img.getvalue(), font_mgr=font_mgr, image_cache=image_cache) if svg.viewbox: _, _, w, h = svg.viewbox else: diff --git a/fpdf/svg.py b/fpdf/svg.py index f2836fdfb..bc0a138a9 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -14,8 +14,6 @@ from fontTools.svgLib.path import parse_path -from .enums import PathPaintRule - try: from defusedxml.ElementTree import fromstring as parse_xml_str except ImportError: @@ -35,8 +33,10 @@ PathPen, ClippingPath, ) +from .enums import PathPaintRule from .image_datastructures import ImageCache, VectorImageInfo from .output import stream_content_for_raster_image +from .text_renderer import TextRendererMixin LOGGER = logging.getLogger(__name__) @@ -582,7 +582,13 @@ def from_file(cls, filename, *args, encoding="utf-8", **kwargs): with open(filename, "r", encoding=encoding) as svgfile: return cls(svgfile.read(), *args, **kwargs) - def __init__(self, svg_text, image_cache: ImageCache = None): + def __init__( + self, + svg_text, + font_mgr: TextRendererMixin = None, + image_cache: ImageCache = None, + ): + self.font_mgr = font_mgr # Needed to render text self.image_cache = image_cache # Needed to render images self.cross_references = {} @@ -772,6 +778,7 @@ def draw_to_page(self, pdf, x=None, y=None, debug_stream=None): debug_stream (io.TextIO): the stream to which rendering debug info will be written. """ + self.font_mgr = pdf # Needed to render text self.image_cache = pdf.image_cache # Needed to render images _, _, path = self.transform_to_page_viewport(pdf) @@ -805,6 +812,10 @@ def handle_defs(self, defs): self.build_path(child) elif child.tag in xmlns_lookup("svg", "image"): self.build_image(child) + # pylint: disable=fixme + # TODO: enable this + # elif child.tag in xmlns_lookup("svg", "text"): + # self.build_text(child) elif child.tag in shape_tags: self.build_shape(child) elif child.tag in xmlns_lookup("svg", "clipPath"): @@ -870,22 +881,26 @@ def build_group(self, group, pdf_group=None): if child.tag in xmlns_lookup("svg", "defs"): self.handle_defs(child) elif child.tag in xmlns_lookup("svg", "g"): - pdf_group.add_item(self.build_group(child), False) + pdf_group.add_item(self.build_group(child), clone=False) elif child.tag in xmlns_lookup("svg", "a"): # tags aren't supported but we need to recurse into them to # render nested elements. LOGGER.warning( "Ignoring unsupported SVG tag: (contributions are welcome to add support for it)", ) - pdf_group.add_item(self.build_group(child), False) + pdf_group.add_item(self.build_group(child), clone=False) elif child.tag in xmlns_lookup("svg", "path"): - pdf_group.add_item(self.build_path(child), False) + pdf_group.add_item(self.build_path(child), clone=False) elif child.tag in shape_tags: - pdf_group.add_item(self.build_shape(child), False) + pdf_group.add_item(self.build_shape(child), clone=False) elif child.tag in xmlns_lookup("svg", "use"): - pdf_group.add_item(self.build_xref(child), False) + pdf_group.add_item(self.build_xref(child), clone=False) elif child.tag in xmlns_lookup("svg", "image"): - pdf_group.add_item(self.build_image(child), False) + pdf_group.add_item(self.build_image(child), clone=False) + # pylint: disable=fixme + # TODO: enable this + # elif child.tag in xmlns_lookup("svg", "text"): + # pdf_group.add_item(self.build_text(child), clone=False) else: LOGGER.warning( "Ignoring unsupported SVG tag: <%s> (contributions are welcome to add support for it)", @@ -944,6 +959,43 @@ def apply_clipping_path(self, stylable, svg_element): clipping_path_id = re.search(r"url\((\#\w+)\)", clipping_path) stylable.clipping_path = self.cross_references[clipping_path_id[1]] + @force_nodocument + def build_text(self, text): + if "dx" in text.attrib or "dy" in text.attrib: + raise NotImplementedError( + '"dx" / "dy" defined on is currently not supported (but contributions are welcome!)' + ) + if "lengthAdjust" in text.attrib: + raise NotImplementedError( + '"lengthAdjust" defined on is currently not supported (but contributions are welcome!)' + ) + if "rotate" in text.attrib: + raise NotImplementedError( + '"rotate" defined on is currently not supported (but contributions are welcome!)' + ) + if "style" in text.attrib: + raise NotImplementedError( + '"style" defined on is currently not supported (but contributions are welcome!)' + ) + if "textLength" in text.attrib: + raise NotImplementedError( + '"textLength" defined on is currently not supported (but contributions are welcome!)' + ) + if "transform" in text.attrib: + raise NotImplementedError( + '"transform" defined on is currently not supported (but contributions are welcome!)' + ) + svg_text = SVGText( + text=text.text, + x=float(text.attrib.get("x", "0")), + y=float(text.attrib.get("y", "0")), + font_family=text.attrib.get("font-family"), + font_size=text.attrib.get("font-size"), + svg_obj=self, + ) + self.update_xref(text.attrib.get("id"), svg_text) + return svg_text + @force_nodocument def build_image(self, image): href = None @@ -980,6 +1032,47 @@ def build_image(self, image): return svg_image +class SVGText(NamedTuple): + text: str + x: Number + y: Number + font_family: str + font_size: Number + svg_obj: SVGObject + + def __deepcopy__(self, _memo): + # Defining this method is required to avoid the .svg_obj reference to be cloned: + return SVGText( + text=self.text, + x=self.x, + y=self.y, + font_family=self.font_family, + font_size=self.font_size, + svg_obj=self.svg_obj, + ) + + @force_nodocument + def render(self, _gsd_registry, _style, last_item, initial_point): + font_mgr = self.svg_obj and self.svg_obj.font_mgr + if not font_mgr: + raise AssertionError( + "fpdf2 bug - Cannot render a raster image without a SVGObject.font_mgr" + ) + # pylint: disable=fixme + # TODO: + # * handle font_family & font_size + # * invoke current_font.encode_text(self.text) + # * set default font to Times/16 if not font set + # * support textLength -> .font_stretching + # We need to perform a mirror transform AND invert the Y-axis coordinates, + # so that the text is not horizontally mirrored, + # due to the transformation made by DrawingContext._setup_render_prereqs(): + stream_content = ( + f"q 1 0 0 -1 0 0 cm BT {self.x:.2f} {-self.y:.2f} Td ({self.text}) Tj ET Q" + ) + return stream_content, last_item, initial_point + + class SVGImage(NamedTuple): href: str x: Number @@ -1011,7 +1104,7 @@ def render(self, _resource_registry, _style, last_item, initial_point): # pylint: disable=cyclic-import,import-outside-toplevel from .image_parsing import preload_image - _, _, info = preload_image(image_cache, self.href) + _, _, info = preload_image(self.href, image_cache) if isinstance(info, VectorImageInfo): LOGGER.warning( "Inserting .svg vector graphics in tags is currently not supported (contributions are welcome to add support for it)" diff --git a/fpdf/text_region.py b/fpdf/text_region.py index 9e25bad6d..3dcc796d6 100644 --- a/fpdf/text_region.py +++ b/fpdf/text_region.py @@ -253,7 +253,7 @@ def build_line(self): # We do double duty as a "text line wrapper" here, since all the necessary # information is already in the ImageParagraph object. self.name, self.img, self.info = preload_image( - self.region.pdf.image_cache, self.name + self.name, image_cache=self.region.pdf.image_cache, font_mgr=self.region.pdf ) return self diff --git a/fpdf/text_renderer.py b/fpdf/text_renderer.py new file mode 100644 index 000000000..73895a43d --- /dev/null +++ b/fpdf/text_renderer.py @@ -0,0 +1,791 @@ +import logging +import warnings + +from contextlib import contextmanager +from math import isclose +from os.path import splitext +from pathlib import Path +from typing import Iterator, Union + +from .bidi import BidiParagraph, auto_detect_base_direction +from .deprecation import get_stack_level +from .drawing_primitives import convert_to_device_color +from .errors import FPDFException, FPDFUnicodeEncodingException +from .fonts import CORE_FONTS, CoreFont, FontFace, TextStyle, TTFFont +from .enums import PDFResourceType, TextDirection, TextEmphasis +from .line_break import Fragment, TotalPagesSubstitutionFragment +from .unicode_script import UnicodeScript, get_unicode_script + +HERE = Path(__file__).resolve().parent +FPDF_FONT_DIR = HERE / "font" +LOGGER = logging.getLogger(__name__) + + +class TextRendererMixin: + """ + Mix-in to be added to FPDF(). + # TODO: add details + """ + + def __init__(self, *args, **kwargs): + self.fonts = {} # map font string keys to an instance of CoreFont or TTFFont + self.core_fonts_encoding = "latin-1" + "Font encoding, Latin-1 by default" + # Replace these fonts with these core fonts + self.font_aliases = { + "arial": "helvetica", + "couriernew": "courier", + "timesnewroman": "times", + } + # Graphics state variables defined as properties by GraphicsStateMixin. + # We set their default values here. + self.font_family = "" # current font family + # current font style (BOLD/ITALICS - does not handle UNDERLINE nor STRIKETHROUGH): + self.font_style = "" + self.underline = False + self.strikethrough = False + self.font_size_pt = 12 # current font size in points + self.font_stretching = 100 # current font stretching + self.char_spacing = 0 # current character spacing + self.current_font = None # None or an instance of CoreFont or TTFFont + self.current_font_is_set_on_page = False + self.text_color = self.DEFAULT_TEXT_COLOR + self.render_color_fonts = True + self._fallback_font_ids = [] + self._fallback_font_exact_match = False + # pylint: disable=fixme + # TODO: add self.text_mode + self._record_text_quad_points / ._text_quad_points + super().__init__(*args, **kwargs) + + @property + def emphasis(self) -> TextEmphasis: + "The current text emphasis: bold, italics, underline and/or strikethrough." + font_style = self.font_style + if self.strikethrough: + font_style += "S" + if self.underline: + font_style += "U" + return TextEmphasis.coerce(font_style) + + @property + def is_ttf_font(self) -> bool: + return self.current_font and self.current_font.type == "TTF" + + def set_text_color(self, r, g=-1, b=-1): + """ + Defines the color used for text. + Accepts either a single greyscale value, 3 values as RGB components, a single `#abc` or `#abcdef` hexadecimal color string, + or an instance of `fpdf.drawing.DeviceCMYK`, `fpdf.drawing.DeviceRGB` or `fpdf.drawing.DeviceGray`. + The method can be called before the first page is created and the value is retained from page to page. + + Args: + r (int, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): if `g` and `b` are given, this indicates the red component. + Else, this indicates the grey level. The value must be between 0 and 255. + g (int): green component (between 0 and 255) + b (int): blue component (between 0 and 255) + """ + self.text_color = convert_to_device_color(r, g, b) + + def set_font_size(self, size): + """ + Configure the font size in points + + Args: + size (float): font size in points + """ + if isclose(self.font_size_pt, size): + return + self.font_size_pt = size + self.current_font_is_set_on_page = False + + def set_char_spacing(self, spacing): + """ + Sets horizontal character spacing. + A positive value increases the space between characters, a negative value + reduces it (which may result in glyph overlap). + By default, no spacing is set (which is equivalent to a value of 0). + + Args: + spacing (float): horizontal spacing in document units + """ + if self.char_spacing == spacing: + return + self.char_spacing = spacing + if self.page > 0: + self._out(f"BT {spacing:.2f} Tc ET") + + def set_stretching(self, stretching): + """ + Sets horizontal font stretching. + By default, no stretching is set (which is equivalent to a value of 100). + + Args: + stretching (float): horizontal stretching (scaling) in percents. + """ + if self.font_stretching == stretching: + return + self.font_stretching = stretching + if self.page > 0: + self._out(f"BT {stretching:.2f} Tz ET") + + def set_fallback_fonts(self, fallback_fonts, exact_match=True): + """ + Allows you to specify a list of fonts to be used if any character is not available on the font currently set. + Detailed documentation: https://py-pdf.github.io/fpdf2/Unicode.html#fallback-fonts + + Args: + fallback_fonts: sequence of fallback font IDs + exact_match (bool): when a glyph cannot be rendered uing the current font, + fpdf2 will look for a fallback font matching the current character emphasis (bold/italics). + If it does not find such matching font, and `exact_match` is True, no fallback font will be used. + If it does not find such matching font, and `exact_match` is False, a fallback font will still be used. + To get even more control over this logic, you can also override `FPDF.get_fallback_font()` + """ + fallback_font_ids = [] + for fallback_font in fallback_fonts: + found = False + for fontkey in self.fonts: + # will add all font styles on the same family + if fontkey.replace("B", "").replace("I", "") == fallback_font.lower(): + fallback_font_ids.append(fontkey) + found = True + if not found: + raise FPDFException( + f"Undefined fallback font: {fallback_font} - Use FPDF.add_font() beforehand" + ) + self._fallback_font_ids = tuple(fallback_font_ids) + self._fallback_font_exact_match = exact_match + + @contextmanager + def use_text_style(self, text_style: TextStyle): + prev_l_margin = None + if text_style: + if text_style.t_margin: + self.ln(text_style.t_margin) + if text_style.l_margin: + if isinstance(text_style.l_margin, (float, int)): + prev_l_margin = self.l_margin + self.l_margin = text_style.l_margin + self.x = self.l_margin + else: + LOGGER.debug( + "Unsupported '%s' value provided as l_margin to .use_text_style()", + text_style.l_margin, + ) + with self.use_font_face(text_style): + yield + if text_style and text_style.b_margin: + self.ln(text_style.b_margin) + if prev_l_margin is not None: + self.l_margin = prev_l_margin + self.x = self.l_margin + + @contextmanager + def use_font_face(self, font_face: FontFace): + """ + Sets the provided `fpdf.fonts.FontFace` in a local context, + then restore font settings back to they were initially. + This method must be used as a context manager using `with`: + + with pdf.use_font_face(FontFace(emphasis="BOLD", color=255, size_pt=42)): + put_some_text() + + Known limitation: in case of a page jump in this local context, + the temporary style may "leak" in the header() & footer(). + """ + if not font_face: + yield + return + prev_font = (self.font_family, self.font_style, self.font_size_pt) + self.set_font( + font_face.family or self.font_family, + ( + font_face.emphasis.style + if font_face.emphasis is not None + else self.font_style + ), + font_face.size_pt or self.font_size_pt, + ) + self.current_font_is_set_on_page = False + prev_text_color = self.text_color + if font_face.color is not None and font_face.color != self.text_color: + self.set_text_color(font_face.color) + prev_fill_color = self.fill_color + if font_face.fill_color is not None: + self.set_fill_color(font_face.fill_color) + yield + if font_face.fill_color is not None: + self.set_fill_color(prev_fill_color) + self.text_color = prev_text_color + self.set_font(*prev_font) + + def set_new_page_font_settings(self): + self.font_family = "" + self.font_stretching = 100 + self.char_spacing = 0 + + def add_font(self, family=None, style="", fname=None, uni="DEPRECATED"): + """ + Imports a TrueType or OpenType font and makes it available + for later calls to the `FPDF.set_font()` method. + + You will find more information on the "Unicode" documentation page. + + Args: + family (str): optional name of the font family. Used as a reference for `FPDF.set_font()`. + If not provided, use the base name of the `fname` font path, without extension. + style (str): font style. "" for regular, include 'B' for bold, and/or 'I' for italic. + fname (str): font file name. You can specify a relative or full path. + If the file is not found, it will be searched in `FPDF_FONT_DIR`. + uni (bool): [**DEPRECATED since 2.5.1**] unused + """ + if not fname: + raise ValueError('"fname" parameter is required') + + ext = splitext(str(fname))[1].lower() + if ext not in (".otf", ".otc", ".ttf", ".ttc"): + raise ValueError( + f"Unsupported font file extension: {ext}." + " add_font() used to accept .pkl file as input, but for security reasons" + " this feature is deprecated since v2.5.1 and has been removed in v2.5.3." + ) + + if uni != "DEPRECATED": + warnings.warn( + ( + '"uni" parameter is deprecated since v2.5.1, ' + "unused and will soon be removed" + ), + DeprecationWarning, + stacklevel=get_stack_level(), + ) + + style = "".join(sorted(style.upper())) + if any(letter not in "BI" for letter in style): + raise ValueError( + f"Unknown style provided (only B & I letters are allowed): {style}" + ) + + for parent in (".", FPDF_FONT_DIR): + if not parent: + continue + if (Path(parent) / fname).exists(): + font_file_path = Path(parent) / fname + break + else: + raise FileNotFoundError(f"TTF Font file not found: {fname}") + + if family is None: + family = font_file_path.stem + + fontkey = f"{family.lower()}{style}" + # Check if font already added or one of the core fonts + if fontkey in self.fonts or fontkey in CORE_FONTS: + warnings.warn( + f"Core font or font already added '{fontkey}': doing nothing", + stacklevel=get_stack_level(), + ) + return + + self.fonts[fontkey] = TTFFont(self, font_file_path, fontkey, style) + + def set_font(self, family=None, style: Union[str, TextEmphasis] = "", size=0): + """ + Sets the font used to print character strings. + It is mandatory to call this method at least once before printing text. + + Default encoding is not specified, but all text writing methods accept only + unicode for external fonts and one byte encoding for standard. + + Standard fonts use `Latin-1` encoding by default, but Windows + encoding `cp1252` (Western Europe) can be used with + `self.core_fonts_encoding = encoding`. + + The font specified is retained from page to page. + The method can be called before the first page is created. + + Args: + family (str): name of a font added with `FPDF.add_font`, + or name of one of the 14 standard "PostScript" fonts: + Courier (fixed-width), Helvetica (sans serif), Times (serif), + Symbol (symbolic) or ZapfDingbats (symbolic) + If an empty string is provided, the current family is retained. + style (str, fpdf.enums.TextEmphasis): empty string (by default) or a combination + of one or several letters among B (bold), I (italic), S (strikethrough) and U (underline). + Bold and italic styles do not apply to Symbol and ZapfDingbats fonts. + size (float): in points. The default value is the current size. + """ + if not family: + family = self.font_family + + family = family.lower() + if isinstance(style, TextEmphasis): + style = style.style + style = "".join(sorted(style.upper())) + if any(letter not in "BISU" for letter in style): + raise ValueError( + f"Unknown style provided (only B/I/S/U letters are allowed): {style}" + ) + if "U" in style: + self.underline = True + style = style.replace("U", "") + else: + self.underline = False + if "S" in style: + self.strikethrough = True + style = style.replace("S", "") + else: + self.strikethrough = False + + if family in self.font_aliases and family + style not in self.fonts: + warnings.warn( + f"Substituting font {family} by core font {self.font_aliases[family]}" + " - This is deprecated since v2.7.8, and will soon be removed", + DeprecationWarning, + stacklevel=get_stack_level(), + ) + family = self.font_aliases[family] + elif family in ("symbol", "zapfdingbats") and style: + warnings.warn( + f"Built-in font {family} only has a single 'style' " + "and can't be bold or italic", + stacklevel=get_stack_level(), + ) + style = "" + + if not size: + size = self.font_size_pt + + # Test if font is already selected + if ( + self.font_family == family + and self.font_style == style + and isclose(self.font_size_pt, size) + ): + return + + # Test if used for the first time + fontkey = family + style + if fontkey not in self.fonts: + if fontkey not in CORE_FONTS: + raise FPDFException( + f"Undefined font: {fontkey} - " + f"Use built-in fonts or FPDF.add_font() beforehand" + ) + # If it's one of the core fonts, add it to self.fonts + self.fonts[fontkey] = CoreFont(len(self.fonts) + 1, fontkey, style) + + # Select it + self.font_family = family + self.font_style = style + self.font_size_pt = size + self.current_font = self.fonts[fontkey] + self.current_font_is_set_on_page = False + + def set_text_shaping( + self, + use_shaping_engine: bool = True, + features: dict = None, + direction: Union[str, TextDirection] = None, + script: str = None, + language: str = None, + ): + """ + Enable or disable text shaping engine when rendering text. + If features, direction, script or language are not specified the shaping engine will try + to guess the values based on the input text. + + Args: + use_shaping_engine: enable or disable the use of the shaping engine to process the text + features: a dictionary containing 4 digit OpenType features and whether each feature + should be enabled or disabled + example: features={"kern": False, "liga": False} + direction: the direction the text should be rendered, either "ltr" (left to right) + or "rtl" (right to left). + script: a valid OpenType script tag like "arab" or "latn" + language: a valid OpenType language tag like "eng" or "fra" + """ + if not use_shaping_engine: + self.text_shaping = None + return + + try: + # pylint: disable=import-outside-toplevel, unused-import + import uharfbuzz + except ImportError as exc: + raise FPDFException( + "The uharfbuzz package could not be imported, but is required for text shaping. Try: pip install uharfbuzz" + ) from exc + + # + # Features must be a dictionary containing opentype features and a boolean flag + # stating whether the feature should be enabled or disabled. + # + # e.g. features={"liga": True, "kern": False} + # + # https://harfbuzz.github.io/shaping-opentype-features.html + # + + if features and not isinstance(features, dict): + raise FPDFException( + "Features must be a dictionary. See text shaping documentation" + ) + if not features: + features = {} + + # Buffer properties (direction, script and language) + # if the properties are not provided, Harfbuzz "guessing" logic is used. + # https://harfbuzz.github.io/setting-buffer-properties.html + # Valid harfbuzz directions are ltr (left to right), rtl (right to left), + # ttb (top to bottom) or btt (bottom to top) + + text_direction = None + if direction: + text_direction = ( + direction + if isinstance(direction, TextDirection) + else TextDirection.coerce(direction) + ) + if text_direction not in [TextDirection.LTR, TextDirection.RTL]: + raise FPDFException( + "FPDF2 only accept ltr (left to right) or rtl (right to left) directions for now." + ) + + self.text_shaping = { + "use_shaping_engine": True, + "features": features, + "direction": text_direction, + "script": script, + "language": language, + "fragment_direction": None, + "paragraph_direction": None, + } + + def get_string_width(self, s, normalized=False, markdown=False): + """ + Returns the length of a string in user unit. A font must be selected. + The value is calculated with stretching and spacing. + + Note that the width of a cell has some extra padding added to this width, + on the left & right sides, equal to the .c_margin property. + + Args: + s (str): the string whose length is to be computed. + normalized (bool): whether normalization needs to be performed on the input string. + markdown (bool): indicates if basic markdown support is enabled + """ + # normalized is parameter for internal use + s = s if normalized else self.normalize_text(s) + w = 0 + for frag in self._preload_bidirectional_text(s, markdown): + w += frag.get_width() + return w + + def get_fallback_font(self, char, style=""): + """ + Returns which fallback font has the requested glyph. + This method can be overridden to provide more control than the `select_mode` parameter + of `FPDF.set_fallback_fonts()` provides. + """ + emphasis = TextEmphasis.coerce(style) + fonts_with_char = [ + font_id + for font_id in self._fallback_font_ids + if ord(char) in self.fonts[font_id].cmap + ] + if not fonts_with_char: + return None + font_with_matching_emphasis = next( + (font for font in fonts_with_char if self.fonts[font].emphasis == emphasis), + None, + ) + if font_with_matching_emphasis: + return font_with_matching_emphasis + if self._fallback_font_exact_match: + return None + return fonts_with_char[0] + + def normalize_text(self, text): + """Check that text input is in the correct format/encoding""" + # - for TTF unicode fonts: unicode object (utf8 encoding) + # - for built-in fonts: string instances (encoding: latin-1, cp1252) + if not self.is_ttf_font and self.core_fonts_encoding: + try: + return text.encode(self.core_fonts_encoding).decode("latin-1") + except UnicodeEncodeError as error: + raise FPDFUnicodeEncodingException( + text_index=error.start, + character=text[error.start], + font_name=self.font_family + self.font_style, + ) from error + return text + + def _preload_bidirectional_text(self, text, markdown): + """ " + Break the text into bidirectional segments and preload font styles for each fragment + """ + if not self.text_shaping: + return self._preload_font_styles(text, markdown) + paragraph_direction = ( + self.text_shaping["direction"] + if self.text_shaping["direction"] + else auto_detect_base_direction(text) + ) + + paragraph = BidiParagraph(text=text, base_direction=paragraph_direction) + directional_segments = paragraph.get_bidi_fragments() + self.text_shaping["paragraph_direction"] = paragraph.base_direction + + fragments = [] + for bidi_text, bidi_direction in directional_segments: + self.text_shaping["fragment_direction"] = bidi_direction + fragments += self._preload_font_styles(bidi_text, markdown) + return tuple(fragments) + + def _preload_font_styles(self, text, markdown): + """ + When Markdown styling is enabled, we require secondary fonts + to ender text in bold & italics. + This function ensure that those fonts are available. + It needs to perform Markdown parsing, + so we return the resulting `styled_txt_frags` tuple + to avoid repeating this processing later on. + """ + if not text: + return tuple() + prev_font_style = self.font_style + if self.underline: + prev_font_style += "U" + if self.strikethrough: + prev_font_style += "S" + styled_txt_frags = tuple(self._parse_chars(text, markdown)) + if markdown: + page = self.page + # We set the current to page to zero so that + # set_font() does not produce any text object on the stream buffer: + self.page = 0 + if any(frag.font_style == "B" for frag in styled_txt_frags): + # Ensuring bold font is supported: + self.set_font(style="B") + if any(frag.font_style == "I" for frag in styled_txt_frags): + # Ensuring italics font is supported: + self.set_font(style="I") + if any(frag.font_style == "BI" for frag in styled_txt_frags): + # Ensuring bold italics font is supported: + self.set_font(style="BI") + if any(frag.font_style == "" for frag in styled_txt_frags): + # Ensuring base font is supported: + self.set_font(style="") + for frag in styled_txt_frags: + frag.font = self.fonts[frag.font_family + frag.font_style] + # Restoring initial style: + self.set_font(style=prev_font_style) + self.page = page + return styled_txt_frags + + def _parse_chars(self, text: str, markdown: bool) -> Iterator[Fragment]: + "Split text into fragments" + if not markdown and not self.text_shaping and not self._fallback_font_ids: + if self.str_alias_nb_pages: + for seq, fragment_text in enumerate( + text.split(self.str_alias_nb_pages) + ): + if seq > 0: + yield TotalPagesSubstitutionFragment( + 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_strikethrough, in_underline = ( + [], + "B" in self.font_style, + "I" in self.font_style, + bool(self.strikethrough), + bool(self.underline), + ) + current_fallback_font = None + current_text_script = None + + def frag(): + nonlocal txt_frag, current_fallback_font, current_text_script + gstate = self._get_current_graphics_state() + gstate["font_style"] = ("B" if in_bold else "") + ( + "I" if in_italics else "" + ) + gstate["strikethrough"] = in_strikethrough + gstate["underline"] = in_underline + if current_fallback_font: + style = "".join(c for c in current_fallback_font if c in ("BI")) + family = current_fallback_font.replace("B", "").replace("I", "") + gstate["font_family"] = family + gstate["font_style"] = style + gstate["current_font"] = self.fonts[current_fallback_font] + current_fallback_font = None + current_text_script = None + fragment = Fragment( + txt_frag, + gstate, + self.k, + ) + txt_frag = [] + return fragment + + if self.is_ttf_font: + font_glyphs = self.current_font.cmap + else: + font_glyphs = [] + num_escape_chars = 0 + + while text: + is_marker = text[:2] in ( + self.MARKDOWN_BOLD_MARKER, + self.MARKDOWN_ITALICS_MARKER, + self.MARKDOWN_STRIKETHROUGH_MARKER, + self.MARKDOWN_UNDERLINE_MARKER, + ) + half_marker = text[0] + text_script = get_unicode_script(text[0]) + if text_script not in ( + UnicodeScript.COMMON, + UnicodeScript.UNKNOWN, + current_text_script, + ): + if txt_frag and current_text_script: + 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["strikethrough"] = in_strikethrough + gstate["underline"] = in_underline + yield TotalPagesSubstitutionFragment( + 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 ( + is_marker + and (not txt_frag or txt_frag[-1] != half_marker) + and (len(text) < 3 or text[2] != half_marker) + ): + txt_frag = ( + txt_frag[: -((num_escape_chars + 1) // 2)] + if num_escape_chars > 0 + else txt_frag + ) + if num_escape_chars % 2 == 0: + if txt_frag: + yield frag() + if text[:2] == self.MARKDOWN_BOLD_MARKER: + in_bold = not in_bold + if text[:2] == self.MARKDOWN_ITALICS_MARKER: + in_italics = not in_italics + if text[:2] == self.MARKDOWN_STRIKETHROUGH_MARKER: + in_strikethrough = not in_strikethrough + if text[:2] == self.MARKDOWN_UNDERLINE_MARKER: + in_underline = not in_underline + text = text[2:] + continue + num_escape_chars = ( + num_escape_chars + 1 + if text[0] == self.MARKDOWN_ESCAPE_CHARACTER + else 0 + ) + is_link = self.MARKDOWN_LINK_REGEX.match(text) + if is_link: + link_text, link_dest, text = is_link.groups() + if txt_frag: + yield frag() + gstate = self._get_current_graphics_state() + gstate["underline"] = self.MARKDOWN_LINK_UNDERLINE + if self.MARKDOWN_LINK_COLOR: + gstate["text_color"] = self.MARKDOWN_LINK_COLOR + try: + page = int(link_dest) + link_dest = self.add_link(page=page) + except ValueError: + pass + yield Fragment( + list(link_text), + gstate, + self.k, + link=link_dest, + ) + continue + if self.is_ttf_font and text[0] != "\n" and not ord(text[0]) in font_glyphs: + style = ("B" if in_bold else "") + ("I" if in_italics else "") + fallback_font = self.get_fallback_font(text[0], style) + if fallback_font: + if fallback_font == current_fallback_font: + txt_frag.append(text[0]) + text = text[1:] + continue + if txt_frag: + yield frag() + current_fallback_font = fallback_font + txt_frag.append(text[0]) + text = text[1:] + continue + if current_fallback_font: + if txt_frag: + yield frag() + current_fallback_font = None + txt_frag.append(text[0]) + text = text[1:] + if txt_frag: + yield frag() + + def set_doc_option(self, opt, value): + """ + Defines a document option. + + Args: + opt (str): name of the option to set + value (str) option value + + .. deprecated:: 2.4.0 + Simply set the `FPDF.core_fonts_encoding` property as a replacement. + """ + warnings.warn( + ( + "set_doc_option() is deprecated since v2.4.0 " + "and will be removed in a future release. " + "Simply set the `.core_fonts_encoding` property as a replacement." + ), + DeprecationWarning, + stacklevel=get_stack_level(), + ) + if opt != "core_fonts_encoding": + raise FPDFException(f'Unknown document option "{opt}"') + self.core_fonts_encoding = value + + def _set_font_for_page(self, font, font_size_pt, wrap_in_text_object=True): + """ + Set font and size for current page. + This step is needed before adding text into page and not needed in set_font and set_font_size. + """ + sl = f"/F{font.i} {font_size_pt:.2f} Tf" + if wrap_in_text_object: + sl = f"BT {sl} ET" + self._resource_catalog.add(PDFResourceType.FONT, font.i, self.page) + self.current_font_is_set_on_page = True + return sl diff --git a/test/outline/toc_no_reset_page_indices.pdf b/test/outline/toc_no_reset_page_indices.pdf index f8074bf4e242a398252fd571825b2f20f2747269..2a88e833fb5b8390abcdb2553b7a3031eb1c73b7 100644 GIT binary patch literal 126126 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 XJ0&X{2Sy^c0{*x=@4UZ5%eemoDg?3F literal 126372 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 diff --git a/test/outline/toc_with_extra_page_0.pdf b/test/outline/toc_with_extra_page_0.pdf index 1e74d73122edc5423611cc0d336d4ef2d8bf8370..a43fd7e3de838e8e210261d8aa80dcefc7651f53 100644 GIT binary patch delta 5682 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 U@^kl)#bBCDWVu|PjfL!g0YIxvf&c&j delta 5680 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 diff --git a/test/outline/toc_with_extra_page_1.pdf b/test/outline/toc_with_extra_page_1.pdf index 4b5060a983c26c0b1ed71428ce9a365046dad51c..0c776020c61e97cd2cc9e2375843439ddbd885b4 100644 GIT binary patch delta 5809 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 SmW$1s4Wm+QHrINM)c*jW1z|P- delta 5840 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 diff --git a/test/outline/toc_with_extra_page_2.pdf b/test/outline/toc_with_extra_page_2.pdf index 0aa733aad24c88243f4194793310d97f99f700de..430855ecb76ff1a16304d5337faf42e1ac7423af 100644 GIT binary patch delta 5700 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 delta 5725 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 m6Nr5R>n-_zE$qMlJcu}1I diff --git a/test/svg/parameters.py b/test/svg/parameters.py index 476257b05..1d056e7ec 100644 --- a/test/svg/parameters.py +++ b/test/svg/parameters.py @@ -783,6 +783,9 @@ def Gs(**kwargs): svgfile("path_clippingpath.svg"), id=" containing a used in a group with color - issue #1147", ), + # pylint: disable=fixme + # TODO: enable this test + # pytest.param(svgfile("text-samples.svg"), id=" tests"), ) svg_path_edge_cases = ( diff --git a/test/svg/svg_sources/embedded-raster-images.svg b/test/svg/svg_sources/embedded-raster-images.svg index ffef5b6ed..ddb936169 100644 --- a/test/svg/svg_sources/embedded-raster-images.svg +++ b/test/svg/svg_sources/embedded-raster-images.svg @@ -1,7 +1,7 @@ - Example image.svg - embedding raster images + Example embedded-raster-images.svg diff --git a/test/svg/svg_sources/text-samples.svg b/test/svg/svg_sources/text-samples.svg new file mode 100644 index 000000000..ccdc4cc61 --- /dev/null +++ b/test/svg/svg_sources/text-samples.svg @@ -0,0 +1,17 @@ + + + Example text-samples.svg + + + + Text without any attributes + + My + cat + is + Grumpy! + + Bottom right + + diff --git a/test/text/clip_text_modes.pdf b/test/text/clip_text_modes.pdf index ef77ea179ff3a6e5b4de3208c44fc2b8d7ecfeff..4fe627b4bac219501e1d00669b107ccba607ad08 100644 GIT binary patch literal 22474 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 SlL>)<>mwwUm8~6ZB>x9@@Fp7o literal 20634 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