From 1ba8fcd9d8fb486d9f8aa517de4ed3ddf8f2890d Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:01:35 +0100 Subject: [PATCH] Support for striketrough text - solve #1322 (#1340) --- CHANGELOG.md | 1 + docs/DocumentOutlineAndTableOfContents.md | 15 ++-- docs/HTML.md | 2 +- docs/PageLabels.md | 1 + docs/Text.md | 3 +- docs/TextStyling.md | 4 +- docs/Unicode.md | 7 +- docs/index.md | 2 +- fpdf/enums.py | 5 ++ fpdf/fonts.py | 42 ++++++--- fpdf/fpdf.py | 103 ++++++++++++++++------ fpdf/graphics_state.py | 9 ++ fpdf/html.py | 5 +- fpdf/line_break.py | 4 + mkdocs.yml | 9 +- test/fonts/test_set_font.py | 2 +- test/html/html_strikethrough.pdf | Bin 0 -> 8715 bytes test/html/test_html.py | 10 +++ 18 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 test/html/html_strikethrough.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a519e6e..fda2db23a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.8.3] - Not released yet ### Added * support for [shading patterns (gradients)](https://py-pdf.github.io/fpdf2/Patterns.html) +* support for strikethrough text ### Fixed * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): Fixed rendering of content following `` tags; now correctly resets emphasis style post `` tag: hyperlink styling contained within the tag authority. - [Issue #1311](https://github.com/py-pdf/fpdf2/issues/1311) diff --git a/docs/DocumentOutlineAndTableOfContents.md b/docs/DocumentOutlineAndTableOfContents.md index 058aa7135..1316d1577 100644 --- a/docs/DocumentOutlineAndTableOfContents.md +++ b/docs/DocumentOutlineAndTableOfContents.md @@ -1,13 +1,11 @@ # Document Outline & Table of Contents ## Overview - This document explains how to implement and customize the Document Outline (also known as Bookmarks) and Table of Contents (ToC) features in `fpdf2`. --- ## Document Outline (Bookmarks) - Document outlines allow users to navigate quickly through sections in the PDF by creating a hierarchical structure of clickable links. Quoting the 6th edition of the PDF format reference (v1.7 - 2006) : @@ -27,7 +25,6 @@ However, you can configure **global title styles** by calling [`set_section_titl To provide a document outline to the PDF you generate, you just have to call the `start_section` method for every hierarchical section you want to define. ### Nested outlines - Outlines can be nested by specifying different levels. Higher-level outlines (e.g., level 0) appear at the top, while sub-levels (e.g., level 1, level 2) are indented. ```python @@ -38,15 +35,14 @@ pdf.start_section(name="Section 1.1: Background", level=1) --- ## Table of Contents - Quoting [Wikipedia](https://en.wikipedia.org/wiki/Table_of_contents), a **table of contents** is: > a list, usually found on a page before the start of a written work, of its chapter or section titles or brief descriptions with their commencing page numbers. ### Inserting a Table of Contents - Use the [`insert_toc_placeholder`](fpdf/fpdf.html#fpdf.fpdf.FPDF.insert_toc_placeholder) method to define a placeholder for the ToC. A page break is triggered after inserting the ToC. -**Parameters:** +Parameters: + - **render_toc_function**: Function called to render the ToC, receiving two parameters: `pdf`, an FPDF instance, and `outline`, a list of `fpdf.outline.OutlineSection`. - **pages**: The number of pages that the ToC will span, including the current one. A page break occurs for each page specified. - **allow_extra_pages**: If `True`, allows unlimited additional pages to be added to the ToC as needed. These extra ToC pages are initially created at the end of the document and then reordered when the final PDF is produced. @@ -54,7 +50,6 @@ Use the [`insert_toc_placeholder`](fpdf/fpdf.html#fpdf.fpdf.FPDF.insert_toc_plac **Note**: Enabling `allow_extra_pages` may affect page numbering for headers or footers. Since extra ToC pages are added after the document content, they might cause page numbers to appear out of sequence. To maintain consistent numbering, use (Page Labels)[PageLabels.md] to assign a specific numbering style to the ToC pages. When using Page Labels, any extra ToC pages will follow the numbering style of the first ToC page. ### Reference Implementation - _New in [:octicons-tag-24: 2.8.2](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ The `fpdf.outline.TableOfContents` class provides a reference implementation of the ToC, which can be used as-is or subclassed. @@ -72,7 +67,6 @@ pdf.insert_toc_placeholder(toc.render_toc, allow_extra_pages=True) --- ## Using Outlines and ToC with HTML - When using [`FPDF.write_html`](HTML.md), a document outline is automatically generated, and a ToC can be added with the `` tag. To customize ToC styling, override the `render_toc` method in a subclass: @@ -105,7 +99,6 @@ pdf.output("html_toc.pdf") --- ## Additional Code Samples - The regression tests are a good place to find code samples. For example, the [`test_simple_outline`](https://github.com/py-pdf/fpdf2/blob/master/test/outline/test_outline.py) test function generates the PDF document [simple_outline.pdf](https://github.com/py-pdf/fpdf2/blob/master/test/outline/simple_outline.pdf). @@ -116,4 +109,6 @@ generates [test_html_toc.pdf](https://github.com/py-pdf/fpdf2/blob/5453422bf560a --- ## Manually Adjusting `pdf.page` -Setting `pdf.page` manually may result in unexpected behavior. `pdf.add_page()` takes special care to ensure the page's content stream matches fpdf's instance attributes. Manually setting the page does not. +⚠️ Setting `pdf.page` manually may result in unexpected behavior. +`pdf.add_page()` takes special care to ensure the page's content stream matches fpdf's instance attributes. +Manually setting the page does not. diff --git a/docs/HTML.md b/docs/HTML.md index 5e5f510f7..9305729a4 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -159,7 +159,7 @@ pdf.output("html_helvetica.pdf") * `

` to `

`: headings (and `align` attribute) * `

`: paragraphs (and `align`, `line-height` attributes) * `
` & `


` tags -* ``, ``, ``: bold, italic, underline +* ``, ``, ``, ``: bold, italic, strikethrough, underline * ``: (and `face`, `size`, `color` attributes) * `
` for aligning * ``: links (and `href` attribute) to a file, URL, or page number. diff --git a/docs/PageLabels.md b/docs/PageLabels.md index b361c27c1..ac201fc87 100644 --- a/docs/PageLabels.md +++ b/docs/PageLabels.md @@ -7,6 +7,7 @@ _New in [:octicons-tag-24: 2.8.2](https://github.com/py-pdf/fpdf2/blob/master/CH In a PDF document, each page is identified by an integer page index, representing the page's position within the document. Optionally, a document can also define **page labels** to visually display page identifiers. **Page labels** can be customized. For example, a document might begin with front matter numbered in roman numerals and transition to arabic numerals for the main content. In this case: + - The first page (index `0`) would have a label `i` - The twelfth page (index `11`) would have label `xii` - The thirteenth page (index `12`) would start with label `1` diff --git a/docs/Text.md b/docs/Text.md index d0cc7178f..d449149e9 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -77,8 +77,7 @@ character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the selected `new_x`/`new_y` position. It is possible to put a link on the text. If `markdown=True`, then minimal [markdown](TextStyling.md#markdowntrue) -styling is enabled, to render parts of the text in bold, italics, and/or -underlined. +styling is enabled, to render parts of the text in bold, italics, strikethrough and/or underlined. If automatic page breaking is enabled and the cell goes beyond the limit, a page break is performed before outputting. diff --git a/docs/TextStyling.md b/docs/TextStyling.md index f9540b31b..84abbe2ef 100644 --- a/docs/TextStyling.md +++ b/docs/TextStyling.md @@ -6,8 +6,10 @@ Setting emphasis on text can be controlled by using `.set_font(style=...)`: * `style="B"` indicates **bold** * `style="I"` indicates _italics_ +* `style="S"` indicates strikethrough * `style="U"` indicates underline -* `style="BI"` indicates _**bold italics**_ + +Letters can be combined, for example: `style="BI"` indicates _**bold italics**_ ```python from fpdf import FPDF diff --git a/docs/Unicode.md b/docs/Unicode.md index 72b29efd8..630bb5c88 100644 --- a/docs/Unicode.md +++ b/docs/Unicode.md @@ -63,7 +63,12 @@ pdf.add_font("dejavu-sans-narrow", style="", fname="DejaVuSansCondensed.ttf") pdf.add_font("dejavu-sans-narrow", style="i", fname="DejaVuSansCondensed-Oblique.ttf") ``` -To actually use the loaded font, or to use one of the standard built-in fonts, you'll have to set the current font before calling any text generating method. `.set_font()` uses the same combinations of family name and style as arguments, plus the font size in typographic points. In addition to the previously mentioned styles, the letter "u" may be included for creating underlined text. If the family or size are omitted, the already set values will be retained. If the style is omitted, it defaults to regular. +To actually use the loaded font, or to use one of the standard built-in fonts, you'll have to set the current font before calling any text generating method. +`.set_font()` uses the same combinations of family name and style as arguments, plus the font size in typographic points. +In addition to the previously mentioned styles, the letter `u` may be included for creating underlined text, +and `s` for creating strikethrough text. +If the family or size are omitted, the already set values will be retained. +If the style is omitted, it defaults to regular. ```python # Set and use first family in regular style. diff --git a/docs/index.md b/docs/index.md index 45d34e191..3a62d4127 100644 --- a/docs/index.md +++ b/docs/index.md @@ -109,6 +109,7 @@ Online classes & open source projects: * [OpenSfM](https://github.com/mapillary/OpenSfM) : a Structure from Motion library, serving as a processing pipeline for reconstructing camera poses and 3D scenes from multiple images * [RPA Framework](https://github.com/robocorp/rpaframework) : libraries and tools for Robotic Process Automation (RPA), designed to be used with both [Robot Framework](https://robotframework.org) : [rpa-pdf](https://pypi.org/project/rpa-pdf/) package * [Concordia](https://github.com/LibraryOfCongress/concordia) : a platform developed by the US Library of Congress for crowdsourcing transcription and tagging of text in digitized images +* [FreeCAD-Beginner-Assistant](https://github.com/alekssadowski95/FreeCAD-Beginner-Assistant) : FreeCAD plugin providing feedback on best practices for beginning FreeCAD users * [wudududu/extract-video-ppt](https://github.com/wudududu/extract-video-ppt) : create a one-page-per-frame PDF from a video or PPT file. `fpdf2` also has a demo script to convert a GIF into a one-page-per-frame PDF: [gif2pdf.py](https://github.com/py-pdf/fpdf2/blob/master/tutorial/gif2pdf.py) * [Planet-Matriarchy-RPG-CharGen](https://github.com/ShawnDriscoll/Planet-Matriarchy-RPG-CharGen) : a PyQt based desktop application (= `.exe` under Windows) that provides a RPG character sheet generator @@ -137,7 +138,6 @@ Online classes & open source projects: ## Misc ## * Release notes for every versions of `fpdf2`: [CHANGELOG.md](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md) -* Project history: [History](History.md) * This library could only exist thanks to the dedication of many volunteers around the world: [list & map of contributors](https://github.com/py-pdf/fpdf2/blob/master/README.md#contributors-) * You can download an offline PDF version of this manual: [fpdf2-manual.pdf](fpdf2-manual.pdf) diff --git a/fpdf/enums.py b/fpdf/enums.py index e28d745a1..500454dbd 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -243,6 +243,9 @@ class TextEmphasis(CoerciveIntFlag): U = 4 "Underline" + S = 8 + "Strikethrough" + @property def style(self): return "".join( @@ -268,6 +271,8 @@ def coerce(cls, value): return cls.I if value.upper() == "UNDERLINE": return cls.U + if value.upper() == "STRIKETHROUGH": + return cls.S return super(cls, cls).coerce(value) diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 9618c7372..ecc501f13 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -194,14 +194,27 @@ def __init__(self, *args, **kwargs): class CoreFont: # RAM usage optimization: - __slots__ = ("i", "type", "name", "up", "ut", "cw", "fontkey", "emphasis") + __slots__ = ( + "i", + "type", + "name", + "sp", + "ss", + "up", + "ut", + "cw", + "fontkey", + "emphasis", + ) def __init__(self, fpdf, fontkey, style): self.i = len(fpdf.fonts) + 1 self.type = "core" self.name = CORE_FONTS[fontkey] - self.up = -100 - self.ut = 50 + self.sp = 250 # strikethrough horizontal position + self.ss = 50 # strikethrough size (height) + self.up = -100 # underline horizontal position + self.ut = 50 # underline height self.cw = CORE_FONTS_CHARWIDTHS[fontkey] self.fontkey = fontkey self.emphasis = TextEmphasis.coerce(style) @@ -226,6 +239,8 @@ class TTFFont: "desc", "glyph_ids", "hbfont", + "sp", + "ss", "up", "ut", "cw", @@ -254,18 +269,21 @@ def __init__(self, fpdf, font_file_path, fontkey, style): self.scale = 1000 / self.ttfont["head"].unitsPerEm default_width = round(self.scale * self.ttfont["hmtx"].metrics[".notdef"][0]) + os2_table = self.ttfont["OS/2"] + post_table = self.ttfont["post"] + try: - cap_height = self.ttfont["OS/2"].sCapHeight + cap_height = os2_table.sCapHeight except AttributeError: cap_height = self.ttfont["hhea"].ascent # entry for the PDF font descriptor specifying various characteristics of the font flags = FontDescriptorFlags.SYMBOLIC - if self.ttfont["post"].isFixedPitch: + if post_table.isFixedPitch: flags |= FontDescriptorFlags.FIXED_PITCH - if self.ttfont["post"].italicAngle != 0: + if post_table.italicAngle != 0: flags |= FontDescriptorFlags.ITALIC - if self.ttfont["OS/2"].usWeightClass >= 600: + if os2_table.usWeightClass >= 600: flags |= FontDescriptorFlags.FORCE_BOLD self.desc = PDFFontDescriptor( @@ -277,8 +295,8 @@ def __init__(self, fpdf, font_file_path, fontkey, style): f"[{self.ttfont['head'].xMin * self.scale:.0f} {self.ttfont['head'].yMin * self.scale:.0f}" f" {self.ttfont['head'].xMax * self.scale:.0f} {self.ttfont['head'].yMax * self.scale:.0f}]" ), - italic_angle=int(self.ttfont["post"].italicAngle), - stem_v=round(50 + int(pow((self.ttfont["OS/2"].usWeightClass / 65), 2))), + italic_angle=int(post_table.italicAngle), + stem_v=round(50 + int(pow((os2_table.usWeightClass / 65), 2))), missing_width=default_width, ) @@ -320,8 +338,10 @@ def __init__(self, fpdf, font_file_path, fontkey, style): sbarr += fpdf.str_alias_nb_pages self.name = re.sub("[ ()]", "", self.ttfont["name"].getBestFullName()) - self.up = round(self.ttfont["post"].underlinePosition * self.scale) - self.ut = round(self.ttfont["post"].underlineThickness * self.scale) + self.up = round(post_table.underlinePosition * self.scale) + self.ut = round(post_table.underlineThickness * self.scale) + self.sp = round(os2_table.yStrikeoutPosition * self.scale) + self.ss = round(os2_table.yStrikeoutSize * self.scale) self.emphasis = TextEmphasis.coerce(style) self.subset = SubsetMap(self, [ord(char) for char in sbarr]) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 6a84a5ed1..6c5720960 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -323,9 +323,10 @@ def __init__( # 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): + # current font style (BOLD/ITALICS - does not handle UNDERLINE nor STRIKETHROUGH): self.font_style = "" - self.underline = False # underlining flag + 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 @@ -441,10 +442,13 @@ def _set_min_pdf_version(self, version): @property def emphasis(self) -> TextEmphasis: - "The current text emphasis: bold, italics and/or underlined." - return TextEmphasis.coerce( - f"{self.font_style}U" if self.underline else self.font_style - ) + "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: @@ -857,7 +861,7 @@ def alias_nb_pages(self, alias="{nb}"): ----- When using this feature with the `FPDF.cell` / `FPDF.multi_cell` methods, - or the `.underline` attribute of `FPDF` class, + or the `.underline` / `.strikethrough` attributes of `FPDF` class, the width of the text rendered will take into account the alias length, not the length of the "actual number of pages" string, which can causes slight positioning differences. @@ -931,7 +935,7 @@ def add_page( "A page cannot be added on a closed document, after calling output()" ) family = self.font_family - style = f"{self.font_style}U" if self.underline else self.font_style + emphasis = self.emphasis size = self.font_size_pt lw = self.line_width dc = self.draw_color @@ -990,7 +994,7 @@ def add_page( # Set font if family: - self.set_font(family, style, size) + self.set_font(family, emphasis, size) # Set colors self.draw_color = dc @@ -1010,7 +1014,7 @@ def add_page( self._out(f"{lw * self.k:.2f} w") if family: - self.set_font(family, style, size) # Restore font + self.set_font(family, emphasis, size) # Restore font if self.draw_color != dc: # Restore colors self.draw_color = dc @@ -2058,7 +2062,7 @@ def add_font(self, family=None, style="", fname=None, uni="DEPRECATED"): self.fonts[fontkey] = TTFFont(self, font_file_path, fontkey, style) - def set_font(self, family=None, style="", size=0): + 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. @@ -2079,8 +2083,8 @@ def set_font(self, family=None, style="", size=0): 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): empty string (by default) or a combination - of one or several letters among B (bold), I (italic) and U (underline). + 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. """ @@ -2088,16 +2092,23 @@ def set_font(self, family=None, style="", size=0): family = self.font_family family = family.lower() + if isinstance(style, TextEmphasis): + style = style.style style = "".join(sorted(style.upper())) - if any(letter not in "BIU" for letter in style): + if any(letter not in "BISU" for letter in style): raise ValueError( - f"Unknown style provided (only B/I/U letters are allowed): {style}" + 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( @@ -2687,10 +2698,15 @@ def text(self, x, y, text=""): sl.append(f" {self.text_mode} Tr {self.line_width:.2f} w") sl.append(f"{self.current_font.encode_text(text)} ET") self._resource_catalog.add(PDFResourceType.FONT, self.current_font.i, self.page) - if (self.underline and text != "") or self._record_text_quad_points: + if ( + text != "" and (self.underline or self.strikethrough) + ) or self._record_text_quad_points: w = self.get_string_width(text, normalized=True, markdown=False) - if self.underline and text != "": - sl.append(self._do_underline(x, y, w)) + if text != "": + if self.underline: + sl.append(self._do_underline(x, y, w)) + if self.strikethrough: + sl.append(self._do_strikethrough(x, y, w)) if self._record_text_quad_points: h = self.font_size y -= 0.8 * h # same coefficient as in _render_styled_text_line() @@ -2880,6 +2896,7 @@ def local_context(self, **kwargs): * nom_lift * nom_scale * paint_rule + * strikethrough * stroke_cap_style * stroke_join_style * stroke_miter_limit @@ -2960,6 +2977,7 @@ def _start_local_context( "font_stretching", "nom_lift", "nom_scale", + "strikethrough", "sub_lift", "sub_scale", "sup_lift", @@ -3067,7 +3085,8 @@ def cell( (identifier returned by `FPDF.add_link`) or external URL. center (bool): center the cell horizontally on the page. markdown (bool): enable minimal markdown-like markup to render part - of text as bold / italics / underlined. Supports `\\` as escape character. Default to False. + of text as bold / italics / strikethrough / underlined. + Supports `\\` as escape character. Default to False. txt (str): [**DEPRECATED since v2.7.6**] String to print. Default value: empty string. Returns: a boolean indicating if page break was triggered @@ -3362,6 +3381,15 @@ def _render_styled_text_line( frag.font, ) ) + if frag.strikethrough: + sl.append( + self._do_strikethrough( + self.x + dx + s_width, + self.y + (0.5 * h) + (0.3 * frag.font_size), + frag_width, + frag.font, + ) + ) if frag.link: self.link( x=self.x + dx + s_width, @@ -3489,6 +3517,8 @@ def _preload_font_styles(self, text, markdown): 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 @@ -3847,7 +3877,8 @@ def multi_cell( ln (int): **DEPRECATED since 2.5.1**: Use `new_x` and `new_y` instead. max_line_height (float): optional maximum height of each sub-cell generated markdown (bool): enable minimal markdown-like markup to render part - of text as bold / italics / underlined. Supports `\\` as escape character. Default to False. + of text as bold / italics / strikethrough / underlined. + Supports `\\` as escape character. Default to False. print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable character, instead of a line breaking opportunity. Default value: False wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default), @@ -4872,16 +4903,32 @@ def _default_file_id(self, buffer): hash_hex = id_hash.hexdigest().upper() return f"<{hash_hex}><{hash_hex}>" - def _do_underline(self, x, y, w, current_font=None): - "Draw an horizontal line starting from (x, y) with a length equal to 'w'" - if current_font is None: - current_font = self.current_font - up = current_font.up - ut = current_font.ut + def _do_underline(self, x, y, w, font=None): + """ + Draw an horizontal line under some text, + starting from (x, y) with a length equal to 'w' + """ + if font is None: + font = self.current_font + return ( + f"{x * self.k:.2f} " + f"{(self.h - y + font.up / 1000 * self.font_size) * self.k:.2f} " + f"{w * self.k:.2f} " + f"{-font.ut / 1000 * self.font_size_pt:.2f} re f" + ) + + def _do_strikethrough(self, x, y, w, font=None): + """ + Draw an horizontal line through some text, + starting from (x, y) with a length equal to 'w' + """ + if font is None: + font = self.current_font return ( f"{x * self.k:.2f} " - f"{(self.h - y + up / 1000 * self.font_size) * self.k:.2f} " - f"{w * self.k:.2f} {-ut / 1000 * self.font_size_pt:.2f} re f" + f"{(self.h - y + font.sp / 1000 * self.font_size) * self.k:.2f} " + f"{w * self.k:.2f} " + f"{-font.ss / 1000 * self.font_size_pt:.2f} re f" ) def _out(self, s): diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py index 43dce75d0..ed79e407c 100644 --- a/fpdf/graphics_state.py +++ b/fpdf/graphics_state.py @@ -36,6 +36,7 @@ def __init__(self, *args, **kwargs): fill_color=self.DEFAULT_FILL_COLOR, text_color=self.DEFAULT_TEXT_COLOR, underline=False, + strikethrough=False, font_style="", font_stretching=100, char_spacing=0, @@ -111,6 +112,14 @@ def underline(self): def underline(self, v): self.__statestack[-1]["underline"] = v + @property + def strikethrough(self): + return self.__statestack[-1]["strikethrough"] + + @strikethrough.setter + def strikethrough(self, v): + self.__statestack[-1]["strikethrough"] = v + @property def font_style(self): return self.__statestack[-1]["font_style"] diff --git a/fpdf/html.py b/fpdf/html.py index 04b7e34a2..585742410 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -34,6 +34,7 @@ "em": FontFace(emphasis="ITALICS"), "font": FontFace(), "i": FontFace(emphasis="ITALICS"), + "s": FontFace(emphasis="STRIKETHROUGH"), "strong": FontFace(emphasis="BOLD"), "u": FontFace(emphasis="UNDERLINE"), # Block tags are TextStyle instances : @@ -71,7 +72,7 @@ "ol": TextStyle(t_margin=2), "ul": TextStyle(t_margin=2), } -INLINE_TAGS = ("a", "b", "code", "em", "font", "i", "strong", "u") +INLINE_TAGS = ("a", "b", "code", "em", "font", "i", "s", "strong", "u") BLOCK_TAGS = HEADING_TAGS + ( "blockquote", "center", @@ -782,6 +783,7 @@ def handle_starttag(self, tag, attrs): "dd", "dt", "pre", + "s", "strong", "u", ): @@ -1078,6 +1080,7 @@ def handle_endtag(self, tag): "dd", "dt", "pre", + "s", "strong", "u", ): diff --git a/fpdf/line_break.py b/fpdf/line_break.py index 8edd8f6d6..b7857985b 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -123,6 +123,10 @@ def text_mode(self): def underline(self): return self.graphics_state["underline"] + @property + def strikethrough(self): + return self.graphics_state["strikethrough"] + @property def draw_color(self): return self.graphics_state["draw_color"] diff --git a/mkdocs.yml b/mkdocs.yml index 14beb561f..1ec36d22c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -168,17 +168,16 @@ nav: - 'Signing': 'Signing.md' - 'File attachments': 'FileAttachments.md' - 'Mixing other libs': - - 'Combine with borb': 'CombineWithBorb.md' - - 'Combine with livereload': 'CombineWithLivereload.md' - - 'Combine with mistletoe': 'CombineWithMistletoeoToUseMarkdown.md' - 'Combine with pypdf': 'CombineWithPypdf.md' + - 'Combine with mistletoe': 'CombineWithMistletoeoToUseMarkdown.md' + - 'Combine with livereload': 'CombineWithLivereload.md' + - 'Combine with borb': 'CombineWithBorb.md' - 'Combine with pdfrw': 'CombineWithPdfrw.md' - 'Matplotlib, Pandas, Plotly, Pygal': 'CombineWithChartingLibs.md' - 'Usage in web APIs': 'UsageInWebAPI.md' - 'Rendering spreadsheets as PDF tables': 'RenderingSpreadsheetsAsPDFTables.md' - - 'Combine with Rough.js': 'CombineWithRoughJS.md' - 'Templating with Jinja': 'TemplatingWithJinja.md' - - 'Database storage': 'DatabaseStorage.md' + - 'Combine with Rough.js': 'CombineWithRoughJS.md' - 'Development': - 'Development guidelines': 'Development.md' - 'Logging': 'Logging.md' diff --git a/test/fonts/test_set_font.py b/test/fonts/test_set_font.py index 831c3bd95..eca932605 100644 --- a/test/fonts/test_set_font.py +++ b/test/fonts/test_set_font.py @@ -37,7 +37,7 @@ def test_set_unknown_style(): with pytest.raises(ValueError) as e: pdf.set_font("Times", style="bold") assert ( - str(e.value) == "Unknown style provided (only B/I/U letters are allowed): BDLO" + str(e.value) == "Unknown style provided (only B/I/S/U letters are allowed): BDLO" ) diff --git a/test/html/html_strikethrough.pdf b/test/html/html_strikethrough.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d7fd5aeafd9af8a11a5ab78f61b328f16011bba9 GIT binary patch literal 8715 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 CTy5q6 literal 0 HcmV?d00001 diff --git a/test/html/test_html.py b/test/html/test_html.py index 18eb68bcb..d7e55f6ab 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -194,6 +194,16 @@ def test_html_bold_italic_underline(tmp_path): assert_pdf_equal(pdf, HERE / "html_bold_italic_underline.pdf", tmp_path) +def test_html_strikethrough(tmp_path): + pdf = FPDF() + pdf.add_font(fname=HERE / "../fonts/DejaVuSans.ttf") + pdf.set_font_size(30) + pdf.add_page() + pdf.write_html("strikethrough") + pdf.write_html('strikethrough') + assert_pdf_equal(pdf, HERE / "html_strikethrough.pdf", tmp_path) + + def test_html_customize_ul(tmp_path): html = """
  • term1: definition1