From dbc1114d247da3739dfc8777fab1ed0ed0fd61cb Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Thu, 8 Sep 2022 12:01:39 +0200 Subject: [PATCH] Support Superscript, Subscript and Fractional positioning (#520) --- CHANGELOG.md | 5 +- docs/HTML.md | 1 + docs/TextStyling.md | 63 +++++++ docs/char_vpos.png | Bin 0 -> 3308 bytes fpdf/enums.py | 18 ++ fpdf/fpdf.py | 18 +- fpdf/graphics_state.py | 155 ++++++++++++++++- fpdf/html.py | 16 +- fpdf/line_break.py | 35 +++- test/html/html_custom_heading_sizes.pdf | Bin 2321 -> 2324 bytes test/html/html_features.pdf | Bin 5928 -> 5935 bytes test/html/html_heading_hebrew.pdf | Bin 9003 -> 9003 bytes test/html/html_headings_line_height.pdf | Bin 5174 -> 5190 bytes test/html/html_superscript.pdf | Bin 0 -> 1424 bytes test/html/test_html.py | 10 ++ test/local_context_inherited_shared_props.pdf | Bin 1130 -> 1191 bytes test/outline/custom_HTML2FPDF.pdf | Bin 2553 -> 2564 bytes test/outline/html_toc.pdf | Bin 4492 -> 4512 bytes test/outline/html_toc_2_pages.pdf | Bin 21774 -> 22465 bytes .../html_toc_with_h1_as_2nd_heading.pdf | Bin 2835 -> 2843 bytes test/test_graphics_context.py | 157 ++++++++++++------ test/text/test_line_break.py | 5 + test/text/test_write.py | 49 ++++++ test/text/write_superscript.pdf | Bin 0 -> 1258 bytes 24 files changed, 466 insertions(+), 66 deletions(-) create mode 100644 docs/char_vpos.png create mode 100644 test/html/html_superscript.pdf create mode 100644 test/text/write_superscript.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0a66647..6b7c84c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,19 +21,22 @@ This can also be enabled programmatically with `warnings.simplefilter('default', - `fpdf2` now uses [fontTools](https://fonttools.readthedocs.io/en/latest/) to read and embed fonts in the PDF, thanks to @gmischler and @RedShy ### Fixed +- Text following a HTML heading can't overlap with that heading anymore, thanks to @gmischler - `arc()` not longer renders artefacts at intersection point, thanks to @Jmillan-Dev; [#488](https://github.com/PyFPDF/fpdf2/issues/488) - [`write_html()`](https://pyfpdf.github.io/fpdf2/HTML.html): * `` & `` HTML tags are now properly supported - they were ignored previously; [#498](https://github.com/PyFPDF/fpdf2/issues/498) * `bgcolor` is not properly support in `` tags; [#512](https://github.com/PyFPDF/fpdf2/issues/512) - the `CreationDate` of PDFs & embedded files now includes the system timezone - ### Added +- Added support for subscript, superscript, nominator and denominator char positioning as well as \ and \ HTML tags, thanks to @gmischler - [`set_page_background()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_page_background): new method added by @semaeostomea: [link to documentation](https://pyfpdf.github.io/fpdf2/PageFormatAndOrientation.html#per-page-format-orientation-and-background) - [`embed_file()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.embed_file) & [`file_attachment_annotation()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.file_attachment_annotation): new methods to add file attachments - [link to documentation](https://pyfpdf.github.io/fpdf2/FileAttachments.html) - A new method [`set_char_spacing()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_char_spacing) allows to increase the spacing between individual characters, thanks to @gmischler: [link to documentation](https://pyfpdf.github.io/fpdf2/TextStyling.html) - workaround by @semaeostomea to support arabic and right-to-left scripts: [link to documentation](https://pyfpdf.github.io/fpdf2/Unicode.html#right-to-left-arabic-script-workaround) - documentation on shapes styling: [link to documentation](https://pyfpdf.github.io/fpdf2/Shapes.html#path-styling) - documentation on sharing the images cache among FPDF instances: [link to documentation](https://pyfpdf.github.io/fpdf2/Images.html#sharing-the-image-cache-among-fpdf-instances) +### Changed +- HTML headings are now rendered with an additional leading of 20% the font size above and below them. ## [2.5.6] - 2022-08-16 ### Added diff --git a/docs/HTML.md b/docs/HTML.md index f1062c8d2..db757d8bd 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -81,6 +81,7 @@ pdf.output("html.pdf") * ``: links (and `href` attribute) * ``: images (and `src`, `width`, `height` attributes) * `
    `, `
      `, `
    • `: ordered, unordered and list items (can be nested) +* ``, ``: superscript and subscript text * `
`: (and `border`, `width` attributes) + ``: header (opens each page) + ``: footer (closes each page) diff --git a/docs/TextStyling.md b/docs/TextStyling.md index bbec6af85..24560718a 100644 --- a/docs/TextStyling.md +++ b/docs/TextStyling.md @@ -66,6 +66,69 @@ pdf.multi_cell(w=150, txt=LOREM_IPSUM[:200], new_x="LEFT", fill=True) ``` ![](char_spacing.png) + +## Subscript, Superscript, and Fractional Numbers + +The class attribute `.char_vpos` controls special vertical positioning modes for text: + +* "LINE" - normal line text (default) +* "SUP" - superscript (exponent) +* "SUB" - subscript (index) +* "NOM" - nominator of a fraction with "/" +* "DENOM" - denominator of a fraction with "/" + +For each positioning mode there are two parameters that can be configured. +The defaults have been set to result in a decent layout with most fonts, and are given in parens. + +The size multiplier for the font size: + +* `.sup_scale` (0.7) +* `.sub_scale` (0.7) +* `.nom_scale` (0.75) +* `.denom_scale` (0.75) + +The lift is given as fraction of the unscaled font size and indicates how much the glyph gets lifted above the base line (negative for below): + +* `.sup_lift` (0.4) +* `.sub_lift` (-0.15) +* `.nom_lift` (0.2) +* `.denom_lift` (0.0) + +**Limitations:** The individual glyphs will be scaled down as configured. This is not typographically correct, as it will also reduce the stroke width, making them look lighter than the normal text. +Unicode fonts may include characters in the [subscripts and superscripts range](https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts). In a high quality font, those glyphs will be smaller than the normal ones, but have a proportionally stronger stroke width in order to maintain the same visual density. If available in good quality, using Characters from this range is preferred and will look better. Unfortunately, many fonts either don't (fully) cover this range, or the glyphs are of unsatisfactory quality. In those cases, this feature of `fpdf2` offers a reliable workaround with suboptimal but consistent output quality. + +Practical use is essentially limited to `.write()` and `html_write()`. +The feature does technically work with `.cell()` and `.multi_cell`, but is of limited usefulness there, since you can't change font properties in the middle of a line (there is no markdown support). It currently gets completely ignored by `.text()`. + +The example shows the most common use cases: + +```python + pdf = fpdf.FPDF() + pdf.add_page() + pdf.set_font("Helvetica", "", 20) + pdf.write(txt="2") + pdf.char_vpos = "SUP" + pdf.write(txt="56") + pdf.char_vpos = "LINE" + pdf.write(txt=" more line text") + pdf.char_vpos = "SUB" + pdf.write(txt="(idx)") + pdf.char_vpos = "LINE" + pdf.write(txt=" end") + pdf.ln() + pdf.write(txt="1234 + ") + pdf.char_vpos = "NOM" + pdf.write(txt="5") + pdf.char_vpos = "LINE" + pdf.write(txt="/") + pdf.char_vpos = "DENOM" + pdf.write(txt="16") + pdf.char_vpos = "LINE" + pdf.write(txt=" + 987 = x") +``` +![](char_vpos.png) + + ## .text_mode ## The PDF spec defines several text modes: diff --git a/docs/char_vpos.png b/docs/char_vpos.png new file mode 100644 index 0000000000000000000000000000000000000000..e17797673cc0fefd504fa721f6b400f928bf628e GIT binary patch literal 3308 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 btW@cO93s@}LG-0R4~50n(!rv}+&%U`TePH$ literal 0 HcmV?d00001 diff --git a/fpdf/enums.py b/fpdf/enums.py index 5b89ebb48..e2a517963 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -108,6 +108,24 @@ def coerce(cls, value): raise TypeError(f"{value} cannot convert to a {cls.__name__}") +class CharVPos(CoerciveEnum): + "Defines the vertical position of text relative to the line." + SUP = intern("SUP") + "Superscript" + + SUB = intern("SUB") + "Subscript" + + NOM = intern("NOM") + "Nominator of a fraction" + + DENOM = intern("DENOM") + "Denominator of a fraction" + + LINE = intern("LINE") + "Default line position" + + class Align(CoerciveEnum): "Defines how to render text in a cell" diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 1075b7d2a..97d1fe99b 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -73,6 +73,7 @@ class Image: YPos, Corner, FontDescriptorFlags, + CharVPos, ) from .errors import FPDFException, FPDFPageFormatException, FPDFUnicodeEncodingException from .fonts import fpdf_charwidths @@ -2555,6 +2556,7 @@ def local_context( text_color text_mode underline + char_vpos Args: **kwargs: key-values settings to set at the beggining of this context. @@ -2577,7 +2579,7 @@ def local_context( setattr(gs, key, value) if key == "blend_mode": self._set_min_pdf_version("1.4") - elif key in ("font_stretching", "text_mode", "underline"): + elif key in ("font_stretching", "text_mode", "underline", "char_vpos"): setattr(self, key, value) else: raise ValueError(f"Unsupported setting: {key}") @@ -2889,6 +2891,7 @@ def _render_styled_text_line( s_width, underlines = 0, [] # We try to avoid modifying global settings for temporary changes. current_ws = frag_ws = 0.0 + current_char_vpos = CharVPos.LINE current_font = self.current_font current_text_mode = self.text_mode current_font_stretching = self.font_stretching @@ -2932,9 +2935,14 @@ def _render_styled_text_line( if current_char_spacing != frag.char_spacing: current_char_spacing = frag.char_spacing sl.append(f"{frag.char_spacing:.2f} Tc") - if current_font != frag.font: + if current_font != frag.font or current_char_vpos != frag.char_vpos: + if current_char_vpos != frag.char_vpos: + current_char_vpos = frag.char_vpos current_font = frag.font sl.append(f"/F{frag.font['i']} {frag.font_size_pt:.2f} Tf") + lift = frag.lift + if lift != 0.0: + sl.append(f"{lift:.2f} Ts") if ( frag.text_mode != TextMode.FILL or frag.text_mode != current_text_mode @@ -3009,10 +3017,12 @@ def _render_styled_text_line( ) if sl: - # If any PDF settings have been left modified, wrap the line in a local context. + # If any PDF settings have been left modified, wrap the line + # in a local context. # pylint: disable=too-many-boolean-expressions if ( current_ws != 0.0 + or current_char_vpos != CharVPos.LINE or current_font != self.current_font or current_text_mode != self.text_mode or self.fill_color != self.text_color @@ -3022,8 +3032,8 @@ def _render_styled_text_line( s = f"q {' '.join(sl)} Q" else: s = " ".join(sl) - self._out(s) # pylint: enable=too-many-boolean-expressions + self._out(s) self.lasth = h # XPos.LEFT -> self.x stays the same diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py index 3c9d6bdf0..8dc55d92d 100644 --- a/fpdf/graphics_state.py +++ b/fpdf/graphics_state.py @@ -1,5 +1,5 @@ from .drawing import DeviceGray -from .enums import TextMode +from .enums import TextMode, CharVPos class GraphicsStateMixin: @@ -34,6 +34,15 @@ def __init__(self, *args, **kwargs): dash_pattern=dict(dash=0, gap=0, phase=0), line_width=0, text_mode=TextMode.FILL, + char_vpos=CharVPos.LINE, + sub_scale=0.7, + sup_scale=0.7, + nom_scale=0.75, + denom_scale=0.75, + sub_lift=-0.15, + sup_lift=0.4, + nom_lift=0.2, + denom_lift=0.0, ), ] super().__init__(*args, **kwargs) @@ -158,3 +167,147 @@ def text_mode(self): @text_mode.setter def text_mode(self, v): self.__statestack[-1]["text_mode"] = TextMode.coerce(v) + + @property + def char_vpos(self): + """ + Return vertical character position relative to line. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["char_vpos"] + + @char_vpos.setter + def char_vpos(self, v): + """ + Set vertical character position relative to line. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["char_vpos"] = CharVPos.coerce(v) + + @property + def sub_scale(self): + """ + Return scale factor for subscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["sub_scale"] + + @sub_scale.setter + def sub_scale(self, v): + """ + Set scale factor for subscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["sub_scale"] = float(v) + + @property + def sup_scale(self): + """ + Return scale factor for superscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["sup_scale"] + + @sup_scale.setter + def sup_scale(self, v): + """ + Set scale factor for superscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["sup_scale"] = float(v) + + @property + def nom_scale(self): + """ + Return scale factor for nominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["nom_scale"] + + @nom_scale.setter + def nom_scale(self, v): + """ + Set scale factor for nominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["nom_scale"] = float(v) + + @property + def denom_scale(self): + """ + Return scale factor for denominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["denom_scale"] + + @denom_scale.setter + def denom_scale(self, v): + """ + Set scale factor for denominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["denom_scale"] = float(v) + + @property + def sub_lift(self): + """ + Return lift factor for subscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["sub_lift"] + + @sub_lift.setter + def sub_lift(self, v): + """ + Set lift factor for subscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["sub_lift"] = float(v) + + @property + def sup_lift(self): + """ + Return lift factor for superscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["sup_lift"] + + @sup_lift.setter + def sup_lift(self, v): + """ + Set lift factor for superscript text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["sup_lift"] = float(v) + + @property + def nom_lift(self): + """ + Return lift factor for nominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["nom_lift"] + + @nom_lift.setter + def nom_lift(self, v): + """ + Set lift factor for nominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["nom_lift"] = float(v) + + @property + def denom_lift(self): + """ + Return lift factor for denominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + return self.__statestack[-1]["denom_lift"] + + @denom_lift.setter + def denom_lift(self, v): + """ + Set lift factor for denominator text. + ([docs](../TextStyling.html#subscript-superscript-and-fractional-numbers)) + """ + self.__statestack[-1]["denom_lift"] = float(v) diff --git a/fpdf/html.py b/fpdf/html.py index aaeedc74f..f233cf47b 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -245,6 +245,8 @@ def __init__( self.table_row_height = 0 self.heading_level = None self.heading_sizes = dict(**DEFAULT_HEADING_SIZES) + self.heading_above = 0.2 # extra space above heading, relative to font size + self.heading_below = 0.2 # extra space below heading, relative to font size if heading_sizes: self.heading_sizes.update(heading_sizes) self._only_imgs_in_td = False @@ -456,8 +458,8 @@ def handle_starttag(self, tag, attrs): self.heading_level = int(tag[1:]) hsize = self.heading_sizes[tag] self.pdf.set_text_color(150, 0, 0) + self.pdf.ln(self.h + self.heading_above * hsize) # more space above heading self.set_font(size=hsize) - self.pdf.ln(self.h) if attrs: self.align = attrs.get("align") if tag == "hr": @@ -595,6 +597,10 @@ def handle_starttag(self, tag, attrs): self.pdf.insert_toc_placeholder( self.render_toc, pages=int(attrs.get("pages", 1)) ) + if tag == "sup": + self.pdf.char_vpos = "SUP" + if tag == "sub": + self.pdf.char_vpos = "SUB" def handle_endtag(self, tag): # Closing tag @@ -602,9 +608,11 @@ def handle_endtag(self, tag): if tag in self.heading_sizes: self.heading_level = None face, size, color = self.font_stack.pop() + self.pdf.ln( + self.h + self.h * self.heading_below + ) # more space below heading self.set_font(face, size) self.set_text_color(*color) - self.pdf.ln(self.h) self.align = None if tag == "pre": face, size, color = self.font_stack.pop() @@ -668,6 +676,10 @@ def handle_endtag(self, tag): self.set_text_color(*self.font_color) if tag == "center": self.align = None + if tag == "sup": + self.pdf.char_vpos = "LINE" + if tag == "sub": + self.pdf.char_vpos = "LINE" def set_font(self, face=None, size=None): if face: diff --git a/fpdf/line_break.py b/fpdf/line_break.py index ae050a19b..e6ae11ffd 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -8,6 +8,7 @@ from typing import NamedTuple, Any, Union, Sequence +from .enums import CharVPos from .errors import FPDFException SOFT_HYPHEN = "\u00ad" @@ -60,7 +61,17 @@ def font_family(self): @property def font_size_pt(self): - return self.graphics_state["font_size_pt"] + size = self.graphics_state["font_size_pt"] + vpos = self.graphics_state["char_vpos"] + if vpos == CharVPos.SUB: + size *= self.graphics_state["sub_scale"] + elif vpos == CharVPos.SUP: + size *= self.graphics_state["sup_scale"] + elif vpos == CharVPos.NOM: + size *= self.graphics_state["nom_scale"] + elif vpos == CharVPos.DENOM: + size *= self.graphics_state["denom_scale"] + return size @property def font_size(self): @@ -98,6 +109,25 @@ def text_color(self): def line_width(self): return self.graphics_state["line_width"] + @property + def char_vpos(self): + return self.graphics_state["char_vpos"] + + @property + def lift(self): + vpos = self.graphics_state["char_vpos"] + if vpos == CharVPos.SUB: + lift = self.graphics_state["sub_lift"] + elif vpos == CharVPos.SUP: + lift = self.graphics_state["sup_lift"] + elif vpos == CharVPos.NOM: + lift = self.graphics_state["nom_lift"] + elif vpos == CharVPos.DENOM: + lift = self.graphics_state["denom_lift"] + else: + lift = 0.0 + return lift * self.graphics_state["font_size_pt"] + @property def string(self): return "".join(self.characters) @@ -140,8 +170,7 @@ def get_width( if self.font_stretching != 100: w *= self.font_stretching * 0.01 char_spacing *= self.font_stretching * 0.01 - if self.font_size_pt: - w *= self.font_size_pt * 0.001 + w *= self.font_size_pt * 0.001 if self.char_spacing != 0: # initial_cs must be False if the fragment is located at the # beginning of a text object, because the first char won't get spaced. diff --git a/test/html/html_custom_heading_sizes.pdf b/test/html/html_custom_heading_sizes.pdf index 2ca72e70bb25bf4455ab15a2d25a46b69e93d3c3..7a1a94f13706b1cb35b549f739de9b2437828866 100644 GIT binary patch delta 583 zcmbOzG(~7aL%pS)9anKlQEFl?SH+y(NoTp340&4K{}w5};+->T+wC}3*FBuIjE;YH zuVjzEFSl#O>fN`u1#lS8)E79o&G1Q-&Fs`eNoNxjlPB~=EM52H{#5BrRxFJtzt=g1 zX$Z4#3G~#x#JIU+pUs5(a?k2fZv1vsW{2_P$0DCSR)`mQ+AsVh8a2gCfM30& zU}0L&$@$-$w-yB@dzN-N=diYa|H$9I_VEX^pgB(hf)6L!SiUUYVZ|q|<2v)niklA0 zE17qmf9P%*cieHwyUnhQiy2wXEiLp6C%c`3?~P22#aExU~XbIxst;!*2&z`#M0H=$;`~r z(8R*U(!kKd)WqD;)Xl`%)X3S<+0KTbidZgiGDs{csVGWK<1(}`GviWKb@g}S0suA& Bs+a%( diff --git a/test/html/html_features.pdf b/test/html/html_features.pdf index 6b374393889ae0badcba99c484d4d87a349ad188..7d6f321b4719c238529fbfb263530a7d659dc5e4 100644 GIT binary patch delta 1052 zcmZ3Xw_a}p7bBy^WNt?F`UOhTg|qGI7~5+F1DD9;{FgVDPnM|3;qcDi&O3jd}At()y!YDIxT#mdIM*O+#Z1tmLlfgPv=EH3aLMTc4gO^ z3{Qzk>AhQCteaSNJM?r|Xf${B%o`tGt?fF#+-uHd%^#Ee4$57>DKx#r>Ql2-^p_>) zs<-yP%$cb?x6k}aC1cPZjVFd%q*lfUKf0o3KcBfjBwXdvdY-aWMJMAWEK~HeUFIF@ zY&GzHytwngEc@F#CHq#Sd_Hh0kK?%kfBj<{8+8Laommq-`xEk)f7-Cu?EBlGug+Dn zn%CrcG$s}1o!R&}f@938vFaK}en*Zv1efJ+qePFBm zd%oj_mj`3mPbM9jos!7B^xB%Lv$3j^IzH)!saRbyc(p9zlJ*LxtSNT_Z)&}o6~(H2 zqGRzVHP=@&tAdJ|)GDFX{54CRJq2A1gR49zUe(Z!4m zFx43uSXf}F1G)|@R*&Q+BNJm2427n~Mi^pdSj5bYFdS%PVT56xv8f@7W9mV{VPb?O zfQ$_wE<|#mi3!xz$YQ2unCi?dF)TL0;usT4Q%wworUs@M!D?z~XgRr8%r4f<1!#_= zg`tz7qq(W6nVGYhxtp<*qpPc%iMg?vk&B%TK^3uFc6MCFC5c5P6-B9OT&6}Q23)GD IuKsRZ0K3ppVE_OC delta 1045 zcmZ3lw?c0N7bBy=WNt?FdM8)2M`pEugv4g+c5rBVZ2R9oQ=gf~E{r9W;ri8Rmi5_O zQxp{iav$2Rntr38GkA$q(}W}q1+NF0>7V}m;JLW{bdN#MqG@+|V)|X@DWnLzUpW2g zVV$K}0^1@JC;em(l3SE=SKu4d_gz5;L&EdI6BQabLm2l6gs^Zi-~Ktzwy=Ixdig3P z?J8xSljXe?CD%LduUnnky(+i0Qq3xEcj(6-A)Vh#xa^h(Z#Z6B^=QtyS$`Jz_0PL< zGvxOSzwp8y3pfb_L4+zPvhOcf67!W*`+kGE_K6BQ6&>+FNfAmt|F1U zHVZtDtTNz?_?&K?@8%nF$#%mg>BE|O^^dl6_*+ZJi{+(kV!rYEW7&*Wrb_c zUwTi@yR8ERVGb7W>Y=W$&7+^td>BA z$>bKnS{5@)pb(2tEt`o6Sgc*BhRwvt7$nUp3}hOBn47zWg&D<+j1>$(Kp{_o3(PPu zF)%ZpyhX&S-q;jf%D}=9L%E@ufg!p&Lo;(rbTK0XOm#*E76usVjE&5}V)aOFGBPnT z!%%2yWP%}PhDFTW2*ZI!7KRw+8JilSIHn#H941Cs0?5b`;zA?`nwUUcjVxwrimA@b z0#nSw5L3+3L=!{M)W8(Qg&+r-8X6i+o+4%!Yhqz&YT)Q>=xpF(Z0PLhZ0=}bVD9GR rY-wg;U}RwEY-dAIMJ$({9anKlVo^y&QED2OsgbcUm#V6(zZ(|-30gu- diff --git a/test/html/html_heading_hebrew.pdf b/test/html/html_heading_hebrew.pdf index 74959777ec8a71ccbcb0da6b20a38b98b37aaec0..d78e6b9c9dafb3964eff27fbe1bb30377dc13882 100644 GIT binary patch delta 103 zcmZ4Ow%TpNah5Yq=kH8BtVfS{OQ8 eT9`RH8Jb(TnVY#dnLD~TIoa6|QZZRTSsDNdDjo#@ delta 103 zcmZ4Ow%TpNah5v{=Wk3rtfuoGi^<%ne=5olFeOOf6gt e%#95ko!!hWO^ggIoy{HHjO}a)shBLFEDZqd2^~lP diff --git a/test/html/html_headings_line_height.pdf b/test/html/html_headings_line_height.pdf index 298b2cb3dd30318e30ad78ecd89a239294aaa006..d4cde8492779793303597a22d8cada6c46188b99 100644 GIT binary patch delta 755 zcmdm{aZF=EeZ7f^ogG(kNl|KIE?32zy;Dy69X1ef{rqCThyVm$F*+P@89?P->OXs7ZY`tZr|r)u3q|V%e$I2FBUD;RF+tF z)Q3fPsj6e(rAM|Ij?4~6TDYe2cNO$8FKAJXYGFLGru&p(VHA@*+UuLshzaw6tF;+S-);QpTtOSvaaJtcH|U;9*st>Sn3 z3jSst6sr9o@gw`7(~k%HHcXcHzHaiJD>W|#8puFaeo_{fp@M+|kly^7v4@e_T+ecI z74ut0HZuzgJxk-wxvXy)nT;&8FcVW#V|2|XW`?GdXA9fKI-5HiI=i_TxmXw& zIXPQc7`U2Pm>N5qnY$X8n;W?p+Sw3P5z7Tm(TPPR6-B9OTqfqmrd+D3uKsRZ0QmL~ AkN^Mx delta 729 zcmX@6u}xz_eZ7&nogG(kNl|KIE?32zy;E-cH9H8jy|3)rd1ZEK`uXbH8O~C|xtsJ8 zbk??Ocm=Jt+nbQ6$(bXzrq%k#^Z7IL)2AI&TIiB>L)DmFyqG1WC8!{KN6}UFJ{Dey zQ=Os@!V1`~7W2JZDHdYrv&Hazz%R+e4b$>BGQMAEs#^1YVp9FfhjK;{dGhb)Cx$)Q z9X{jv{0%qu?Mc<%@$*F2{r}g?@5h(wMX;@D&03@X`E1}^N$Zk4Wv4ABw*+)e1h-7N z%90(G!=n3wq2onB=53X(f90LaO(Vze(*Qj+mg|zy9qo3u4W`IV0QI zYya7mR;FtfShOuUvg}8uv$@YS{&R`C337kma$b9{VEAZB>gtQ!B}}ZA zrXVIS&k`m!Q!_AoJKtMoaT5y@1p^RJ$W!0~GYkyO%q%AF7P6{0H^7iGH#fo%voJA6 z7c(?AF~ktFFt$V&Gcv{$Gd3`S*bB5C?j~b%0}P8zOpVRa4Ky*ew4A(J*e=$<(#_b( z+0op@(Z$ip&D_Pr(8AH&(9y}vz|7Uf+0w<%hMJ HcjE#8zial% diff --git a/test/html/html_superscript.pdf b/test/html/html_superscript.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bab787cb51983bd1c8bc1af246e8ba69872bdd56 GIT binary patch literal 1424 zcmah}O^6gn6eb3ul!Qc)4S0|j!s3cJRXsiZmrN$i&o(Zu88f?>)j_kpvo({}p6<1~ znyeD?2ho$ldJrQy#EZd8g2!AU6QTwo;$HS3B8G%Sk&pxu!X85MYIpGF%pD>5Je@wiz=7^_TAbiFTiQ|R?0?~& z%3dA&@XOgxZ;U)Q_2}uVpKV$Hhx7TbJCEAuOBcq@k_YRYmFep%t@{4VtqW6cbUwN> ze%q*aj&<};zCQWQwri(0{_^wnhi~8Av}MyP@4?NVwqBfUjy(U@)xUS=53k%mTsZyi z^_3Yq_^h zU@qMN@_F1SAmgk_R60|gJHi_RP=UumCVJ|gH5P0)j06~6XFBN&_DLgFW8t|zk5Vww zwjXW+!w9W#^h`JgI&hp5o#cgb%KIM=wHK^ZW$eV""" ) assert_pdf_equal(pdf, HERE / "bgcolor_in_table.pdf", tmp_path) + + +def test_html_superscript(tmp_path): + pdf = MyFPDF() + pdf.add_page() + pdf.write_html( + "

Superscript/Subscript test

" + "256 more line text(idx)" + ) + assert_pdf_equal(pdf, HERE / "html_superscript.pdf", tmp_path) diff --git a/test/local_context_inherited_shared_props.pdf b/test/local_context_inherited_shared_props.pdf index 99e528bcda793fdb8a4c711ddbff69e3dd46f880..63dde0069d62934a341ed5a11b818c69f325fa13 100644 GIT binary patch delta 369 zcmaFGv7B>4eZ7T&ogG(kNl|KIE?32z+P>Xfha3c4fB)@@K6dEjnY7LQVF3sAcFm38 zxY+Q+N1*~g%_qZ@kz5>3R)1hJtS*FL+gZ!3Ox;pBT_eiUYfV^kI6xDNN-By{)3_|m M47pTQUH#p-0ENYdbN~PV delta 317 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 diff --git a/test/outline/html_toc_2_pages.pdf b/test/outline/html_toc_2_pages.pdf index 70ef706f893c1b2e2fae442613d019d02738f26f..e255589c752ecb7e0ec71ebc80d6cfc4aa90cc87 100644 GIT binary patch literal 22465 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 z z1YslHb=mc8c`GFTXZE_Mhu6*xF4BMj_a5oQ^I;ck(@QdHgEyvS_OZ{M+^75P>YlXw zv6Z#wveP~eeCfBPD0gz!x7yFWC8LwJ2i#hqNzN`|;WGBRIb^~h^^V=)pl=At%AC95 z-=?L3GgT~BF36TReZT3>@)xsnPY=Bx^qGiKz4Qx72phR=?bM~-?ZHUV^!{Zo+aKm- zMdgkg7H~XZ{hJxJHt+6rckFoj>o=gt(%}b|-sh&pS#a9I8p1 zx9ec3&sOJY3wK^GcJy1Z^8tM%Z+Wpz>FSLeCMBmlA$C2=jJ&2j-R0i8(ztap+rkvt zy|9~~rrMrm9Z~5wCOYkQ-1{YYCwt^~ihQ%Ic>dy-X@xV3x;-hp|IXIe1aQN9Ne?b1 zrOWo*3Jwc=vEJKGh>BMRf?P~t`ylwj6O)7=QfXf?I(g1*0G-}%6py#fV%t^pSBt=1=dB0WM zW<)?LA-Okm!_CS7uLmpx$J|%zi|n#WxBQm0(?R09?S5bf-P%rXZ!b8z|JOnHPj^no zG9CkGUU<2h4P^`cV|;=$gV<2k{m0j0Hk6H+vdH6P15ReyWTei?cxfpvFfS!YBriR= z;(*^#%x-aw^RW@C4n2mQIu^R}?x$mI6I(O%nQayQzKnTPyY`DgShjT3@N4tqUl)%l zPd*xYzFZx5<;Ii&V$pHs{;0B9x``@mWA?tuF-uRCixTZ`pC5XQ*Y%o_wySu^8@rl?1ycjl zH+Uz!>=qIv=`Zb&z9TMcfy=S*{(s4W&nM>I&;LXL>pQ$dJqydeA*QT!F7fxkFz^?JpYO&K4^^_?X zMv@ptgLhhvVGIS{Cxid&yBchR&j?KaPAd)Xk?iO7zdVH0=Mo%cjAN|#8)KU<1C|!$ zM#g^ZH6RRa$Xrvh!CDAMisJ^zu$)jXdX91o;l!R*ASX~l25on+^;jg+b24;7^Ed^> zs%MKi12>-_ju5Qxn|hCF%xE-Nr(}2p;k{XRVGTyp6EqmK`2-1sU|lZI)K#THC0t3M z7KE8VBLofBZ9YLcLa;8mXo?_3E2JRfMJ_S#)<7(1ux|4Sk_bT-LYNOyI3bnu2tt`_ z#DWIvHlHARQx+sCT1w%l1@&Rk_%s1xL4$RhPmo3|SSJxp62vem!y{-2cMaaf z2J1GTAcGJzjr)z=#d4CA8Xj=qq6}v-!{Zp-CX6x$<2Ik40^!G=QAGqBPsCPgJ+p6}(lJL~|gFP?n$|z$>W$H*K7y#Z@!bmWb*=7=bKF zP!-_SRY3bx<17t!w#=4@?m!rUEaf0x@`@{z8R6mVY#B>Lcp!{WmU57=ArelR5gyLW zmXSn+2f_#>Nq`Cgny*o2l!vpjWh4>hfiOZzf(ikz)qO@W`u{c zvt_nKga^V1WZ6K;g*GERoSiLWi3ksb5z5lE=0clM9?s5|u|$*y!U$y9K+%OZBR!m* zEn|sD4}=lQ(zNOVqGzKC5oc%1SR&E`VFa>lpzH!B=8dy7*x53ci1a`hp)5`6E{qxF z;mm9qNkn-dj6jm6g%^-%Hf~9ar#zsg0aYFdBambRl^0OIY?LKu=H{D{Wr-*cgb~Wp zwDbbzyp6N8c*?`FM3e`@2xMtmdjV@Gjgz!^#>0|Cga^VjMiQz@y;Q@r_QG$b;qG^K zxt24tWfvR~9tb0pB~p87sJ_pc)-qy<)-bH38Hs_%&*C*Ytvh%?O+3(jD6XWH;OR>o z2VFuK7KSSaVAy~`_4=9^HUx4<>${)aBH|;Wbx~}cC<^@gpurpcXQ;IxtS{BIW+lX`r-(2yqY$yo^#8;( zMS(S>DPsK?jxk_`41Ofma$`8g_Ie~nq3w_$CNl5)OcfrDTa~(ztYIxPr)iaax9L?m3#~w19^nW zeoEHt=&?9X@a+PgkyY~Tg5%(qTuhFo2-E99D?pgwVJO(rEII}X>?RllZen}jte`8A ze=G?q*ZeWi3}-T?U?0-p;VRjvkB-T~v(P4Epk8D$Mw;4QPMi1vFcoO`<6|gId;=$O z!el=ZGF~bUzAQO_MQD@h}-9*##8du{377A7kRr z;F)@M%@RG92A*m%MzE<8Z$HxXdeAh+zh{8AnEDH?WY_P}^D&sx1cM>i{GPWTX?iUg z&=_m7AH&WDdB-XVlkcm*K$V!cpMqdZ2k5a1($o(WU`}PSAH$Y#c*iPD?_VVbiUhp< zl!U1dDM_|V5^#pr`=9rD zX5PPyp6>1Ig1eFuQi@6ABi@pXA1@19kf4^zrmJGq67Vxot<_0!@NR%a7NAaw*C$4* zlca=kz(cEz*Ck0q&7Xn;QdyurLI;oHAFF*^3O*O9lZKMGE3T9(aD^*PNeP;ERnk%g zMuV*|smwDzF-n~%4aLBLF|h3`rKB=%brSnI7E3A%nLS5}xnfc!g}W*lsaCJigt@zS z1{q>W35sx~NvWJ6J49C43E7BcMpoiohYYdI$a1FZkRg^CnP$Ir;qTdQYC|kDGSzW~ z*$o+DnUTpZtT2dWMkczj!XTCz8SBCdgIF9g&2(Lb8L`aB%C4(08e#!t07*(|Mb}lB z5zCA$@45=3A(k1L>beS}A(k1L?7|9zSY~9R3o8s_nUV1>tT2dWM#j3Z!XOrhOewpr z!i-pEWJSjn#@H#7P!P+EEbq9&?1l`n%*b@d6=pYNh-F3wL9(MvVmD-nWkx1Ct}weH zLo72gOy9ye%x=i0q;K>pN<`AP8#0&lp-Yk?lD@r=jaZ!8q==+%H)Jm98_!up(zhEj zm-LOuB9gw{kh!F9L>7_s?S{-HeIv4nq;EH5F6kSQMI?Q@A#+LJh%6%M+YOmZ`Vg5A zk@W3_Y{cTOXF^2Mw;M8&^a&A3-)_hd%Y4ovl0FWf582@n4S_^`LPXTJA2gEn2@zS} ze$YtRCq#sO`#~dVpAeDu?FWs-eL_Usw;wc;_i+(<-)_)GFz({QMFf8PK_iJD7m@hw z2aQC2TtwuzA2gEraS@r{e$Yth$3=vG`#~eA9~Y7O?FWs-eq2QCw;wc;`$fclyCFj? z?y44%`t1gcq<#^h-)_K2=ogXsg@MB)GOu{84y-jLfrTHi-sA2rQEQ{v6)ASpv@+!z ztI>f)BAKs7rBi#WBjcmMLY2Q-8>5>mrAY>SCP|m5R>et@b9Sxsh(G&UKi@xE9gc{|Dbo5e#a7X4@4)^2NkHw8UT`S?QKv*q1Jj$yphYh+X1CtM3&uyAHt&9#=F zj`?gTpK-=5^B>2;!)4n`cl=!bAh@RfY*22&sranoVfawp>dVL8bUpU^f_t|T<~69_ zNy)6=UjO5reCLg|uU@+}a$~|fP3M<3H6}6>?i9Y4FgK;?$QSa~`E#2R-v4sVk#N`N zYbzh5h3DM4rYr7!Nj>M(w0i};>zgy?7e84RAp5YfJmt!zu-_i{U0T)pu;szSPe$DD zcWHO!R}=OxeyF%ndU3$0B28WK*pd6T?LIKzz_#6W{+h|Eg=6<^-`%HFGw%2F&=un} zM_m^7IB;VBwakhQ3CEZvm*!3GvwYI;pB(B9ex`-K72+Q;M4PX|mxaMMG}l~?L<~4^ zcby7fe4C7aY0#SUpO^MtKJIJv%7bdKui|3ycZa&~t6xXo8NEB>iL5@nTj;!FFFzb` z{@K!kd9qSXePjMS+2Rwo1Hn5jpENF8_?$+&@zblh^IFdJmSxmc5(g>m@MCwY_op}? zIaYN$@}=;Z>~?WtOGsMQ+PcbL!uPp+#=dm3_z8{&M(V@0KULkXhY(hNz7+0Mx-up? z|DyYbtGS6S$!S?R?k--FT{o^%jb1wK*s9eBwzn=_Gx2W@v!XKCvNBbPsle#A0{j16 z`cxu`M=2)p*lX?62*yoN3NeXC5W4;KDo3wZc2mUl%FyjES2E!4FT`n*p5=HWQ8MH7 zJ7f?>Rg>8Co|6I-`dlR{UhOrIke?$z?CZzkevM_`s&!J=0%1 zIpT_U@x6(UU;M|_Km91pmD(S}dX@I=={RJ>rj#WujSpQadbosmWCWgWOz3mN(Bnes z*(~?kpUMrFRdr|QJQuNW=)=1)!y`*ei&8e!X1(~!6^|VG2iuAC`|~Dv)-Lr5nXkI=;qZ#at4(v>D|hKWyhfd~@T2#~ zYd%Yj9o&4nC@__-IDJU7vgtC6 zH~8%-oUFUPdGlU>pW5}gQDK|+Iy#jV=ieMtlu78973yZ3S?lLhx;{70^~_plr?LV0 zH(iScepTi@HZ(|Aoqsc%+4@4Qk6ZrctuKVXSWRx%Rh0Q>73Td`y>(mU^=wytSl(pU z^s=o3^~ZD0<`zuWFY^h0J!{8`^&h%jC<}Q#FjN0)uY#DVxAJa949#~mIJ)lM!0gzP zw_G3ESf14@54`inlHZ2Y3y#j>R~UOU{y$V0&h|mHGeNj5s>UeG8moJat+5TD#=7fc zq9+zDx!x~v+>gB+Yicx~L{-lDr1p<(^Db7T7iTS5+qcJ%UEe)9vhr~EmG5j0>=FLQ z*Nu0t{r&IxFLZ0(cz4&LmXG#$^_rP*W7PeT@7?SF-C12(a`w`C;u~y9R@}`i_07Fo ziwuVrt=PM0<-*1H9p8S=``xe=3IDu&{(j^8k5}Gi(v(fQ`Uj&P{q^&ikH5;8|Jl`X z8453DQQc#A=YkOxSW9x>rDuzqz3$ID@C}xh6w+74e6Z28`Ck2;)@*I};rsON8U0${ zo4PdhTEFiw?c`LK3TpiJ%Et{!MGbQcK1~nXQ01^KLi4Ia*{As>iiP@l-%m? zf~MpKZIJW|oOE`k)6C$&5jXPlu(i{t3X-nwn|f}LAvJjR>sfnG zg8eS+14Xu0AH*2m*uQIwX1V^?9R*vypM9euZm3UaL0#FC!@q9|-@kL`82=}I!dIR8 zo?m4!RQ)ey2K#tG#h9!RXSZ!DGxJmy>KLBkyJe-pbD`s*%m=;p?|Hf8{M46^-G5-v zo-L6E>iU1rbRXb6?XRW7y+&R9_p-D;2g6%quRa{wt;)CO+>KZFr9Am)w@0s;)9VM8 zOn7{+fAf(9<$@Ovw4U=&q-N#5eWjr+t?}ok%TFqQDyk2EGbQuO0pB*e-M*T5aF3y` z^3O$C+riwn_S;!CPeTV0#?=WH?@ z!H*xnZXfQeYQ5L+X-fI!p6<>K$u~zd|G9tIfNfzny~_Rjuj}dcu*Cg(-Khzuw(Obf zlo;!@W_|kZ!l0Xe+p14)&DVrG#f1gum*75u`1braqMg*Y{CwV^90FGREju3c!A`%U zg^mVIK#jxo+=#HjhT}f7W7ZcJMnyZ#9&B(K=ymb!D00tv!y0n*lXUQt0cUgAH@ji&x|npIl$(wgb#;PE~H{nfj9#Yi0mk zmzLrF>25K1*vTxuhySVHt(2K<-C<_G#4RE{+!j%01c^H4s>6QYV*S$oXc_oqhDSuT zHtt=Ai${L(Io>Pkvk>3aC3T}7HT#x^KI?H`s${y4Ij;EkUy_zSF;kXjo!C|JVCri@ zA1CFFy_jD*(1Tf5(mEAyO~Hn$}z zaKjgUrVLytd8S~?z2w5VQ(j8VeWi8os)CJ@RL^N?{?9+h9RAH~cEfK6mY>RabT;qS zowtKd^xd-K=#z9mU3tbWher#_tau6gbd`6JCrM_pwlohJ~B`Z+Wlmp zB@rB3oeU5r#eptFPt&WH>2(q~8AxFKc<>Kgio=r6>fXcC#fs@;b1Mqrv=r-{|7liDAtEjn^XqJgGXI~9#F+t{gSMuXYW zgrm$gV8&NDV|tBb z-$zcqEuV6fF^;jZV9dRh3Q!E7ITQ9)N@P>n?*NC@)W*@I7Q&I{xB)UOCzOkkBZ(oL z+VW9{oQx0f+H)dcI^uDfo~WKL1udrFO0W=76hXW_K?q^I=sF~bvwZ}JXoRU@mTR9i ziDo2d614LQ5(vSzS)n6>a^tfaL_t#nuQh_4x}7ITA_UuC-NtL~X=NuT*uEE3L5oYH zy@H&&ohL{k1X&1iU$Lp}4p9(#n)MnaOzL)?AadanZI8Y>`ig0a?V>>yG){}=7H&jA zle(QJC`S}*vxp7}8kbxU1x?eP)fzOZ+j)Wtgdl(3e_9PvG~1JdD#%G@YJ%rB*=dF_ zsoQyiN`xSLOmQ_x8K0{o3Yz9Rs|s2(6H)UZgAf#N9;AY29)xxCfb$?>lC$#);uway z34{@+B57=&Xs;qE;3gm$+3F^MB;qI#MjT0k;W-K^;3%BRwke6Y3WO0!(zsxbC@J77 zoXWP5M4W{b?<@qya~4v-SvZw#BZ;^Rgb}BtaVZv=dPo6xfv0a z3@P9+oXWN-iMR}e5l7Ov8itglfXQ$o+r|+w8dAV$;2hZ29AhP8lbo|yF&a)}+ejj2 z17V&bq$Npy=q3fs2A;mnC5adggb_)SAG}Ec!{Jo!yprIy1~nWABaWn%@J$IA4yUqh zBoV`bFd|9v12`pMIGoD1kwgp!!iXbjC4^G~hQq0B8%e})AdEY#T|$a3G9GlKfCk2{;a?vTY<0$AK{7 zNLmTzlz`)KD%(a9aU2LEk|aNvQv!~|scaib#Bm^uIFeR^IVIpYoXVXi2_C4Sjssyt zlH><-TEKBQm2D%5I1YpnNs=GTX#vOKRJM&I;y4gS97!v|oEC5#PG#FjB8~%LM3UqO zbMRk7;a#h>=K`F{wvj{}2f~OW$q(k>S44s8%xA^AdE1-QI#Csr&NS0Pox?I3}IGt@{iFgl$5zCSv z)8zu*!|7}rOT>F1j98ZZoGuse9!}@Zvji;-sP{k^ku0qQb%lWUa5~$@67e1gBbFsU zsVfA$htt_MmWcO27?CXbQC%TmJ)F$8kwmNq!aR*6^cBH}=$6ei+!fER)^al2#uBj} z2qTsy64kBUjd9Z2MhtNq1#4<%V&K2Q=wjow-rzsVNFBZ1a7NDH1c~FIO9;c};LKPI z8#}IDUz3708}8G_?k6vmPNj*DVOtzxz^{BWRqC_^wbmn&JvP^R2KuOU|Ke7!*nRy~ z3*z;lduy6j?G>-l$ARuJu9HfN8||X4H7hw$9WBA63b}-3X8a?S(==H3h?W}1a13Yf z|JgrMt$B=OXGCO--LTvblPlQ{CUidq#jfjGj44=8wHRaAMQU_RiD5u0i!qvATd)|D zv!{xVF*tkoZ7~ME72cSFe{PJDedL9F7RLy7zO@*m*kZRBquFPA7Gnywy~JWn$<`w} zhGXnfp2Zl>@IH%^>^+>tel$-59G5G2oN*<)>|ya)hOzwKZ~|wOF1jD+5#^mPK`D7! z5ptgPIHBb8BN!#`cqGo(o&*<@B^Q!X@Nh{vyAXkLCY8Jz!bygmO40o&AS!PRJR0PU zQEY~>_$+7`XoBG#50n*uKRMe!X7O1C_*IXVV+{Ma4BbzT zv%Q=aV}z2wAH}a{xtw?Hv!%vA9>cD=pco1qieCpxImSB=rGh^nC>j3s%V7L#ivbsiB`!np<|dq>8D5Pu z3ihrE<;;M|jz8A^+SetjVm0bS@GD0__i*gHYSsf}0rBxVDKsOnoIFVz9WRBx#%_3n z-!tmzZ`CN`)exP-{5E& z{p9(Vj2D~h?{n8Ko5}qB``-il>TbH0?Q?xNcbvNwI5XuSpXoMp!yvt~t?z7D=Ut8M zU6=Oc*c`7&Q{Jmvt*KRXV zWou+HvDCAee3~tn*}zEKtLf+ zfeXwqFa)Y9=1i`)G{KNDu{6UFGc~io5Hkmwhin2~|GAy`Zq$w@|r z78n*A8JSp2uHdqZHMMkdbuuz^HaB%NGj%gEH8pW_a<;H^GB!b7F?>TuKsRZ0Ofq2sQ>@~ delta 518 zcmbO&Hd$=L2j+TH6FWPu;*z4 Xe5Icq0uxqYg-7ZZ>|A+)T z?-yI^s$M@?y?bFr7t4>L_u?k=b)NqGkrNgoTkz-Pxt-_z#J*|hshe$^Ij^W+&gaXf zWmD`jKN~cNKP^`G{>;S$jHQCas!uL ztbvn(frY7wn~{m3v89=T5zqrBCT8YN&X$hmmWGBVb~Xf6#Bzb-II*atq9`?u%gETw MfJ;@?)!&T^0H!0JumAu6 diff --git a/test/test_graphics_context.py b/test/test_graphics_context.py index 999908f36..c2c52ce67 100644 --- a/test/test_graphics_context.py +++ b/test/test_graphics_context.py @@ -45,58 +45,58 @@ def test_change_settings(): pdf = FPDF() # verify default tgt_draw_color = pdf.DEFAULT_DRAW_COLOR - assert ( - pdf.draw_color == tgt_draw_color - ), f"pdf.draw_color ({pdf.draw_color}) != tgt_draw_color ({tgt_draw_color})" + assert pdf.draw_color == tgt_draw_color, ( + f"pdf.draw_color ({pdf.draw_color})" f" != tgt_draw_color ({tgt_draw_color})" + ) # change to red tgt_draw_color = drawing.DeviceRGB(*[c / 255 for c in red]) pdf.set_draw_color(*red) - assert ( - pdf.draw_color == tgt_draw_color - ), f"pdf.draw_color ({pdf.draw_color}) != tgt_draw_color ({tgt_draw_color})" + assert pdf.draw_color == tgt_draw_color, ( + f"pdf.draw_color ({pdf.draw_color})" f" != tgt_draw_color ({tgt_draw_color})" + ) # stays the same pdf.set_draw_color(*red) - assert ( - pdf.draw_color == tgt_draw_color - ), f"pdf.draw_color ({pdf.draw_color}) != tgt_draw_color ({tgt_draw_color})" + assert pdf.draw_color == tgt_draw_color, ( + f"pdf.draw_color ({pdf.draw_color})" f" != tgt_draw_color ({tgt_draw_color})" + ) # fill color pdf = FPDF() # verify default tgt_fill_color = pdf.DEFAULT_FILL_COLOR - assert ( - pdf.fill_color == tgt_fill_color - ), f"pdf.fill_color ({pdf.fill_color}) != tgt_fill_color ({tgt_fill_color})" + assert pdf.fill_color == tgt_fill_color, ( + f"pdf.fill_color ({pdf.fill_color})" f" != tgt_fill_color ({tgt_fill_color})" + ) # change to green tgt_fill_color = drawing.DeviceRGB(*[c / 255 for c in green]) pdf.set_fill_color(*green) - assert ( - pdf.fill_color == tgt_fill_color - ), f"pdf.fill_color ({pdf.fill_color}) != tgt_fill_color ({tgt_fill_color})" + assert pdf.fill_color == tgt_fill_color, ( + f"pdf.fill_color ({pdf.fill_color})" f" != tgt_fill_color ({tgt_fill_color})" + ) # stays the same pdf.set_fill_color(*green) - assert ( - pdf.fill_color == tgt_fill_color - ), f"pdf.fill_color ({pdf.fill_color}) != tgt_fill_color ({tgt_fill_color})" + assert pdf.fill_color == tgt_fill_color, ( + f"pdf.fill_color ({pdf.fill_color})" f" != tgt_fill_color ({tgt_fill_color})" + ) # text color pdf = FPDF() # verify default tgt_text_color = pdf.DEFAULT_TEXT_COLOR - assert ( - pdf.text_color == tgt_text_color - ), f"pdf.text_color ({pdf.text_color}) != tgt_text_color ({tgt_text_color})" + assert pdf.text_color == tgt_text_color, ( + f"pdf.text_color ({pdf.text_color})" f" != tgt_text_color ({tgt_text_color})" + ) # change to blue tgt_text_color = drawing.DeviceRGB(*[c / 255 for c in blue]) pdf.set_text_color(*blue) - assert ( - pdf.text_color == tgt_text_color - ), f"pdf.text_color ({pdf.text_color}) != tgt_text_color ({tgt_text_color})" + assert pdf.text_color == tgt_text_color, ( + f"pdf.text_color ({pdf.text_color})" f" != tgt_text_color ({tgt_text_color})" + ) # stays the same pdf.set_text_color(*blue) - assert ( - pdf.text_color == tgt_text_color - ), f"pdf.text_color ({pdf.text_color}) != tgt_text_color ({tgt_text_color})" + assert pdf.text_color == tgt_text_color, ( + f"pdf.text_color ({pdf.text_color})" f" != tgt_text_color ({tgt_text_color})" + ) # underline # no setter method for this one @@ -171,20 +171,20 @@ def test_change_settings(): pdf = FPDF() # verify default tgt_line_width = 0.567 / pdf.k - assert ( - pdf.line_width == tgt_line_width - ), f"pdf.line_width ({pdf.line_width}) != tgt_line_width ({tgt_line_width})" + assert pdf.line_width == tgt_line_width, ( + f"pdf.line_width ({pdf.line_width})" f" != tgt_line_width ({tgt_line_width})" + ) # change tgt_line_width = 0.5 pdf.set_line_width(tgt_line_width) - assert ( - pdf.line_width == tgt_line_width - ), f"pdf.line_width ({pdf.line_width}) != tgt_line_width ({tgt_line_width})" + assert pdf.line_width == tgt_line_width, ( + f"pdf.line_width ({pdf.line_width})" f" != tgt_line_width ({tgt_line_width})" + ) # stays the same pdf.set_line_width(tgt_line_width) - assert ( - pdf.line_width == tgt_line_width - ), f"pdf.line_width ({pdf.line_width}) != tgt_line_width ({tgt_line_width})" + assert pdf.line_width == tgt_line_width, ( + f"pdf.line_width ({pdf.line_width})" f" != tgt_line_width ({tgt_line_width})" + ) # text_mode # no setter method for this one @@ -198,9 +198,9 @@ def test_change_settings(): f"pdf.font_size_pt ({pdf.font_size_pt})" f" != tgt_font_size_pt ({tgt_font_size_pt})" ) - assert ( - pdf.font_size == tgt_font_size - ), f"pdf.font_size ({pdf.font_size}) != tgt_font_size ({tgt_font_size})" + assert pdf.font_size == tgt_font_size, ( + f"pdf.font_size ({pdf.font_size})" f" != tgt_font_size ({tgt_font_size})" + ) # change tgt_font_size_pt = 14 tgt_font_size = tgt_font_size_pt / pdf.k @@ -209,32 +209,32 @@ def test_change_settings(): f"pdf.font_size_pt ({pdf.font_size_pt})" f" != tgt_font_size_pt ({tgt_font_size_pt})" ) - assert ( - pdf.font_size == tgt_font_size - ), f"pdf.font_size ({pdf.font_size}) != tgt_font_size ({tgt_font_size})" + assert pdf.font_size == tgt_font_size, ( + f"pdf.font_size ({pdf.font_size})" f" != tgt_font_size ({tgt_font_size})" + ) # stays the same pdf.set_font_size(tgt_font_size_pt) assert pdf.font_size_pt == tgt_font_size_pt, ( f"pdf.font_size_pt ({pdf.font_size_pt})" f" != tgt_font_size_pt ({tgt_font_size_pt})" ) - assert ( - pdf.font_size == tgt_font_size - ), f"pdf.font_size ({pdf.font_size}) != tgt_font_size ({tgt_font_size})" + assert pdf.font_size == tgt_font_size, ( + f"pdf.font_size ({pdf.font_size})" f" != tgt_font_size ({tgt_font_size})" + ) # font pdf = FPDF() # verify default tgt_font = ("", "", 12) - assert ( - pdf.font_family == tgt_font[0] - ), f"pdf.font_family ({pdf.font_family}) != tgt_font[0] ({tgt_font[0]})" - assert ( - pdf.font_style == tgt_font[1] - ), f"pdf.font_style ({pdf.font_style}) != tgt_font[1] ({tgt_font[1]})" - assert ( - pdf.font_size_pt == tgt_font[2] - ), f"pdf.font_size_pt ({pdf.font_size_pt}) != tgt_font[2] ({tgt_font[2]})" + assert pdf.font_family == tgt_font[0], ( + f"pdf.font_family ({pdf.font_family})" f" != tgt_font[0] ({tgt_font[0]})" + ) + assert pdf.font_style == tgt_font[1], ( + f"pdf.font_style ({pdf.font_style})" f" != tgt_font[1] ({tgt_font[1]})" + ) + assert pdf.font_size_pt == tgt_font[2], ( + f"pdf.font_size_pt ({pdf.font_size_pt})" f" != tgt_font[2] ({tgt_font[2]})" + ) # change for tgt_font in ( dict(family="helvetica"), @@ -272,6 +272,50 @@ def test_change_settings(): ) +def test_vpos_properties(): + pdf = FPDF() + sub_scale = 0.8 + pdf.sub_scale = sub_scale + assert ( + pdf.sub_scale == sub_scale + ), f"pdf.sub_scale ({pdf.sub_scale}) != sub_scale ({sub_scale})" + sup_scale = 0.81 + pdf.sup_scale = sup_scale + assert ( + pdf.sup_scale == sup_scale + ), f"pdf.sup_scale ({pdf.sup_scale}) != sup_scale ({sup_scale})" + nom_scale = 0.82 + pdf.nom_scale = nom_scale + assert ( + pdf.nom_scale == nom_scale + ), f"pdf.nom_scale ({pdf.nom_scale}) != nom_scale ({nom_scale})" + denom_scale = 0.83 + pdf.denom_scale = denom_scale + assert ( + pdf.denom_scale == denom_scale + ), f"pdf.denom_scale ({pdf.denom_scale}) != denom_scale ({denom_scale})" + sub_lift = -0.2 + pdf.sub_lift = sub_lift + assert ( + pdf.sub_lift == sub_lift + ), f"pdf.sub_lift ({pdf.sub_lift}) != sub_lift ({sub_lift})" + sup_lift = 0.5 + pdf.sup_lift = sup_lift + assert ( + pdf.sup_lift == sup_lift + ), f"pdf.sup_lift ({pdf.sup_lift}) != sup_lift ({sup_lift})" + nom_lift = 0.3 + pdf.nom_lift = nom_lift + assert ( + pdf.nom_lift == nom_lift + ), f"pdf.nom_lift ({pdf.nom_lift}) != nom_lift ({nom_lift})" + denom_lift = 1 + pdf.denom_lift = denom_lift + assert ( + pdf.denom_lift == denom_lift + ), f"pdf.denom_lift ({pdf.denom_lift}) != denom_lift ({denom_lift})" + + def test_local_context_init(tmp_path): pdf = FPDF() pdf.add_page() @@ -310,11 +354,14 @@ def test_local_context_inherited_shared_props(tmp_path): pdf.set_fill_color(255, 128, 0) pdf.set_line_width(2) pdf.set_dash_pattern(dash=0.5, gap=9.5, phase=3.25) + pdf.write(txt="normal") with pdf.local_context( - fill_opacity=0.5 + fill_opacity=0.5, char_vpos="SUP" ): # => triggers creation of a local GraphicsStyle pdf.rect(x=60, y=60, w=60, h=60, style="DF") + pdf.write(txt="sup") pdf.rect(x=60, y=150, w=60, h=60, style="DF") + pdf.write(txt="normal") assert_pdf_equal(pdf, HERE / "local_context_inherited_shared_props.pdf", tmp_path) diff --git a/test/text/test_line_break.py b/test/text/test_line_break.py index bbc1b0773..780fae0e7 100644 --- a/test/text/test_line_break.py +++ b/test/text/test_line_break.py @@ -100,6 +100,11 @@ def test_fragment_properties(): assert ( frag.line_width == pdf.line_width ), f"frag.line_width ({frag.line_width}) != pdf.line_width ({pdf.line_width})" + pdf.char_vpos = "SUP" + frag = Fragment("example", pdf._get_current_graphics_state(), pdf.k) + assert ( + frag.char_vpos == pdf.char_vpos + ), f"frag.char_vpos ({frag.char_vpos}) != pdf.char_vpos ({pdf.char_vpos})" def test_no_fragments(): diff --git a/test/text/test_write.py b/test/text/test_write.py index 909b8f517..e7a1f58b8 100644 --- a/test/text/test_write.py +++ b/test/text/test_write.py @@ -80,3 +80,52 @@ def test_write_font_stretching(tmp_path): # issue #478 pdf.line(pdf.l_margin, 10, pdf.l_margin, 100) pdf.line(right_boundary, 10, right_boundary, 100) assert_pdf_equal(pdf, HERE / "write_font_stretching.pdf", tmp_path) + + +def test_write_superscript(tmp_path): + pdf = fpdf.FPDF() + pdf.add_page() + pdf.set_font("Helvetica", "", 20) + + def write_this(): + pdf.write(txt="2") + pdf.char_vpos = "SUP" + pdf.write(txt="56") + pdf.char_vpos = "LINE" + pdf.write(txt=" more line text") + pdf.char_vpos = "SUB" + pdf.write(txt="(idx)") + pdf.char_vpos = "LINE" + pdf.write(txt=" end") + pdf.ln() + pdf.ln() + pdf.write(txt="1234 + ") + pdf.char_vpos = "NOM" + pdf.write(txt="5") + pdf.char_vpos = "LINE" + pdf.write(txt="/") + pdf.char_vpos = "DENOM" + pdf.write(txt="16") + pdf.char_vpos = "LINE" + pdf.write(txt=" + 987 = x") + pdf.ln() + pdf.ln() + pdf.ln() + + write_this() + pdf.sub_scale = 0.5 + pdf.sup_scale = 0.5 + pdf.nom_scale = 0.5 + pdf.denom_scale = 0.5 + write_this() + pdf.sub_lift = 0.0 + pdf.sup_lift = 0.0 + pdf.nom_lift = 0.0 + pdf.denom_lift = 0.0 + write_this() + pdf.sub_lift = 1.0 + pdf.sup_lift = 1.0 + pdf.nom_lift = 1.0 + pdf.denom_lift = 1.0 + write_this() + assert_pdf_equal(pdf, HERE / "write_superscript.pdf", tmp_path) diff --git a/test/text/write_superscript.pdf b/test/text/write_superscript.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5a8ca313a4f4449bf9f3b44069152cf8bc17e08f GIT binary patch literal 1258 zcmY!laB@i4DM>9-(09v8EJ<}qP0mjN8t#*t zmtK;gU~FOt)Kgqil$w~!RWWDk6kGqp1_Ev0|8+?RE3Qj_ey)s7Ohw|x1f#N?30(^~ zR=CvJd3toO{iQrt>FCMN_xAP+@%n2!ZTzr#SAOE>6%N8ej=!1s-&U*+TOo7f*dl{1 z)+@e9-MaqqgK%5&wTQ{(HbNrjSP=@iX>UFa&K&u|;xxh1Z=UEYrRv4T;bG9|+&m8*VKdffL!%U!d?tykpA z2_e%-8HTG|xV(Zk>=24v((>8M;@<5)*L`a3Yo^Xla&r?BSNSO9Df9knv{70&_qMJp zJIu~y{$}-AP$vkA5NP;9qXra}7!h5}rSF}YQmhaSN(Mo(T>8%WrJ!i%()UeG$xL+0 zuTY3K07{u!n(7%@C|H;n>RDLEav>tz6x~!eV9MaqcS)B6r%NkiB~~C zB((x)zGp5ly*fc@XDICn^a& znwU8jF$)u57{k<+6eVWnq!t0o6kw!hR;2=+tRIx0U!njCC}45onU|KY019ys+eIPT z#?;Wl+0@b8&C$rj#KO(V%*e#q*wD<_$2a~0V1(&L- ItG^o;0D)?fZ~y=R literal 0 HcmV?d00001