From eac6120c98a3a9d26d3639b961e2a9077caacf69 Mon Sep 17 00:00:00 2001 From: Andy Friedman Date: Tue, 24 Oct 2023 18:56:12 -0400 Subject: [PATCH 1/6] updates to SVG conversion: defs, shapes and clipping paths (#968) * SVG tags now read before any other tags are read, shapes in tags now work, added additional installs to Development.md * clip-path and clipPath implemented in SVG conversion * ran black * todo fixed * removing leftover pdfs * updated shapes defs test, added documentation, removed extraneous files * adding example code for generating pdfs for testing * changeglog fixed --- .gitignore | 1 + CHANGELOG.md | 3 +- docs/Development.md | 19 +++++ docs/SVG.md | 2 + fpdf/svg.py | 87 +++++++++++++++------ test/svg/generated_pdf/clip_path.pdf | Bin 0 -> 1259 bytes test/svg/generated_pdf/shapes_def_test.pdf | Bin 0 -> 1443 bytes test/svg/parameters.py | 6 +- test/svg/svg_sources/clip_path.svg | 18 +++++ test/svg/svg_sources/shapes_def_test.svg | 25 ++++++ 10 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 test/svg/generated_pdf/clip_path.pdf create mode 100644 test/svg/generated_pdf/shapes_def_test.pdf create mode 100644 test/svg/svg_sources/clip_path.svg create mode 100644 test/svg/svg_sources/shapes_def_test.svg diff --git a/.gitignore b/.gitignore index a52401320..3cb145af9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # my files .env +.DS_Store # codecov.io coverage.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 308c2dd8b..4fdb1a2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ in order to get warned about deprecated features used in your code. This can also be enabled programmatically with `warnings.simplefilter('default', DeprecationWarning)`. ## [2.7.7] - Not released yet - +### Added +* SVG importing now supports clipping paths, and `defs` tags anywhere in the SVG file ## [2.7.6] - 2023-10-11 This release is the first performed from the [@py-pdf GitHub org](https://github.com/py-pdf), where `fpdf2` migrated. diff --git a/docs/Development.md b/docs/Development.md index 15755b1d5..f9ffc3c27 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -70,6 +70,8 @@ pre-commit install To run tests, `cd` into `fpdf2` repository, install the dependencies using `pip install -r test/requirements.txt`, and run `pytest`. +You may also need to install [SWIG](https://swig.org/index.html) and [Ghostscript](https://www.ghostscript.com/). + You can run a single test by executing: `pytest -k function_name`. Alternatively, you can use [Tox](https://tox.readthedocs.io/en/latest/). @@ -116,9 +118,26 @@ All generated PDF files (including those processed by `qpdf`) will be stored in last test runs will be saved and then automatically deleted, so you can check the output in case of a failed test. +### Generating PDF files for testing In order to generate a "reference" PDF file, simply call `assert_pdf_equal` once with `generate=True`. +``` +import fpdf + +svg = fpdf.svg.SVGObject.from_file("path/to/file.svg") +pdf = fpdf.FPDF(unit="pt", format=(svg.width, svg.height)) +pdf.add_page() +svg.draw_to_page(pdf) + +assert_pdf_equal( + pdf, + "path/for/pdf/output.pdf", + "path/for/pdf/", + generate=True +) +``` + ## Testing performances ### Code speed & profiling diff --git a/docs/SVG.md b/docs/SVG.md index 974655938..8e71ece42 100644 --- a/docs/SVG.md +++ b/docs/SVG.md @@ -190,6 +190,8 @@ Additionally, `cairosvg` offers various options for optimizing the rendering per - stroke & fill coloring and opacity - basic stroke styling - Inline CSS styling via `style="..."` attributes. +- clipping paths +- `defs` tags anywhere in the SVG code ## Currently Unsupported Notable SVG Features ## diff --git a/fpdf/svg.py b/fpdf/svg.py index 491ecc4c5..8f5f93ab8 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -26,6 +26,7 @@ GraphicsContext, GraphicsStyle, PaintedPath, + ClippingPath, Transform, ) @@ -329,15 +330,17 @@ class ShapeBuilder: """A namespace within which methods for converting basic shapes can be looked up.""" @staticmethod - def new_path(tag): + def new_path(tag, clipping_path: bool = False): """Create a new path with the appropriate styles.""" path = PaintedPath() + if clipping_path: + path = ClippingPath() apply_styles(path, tag) return path @classmethod - def rect(cls, tag): + def rect(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" # svg rect is wound clockwise if "x" in tag.attrib: @@ -387,25 +390,25 @@ def rect(cls, tag): if ry > (height / 2): ry = height / 2 - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) path.rectangle(x, y, width, height, rx, ry) return path @classmethod - def circle(cls, tag): + def circle(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" cx = float(tag.attrib.get("cx", 0)) cy = float(tag.attrib.get("cy", 0)) r = float(tag.attrib["r"]) - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) path.circle(cx, cy, r) return path @classmethod - def ellipse(cls, tag): + def ellipse(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" cx = float(tag.attrib.get("cx", 0)) cy = float(tag.attrib.get("cy", 0)) @@ -413,7 +416,7 @@ def ellipse(cls, tag): rx = tag.attrib.get("rx", "auto") ry = tag.attrib.get("ry", "auto") - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) if (rx == ry == "auto") or (rx == 0) or (ry == 0): return path @@ -457,11 +460,11 @@ def polyline(cls, tag): return path @classmethod - def polygon(cls, tag): + def polygon(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" points = tag.attrib["points"] - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) points = "M" + points + "Z" svg_path_converter(path, points) @@ -665,6 +668,12 @@ def __init__(self, svg_text): self.extract_shape_info(svg_tree) self.convert_graphics(svg_tree) + @force_nodocument + def update_xref(self, key, referenced): + if key: + key = "#" + key if not key.startswith("#") else key + self.cross_references[key] = referenced + @force_nodocument def extract_shape_info(self, root_tag): """Collect shape info from the given SVG.""" @@ -859,7 +868,15 @@ def handle_defs(self, defs): self.build_group(child) if child.tag in xmlns_lookup("svg", "path"): self.build_path(child) - # We could/should also support that are rect, circle, ellipse, line, polyline, polygon... + elif child.tag in shape_tags: + self.build_shape(child) + if child.tag in xmlns_lookup("svg", "clipPath"): + try: + clip_id = child.attrib["id"] + except KeyError: + clip_id = None + for child_ in child: + self.build_clipping_path(child_, clip_id) # this assumes xrefs only reference already-defined ids. # I don't know if this is required by the SVG spec. @@ -869,7 +886,7 @@ def build_xref(self, xref): pdf_group = GraphicsContext() apply_styles(pdf_group, xref) - for candidate in xmlns_lookup("xlink", "href"): + for candidate in xmlns_lookup("xlink", "href", "id"): try: ref = xref.attrib[candidate] break @@ -901,22 +918,23 @@ def build_group(self, group, pdf_group=None): pdf_group = GraphicsContext() apply_styles(pdf_group, group) + # handle defs before anything else + for child in [ + child for child in group if child.tag in xmlns_lookup("svg", "defs") + ]: + self.handle_defs(child) + for child in group: - if child.tag in xmlns_lookup("svg", "defs"): - self.handle_defs(child) if child.tag in xmlns_lookup("svg", "g"): pdf_group.add_item(self.build_group(child)) if child.tag in xmlns_lookup("svg", "path"): pdf_group.add_item(self.build_path(child)) elif child.tag in shape_tags: - pdf_group.add_item(getattr(ShapeBuilder, shape_tags[child.tag])(child)) + pdf_group.add_item(self.build_shape(child)) if child.tag in xmlns_lookup("svg", "use"): pdf_group.add_item(self.build_xref(child)) - try: - self.cross_references["#" + group.attrib["id"]] = pdf_group - except KeyError: - pass + self.update_xref(group.attrib.get("id"), pdf_group) return pdf_group @@ -925,15 +943,38 @@ def build_path(self, path): """Convert an SVG tag into a PDF path object.""" pdf_path = PaintedPath() apply_styles(pdf_path, path) + self.apply_clipping_path(pdf_path, path) - svg_path = path.attrib.get("d", None) + svg_path = path.attrib.get("d") if svg_path is not None: svg_path_converter(pdf_path, svg_path) - try: - self.cross_references["#" + path.attrib["id"]] = pdf_path - except KeyError: - pass + self.update_xref(path.attrib.get("id"), pdf_path) return pdf_path + + @force_nodocument + def build_shape(self, shape): + """Convert an SVG shape tag into a PDF path object. Necessary to make xref (because ShapeBuilder doesn't have access to this object.)""" + shape_path = getattr(ShapeBuilder, shape_tags[shape.tag])(shape) + self.apply_clipping_path(shape_path, shape) + + self.update_xref(shape.attrib.get("id"), shape_path) + + return shape_path + + @force_nodocument + def build_clipping_path(self, shape, clip_id): + clipping_path_shape = getattr(ShapeBuilder, shape_tags[shape.tag])(shape, True) + + self.update_xref(clip_id, clipping_path_shape) + + return clipping_path_shape + + @force_nodocument + def apply_clipping_path(self, stylable, svg_element): + clipping_path = svg_element.attrib.get("clip-path") + if clipping_path: + clipping_path_id = re.search(r"url\((\#\w+)\)", clipping_path) + stylable.clipping_path = self.cross_references[clipping_path_id[1]] diff --git a/test/svg/generated_pdf/clip_path.pdf b/test/svg/generated_pdf/clip_path.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50db007360e6c7d60b5cb22a3ebd38648a88144e GIT binary patch literal 1259 zcmah}PiWIn92VRd3)>DNqDRPQPV1p_?2(&^jM+(r?h*HuE7_d1o8U z5u|1D8308$%`&lplrg4Q9-M(4&$zb0&s(2P&h0(?t*bn@trMR*5NUoDyK{45C3R07 z9zA;E``?S(uf$P%%kTMJO}$4}yq~9McEgApKYjjd@rQk?B{Q;g`|G|pBfSq|3vWj! zZ(#3Z+}h>s=?@l{zfA7Qwnw7uQrE*B_nTjTyc-x_>|VXpR_=R|D^Fb;-#Ytb;rteH zx^?0mTx~C#m*q#xS0{`P+u-E;b00c=FH1|d+3WLvc6J7usJu1sHKASW%*kLYs}LyS zP#hEPgteb|>q{a0H})8CSYpLZVmZ4@5E)hmi9`}`NlPZxX-$iDEOZY-aSl+pWS>HD z2q>D$s{_zxv5NMFo{A$35-hnNgb*r{r3{Y|v3<3tzxE6#E%Hm2mVP2F-F2Bv0PL6h zp(IGq!9yr<Jw%?|jRu?BX~BXGMa?0j@=uAY7ZByf#&jJMg)1`;YZ)ET zp^^EAiG&_E^vXd&kesrUcX3Wpa5+TL3msfgboc@?Kd^a3MGq4z2CfWL;zY(~3q*s= EKZ4_EKL7v# literal 0 HcmV?d00001 diff --git a/test/svg/generated_pdf/shapes_def_test.pdf b/test/svg/generated_pdf/shapes_def_test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8465dc062206a621a41302e367fe3afd589a68cd GIT binary patch literal 1443 zcmah}YiJx*6sBo>Fx0jX{h?yKkuI%hcjnGyXLsrn-JRK&X`6I6KI*!`$?Q!!WiqqO z+)X#4*lM9aLW@=esZpXfViPfFC@C}qOT{N`)%vGUq+rC-qKKeHXwY+aH{EdsbN|fD zch5cNJKz1z1bVgPW*qF~FcOe6u$zlUIbLX-_$`tWEHc=g`JZ#rs4>5Y#}^VH>Y>isnUw34ODGzxGpv2` zImGw5y6u&8m)OQA;#EL2GGZDen~FmNzz23fhFX>^@tm@25DzJSDvY#`2P?VEBWYTc zxX`1eQ=$>WDH4v~O6(!J3yBelo${zl^kJ?tUdW!bPYY`4a50_yu=M!T@An@&d0*erRr}{=_idYa@VT?=$-@hmT4ug!Pp12~w~6~E zS^|HbXn*a=6Q}lQi}5d7yI1d+AG3}>(EI42JICJmtaWP3bcZ$n*51c%Ie7l3KfW?r z_Plxb_|fT)W`4i?_riQ)lE-%DgSj8ZCwE*BC*SR#N!>Iyy>{2^?YsA0*!=p7FCX~$ zSV&rTcJ!HZiGk4i_+Pgkv^s7ZIg=gyQSV+jb?t4ded<@EwD{8V8;(zoT{&z{EFC!W zUF*gCFW0(?JI~*H`rM%f=A*hwbw`D6NDMbv^<1JtQ#lGEgS;y2HryxwZ|oCD^u>l( zh{gWIkrjf4U`P&gyoU1m0ul`xigg$5bF(Gp2}gws8?S_#F$lICB*7x ztLAhaBS$F8^Dw|51gXH(WQM_w@%mG(KQjfNKcR_6c@@mZrcndw&?apQR%8W>62^)E z>nW^k6oifU`%YI}GRO%q@*E4OEhL9RQV0#A1_mv%h3p2VKs!wsR{k3&L8j(7Spm*X zXTuTAp zswxS$l7TypDsn6)b!u@n5lf0ODG^q+u%^bvq$;c7h^lt298tX`kLoV1s*D9$;R1m~ HPm=oww@9{Q literal 0 HcmV?d00001 diff --git a/test/svg/parameters.py b/test/svg/parameters.py index ebcc7c1f7..ea8914203 100644 --- a/test/svg/parameters.py +++ b/test/svg/parameters.py @@ -759,13 +759,17 @@ def Gs(**kwargs): pytest.param(svgfile("SVG_logo.svg"), id="SVG logo from wikipedia"), pytest.param(svgfile("viewbox.svg"), id="weird viewbox"), pytest.param(svgfile("search.svg"), id="search icon"), # issue 356 - # discovered while investigatin issue 358: + # discovered while investigating issue 358: pytest.param(svgfile("issue_358b.svg"), id="repeated relative move"), pytest.param(svgfile("issue_358.svg"), id="arc start & initial point"), # issue 358 pytest.param(svgfile("Ghostscript_colorcircle.svg"), id="ghostscript colorcircle"), pytest.param(svgfile("Ghostscript_escher.svg"), id="ghostscript escher"), pytest.param(svgfile("use-xlink-href.svg"), id="use xlink:href - issue #446"), pytest.param(svgfile("rgb-color-issue-480.svg"), id="rgb() color - issue #480"), + pytest.param( + svgfile("shapes_def_test.svg"), id="shapes defined in 'defs' tag - issue #858" + ), + pytest.param(svgfile("clip_path.svg"), id="clip path - issue #858"), ) svg_path_edge_cases = ( diff --git a/test/svg/svg_sources/clip_path.svg b/test/svg/svg_sources/clip_path.svg new file mode 100644 index 000000000..92ed421ae --- /dev/null +++ b/test/svg/svg_sources/clip_path.svg @@ -0,0 +1,18 @@ + + + Example clip_path - green circle clipped by rectangular clipping path + + + + + + + + + + + + diff --git a/test/svg/svg_sources/shapes_def_test.svg b/test/svg/svg_sources/shapes_def_test.svg new file mode 100644 index 000000000..3681189c1 --- /dev/null +++ b/test/svg/svg_sources/shapes_def_test.svg @@ -0,0 +1,25 @@ + + + Example circle01 - circle filled with red and stroked with blue + + + + + + + + + + + + + + + + From e23fdcc68d6d3439a7fa2cd45974fe46c752a7f9 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 25 Oct 2023 13:05:54 +0200 Subject: [PATCH 2/6] Updating Unicode.md (#975) * Update Unicode.md * font pack details * Build-in fonts vs. unicode fonts * adding and using fonts * review results * Delete tutorial/thai-accent-working.png * Delete tutorial/thai-accent-error.png --- docs/Unicode.md | 122 +++++++++++++++++++------------ docs/add-unicode-font.png | Bin 0 -> 4287 bytes mkdocs.yml | 2 +- tutorial/thai-accent-error.png | Bin 9003 -> 0 bytes tutorial/thai-accent-working.png | Bin 85314 -> 0 bytes 5 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 docs/add-unicode-font.png delete mode 100644 tutorial/thai-accent-error.png delete mode 100644 tutorial/thai-accent-working.png diff --git a/docs/Unicode.md b/docs/Unicode.md index 6b18ac868..46732db3a 100644 --- a/docs/Unicode.md +++ b/docs/Unicode.md @@ -1,59 +1,93 @@ -# Unicode # +# Fonts and Unicode # -The FPDF class was modified adding UTF-8 support. -Moreover, it embeds only the necessary parts of the fonts that are used in the -document, making the file size much smaller than if the whole fonts were -embedded. These features were originally developed for the -[mPDF](http://mpdf.bpm1.com/) project, and ported from -[Ian Back](mailto:ian@bpm1.com?subject=sFPDF)'s -[sFPDF](http://www.fpdf.org/en/script/script91.php) LGPL PHP version. +Besides the limited set of latin fonts built into the PDF format, Fpdf2 offers full support for using and embedding Unicode (TrueType "ttf" and OpenType "otf") fonts. To keep the output file size small, it only embeds the subset of each font that is actually used in the document. This part of the code has been completely rewritten since the fork from PyFPDF. It uses the [fonttools](https://fonttools.readthedocs.io/en/latest/) library for parsing the font data, and [harfbuzz](https://harfbuzz.github.io/) (via [uharfbuzz](https://github.com/harfbuzz/uharfbuzz)) for [text shaping](TextShaping.html). -Before you can use UTF-8, you have to install at least one Unicode font in the -font directory (or system font folder). Some free font packages are available -for download (extract them into the font folder): +To make use of that functionality, you have to install at least one Unicode font, either in the system font folder or in some other location accessible to your program. +For professional work, many designers prefer commercial fonts, suitable to their specific needs. There are also many sources of free TTF fonts that can be downloaded online and used free of cost (some of them may have restrictions on commercial redistribution, such as server installations or including them in a software project). - * [DejaVu](http://dejavu-fonts.org/) family: Sans, Sans Condensed, Serif, -Serif Condensed, Sans Mono (Supports more than 200 languages) + * [Font Library](https://fontlibrary.org/) - A collection of fonts for many languates with an open source type license. + + * [Google Fonts](https://fonts.google.com/) - A collection of free to use fonts for many languages. + + * [Microsoft Font Library](https://learn.microsoft.com/en-gb/typography/font-list/) - A large collection of fonts that are free to use. + + * [GitHub: Fonts](https://github.com/topics/fonts) - Links to public repositories of open source font projects as well as font related software projects. * [GNU FreeFont](http://www.gnu.org/software/freefont/) family: FreeSans, FreeSerif, FreeMono - * [Indic](http://en.wikipedia.org/wiki/Help:Multilingual_support_(Indic)) -(ttf-indic-fonts Debian and Ubuntu package) for Bengali, Devanagari, Gujarati, -Gurmukhi (including the variants for Punjabi), Kannada, Malayalam, Oriya, -Tamil, Telugu, Tibetan +To use a Unicode font in your program, use the [`add_font()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.add_font), and then the [`set_font()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font) method calls. + + +### Built-in Fonts vs. Unicode Fonts ### + +The PDF file format knows a small number of "standard" fonts, namely "courier", "helvetica", "times", "symbol", and "zapfdingbats". The first three are available in regular, bold, italic, and bold-italic versions. Any PDF processor (eg. a viewer) must provide those fonts for display. To use them, you don't need to call `.add_font()`, but only `.set_font()`. + +While that may seem convenient, there's a big drawback. Those fonts only support latin characters, or a set of special characters for the last two. If you try to render any Unicode character outside of those ranges, then you'll get an error like: "`Character "θ" at index 13 in text is outside the range of characters supported by the font used: "courier". Please consider using a Unicode font.`". +So if you want to create documents with any characters other than those common in English and a small number of european languages, then you need to add a Unicode font containing the respective glyph as described in this document. + +Note that even if you have a font eg. named "Courier" installed as a system font on your computer, by default this will not be used. You'll have to explicitly call eg. `.add_font("courier2", "", r"C:\Windows\Fonts\cour.ttf")` to make it available. If the name is really the same (ignoring case), then you'll have to use a suitable variation, since trying to overwrite one of the "standard" names with `.add_font()` will result in an error. + + +### Adding and Using Fonts ### + +Before using a Unicode font, you need to load it from a font file. Usually you'll have call `add_font()` for each style of the same font family you want to use. The styles that fpdf2 understands are: - * [AR PL New Sung](http://www.study-area.org/apt/firefly-font/) (firefly): -The Open Source Chinese Font (also supports other east Asian languages) +* Regular: "" +* Bold: "b" +* Italic/Oblique: "i" +* Bold-Italic: "bi" - * [Alee](https://wiki.archlinux.org/index.php/Fonts) (ttf-alee Arch Linux -package): General purpose Hangul Truetype fonts that contain Korean syllable -and Latin9 (iso8859-15) characters. +Note that we use the same family name for each of them, but load them from different files. Only when a font has variants (eg. "narrow"), or there are more styles than the four standard ones (eg. "black" or "extra light"), you'll have to add those with a different family name. If the font files are not located in the current directory, you'll have to provide a file name with a relative or absolute path. + +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.add_page() +# Different styles of the same font family. +pdf.add_font("dejavu-sans", style="", fname="DejaVuSans.ttf") +pdf.add_font("dejavu-sans", style="b", fname="DejaVuSans-Bold.ttf") +pdf.add_font("dejavu-sans", style="i", fname="DejaVuSans-Oblique.ttf") +pdf.add_font("dejavu-sans", style="bi", fname="DejaVuSans-BoldOblique.ttf") +# Different type of the same font design. +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. + +```python +# Set and use first family in regular style. +pdf.set_font(family="dejavu-sans", style="", size=12) +pdf.cell(text="Hello") +# Set and use the same family in bold style. +pdf.set_font(style="b", size=18) # still uses the same dejavu-sans font family. +pdf.cell(text="Fat World") +# Set and use a variant in italic and underlined. +pdf.set_font(family="dejavu-sans-narrow", style="iu", size=12) +pdf.cell(text="lean on me") +``` - * [Fonts-TLWG](http://linux.thai.net/projects/fonts-tlwg/) (formerly -ThaiFonts-Scalable) +![add-unicode-font](add-unicode-font.png) -These fonts are included with this library's installers; see -[Free Font Pack for FPDF](#free-font-pack-and-copyright-restrictions) below for -more information. -Then, to use a Unicode font in your script, pass `True` as the fourth parameter -of [`add_font`](fpdf/fpdf.html#fpdf.fpdf.FPDF.add_font). +### Note on non-latin languages ### -### Notes on non-latin languages +Many non-latin writing systems have complex ways to combine characters, ligatures, and possibly multiple diacritic symbols together, change the shape of characters depending on its location in a word, or use a different writing direction. A small number of examples are: -Some users may encounter a problem where some characters displayed incorrectly. For example, using Thai language in the picture below +* Hebrew - right-to-left, placement of diacritics +* Arabic - right-to-left, contextual shapes +* Thai - stacked diacritics +* Devanagari (and other indic scripts) - multi-character ligatures, reordering -![thai-font-problem](https://raw.githubusercontent.com/py-pdf/fpdf2/master/tutorial/thai-accent-error.png) +To make sure those scripts to be rendered correctly, [text shaping](TextShaping.html) must be enabled with `.set_text_shaping(True)`. -The solution is to find and use a font that covers the characters of your language. -From the error in the image above, Thai characters can be fixed using fonts from [Fonts-TLWG](http://linux.thai.net/projects/fonts-tlwg/) which can be downloaded from -[this link](https://linux.thai.net/pub/thailinux/software/fonts-tlwg/fonts/). The example shown below. -![thai-font-working](https://raw.githubusercontent.com/py-pdf/fpdf2/master/tutorial/thai-accent-working.png) +### Right-to-Left & Arabic Script workaround ### -### Right-to-Left & Arabic Script workaround -For Arabic and RTL scripts there is a temporary solution (using two additional libraries `python-bidi` and `arabic-reshaper`) that works for most languages; only a few (rare) Arabic characters aren't supported. Using it on other scripts(eg. when the input is unknown or mixed scripts) does not affect them: +Arabic, Hebrew and other scripts written right-to-left (RTL) should work correctly when text is added that only contains one script at a time. As of release 2.7.6, more complete support for mixing RTL and LTR text is being worked on. +In the mean time, there is a temporary solution for Arabic and other RTL scripts using two additional libraries `python-bidi` and `arabic-reshaper`. It works for most languages; only a few (rare) Arabic characters aren't supported. Using it on other scripts (eg. when the input is unknown or mixed scripts) does not affect them: ```python from arabic_reshaper import reshape from bidi.algorithm import get_display @@ -140,17 +174,11 @@ pdf.output("unicode.pdf") View the result here: [unicode.pdf](https://github.com/py-pdf/fpdf2/raw/master/tutorial/unicode.pdf) -## Free Font Pack and Copyright Restrictions ## +## Free Font Pack ## -For your convenience, this library collected 96 TTF files in an optional -["Free Unicode TrueType Font Pack for FPDF"](https://github.com/reingart/pyfpdf/releases/download/binary/fpdf_unicode_font_pack.zip), -with useful fonts commonly distributed with GNU/Linux operating systems (see -above for a complete description). This pack is included in the Windows -installers, or can be downloaded separately (for any operating system). +For your convenience, the author of the original PyFPDF has collected 96 TTF files in an optional +["Free Unicode TrueType Font Pack for FPDF"](https://github.com/reingart/pyfpdf/releases/download/binary/fpdf_unicode_font_pack.zip), with useful fonts commonly distributed with GNU/Linux operating systems. Note that this collection is from 2015, so it will not contain any newer fonts or possible updates. -You could use any TTF font file as long embedding usage is allowed in the licence. -If not, a runtime exception will be raised saying: "ERROR - Font file -filename.ttf cannot be embedded due to copyright restrictions." # Fallback fonts # diff --git a/docs/add-unicode-font.png b/docs/add-unicode-font.png new file mode 100644 index 0000000000000000000000000000000000000000..ac07ffdc32c518fe06b2c58d7aa13c608b068fcd GIT binary patch literal 4287 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 F{vYj>>rVgx literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 134604e75..f8e7ce4bc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -131,7 +131,7 @@ nav: - 'Line breaks': 'LineBreaks.md' - 'Page breaks': 'PageBreaks.md' - 'Text styling': 'TextStyling.md' - - 'Unicode': 'Unicode.md' + - 'Fonts and Unicode': 'Unicode.md' - 'Text Shaping': 'TextShaping.md' - 'Emojis, Symbols & Dingbats': 'EmojisSymbolsDingbats.md' - 'HTML': 'HTML.md' diff --git a/tutorial/thai-accent-error.png b/tutorial/thai-accent-error.png deleted file mode 100644 index 23166db9de370ae5897d427106df40d490d88c6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9003 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 From da2eda9f07914e8517239c8cd1f63a140be05a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Alberto=20Mu=C3=B1oz=20Mesa?= <48921408+jmunoz94@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:54:18 -0500 Subject: [PATCH 3/6] Update Tutorial-es.md (#982) Update sections 4 and 5 of the Spanish tutorial. --- docs/Tutorial-es.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/Tutorial-es.md b/docs/Tutorial-es.md index 943e9df62..e813a98d1 100644 --- a/docs/Tutorial-es.md +++ b/docs/Tutorial-es.md @@ -155,16 +155,16 @@ Este ejemplo es una variante del anterior, mostrando cómo poner el texto en mú [Texto de Julio Verne](https://github.com/py-pdf/fpdf2/raw/master/tutorial/20k_c1.txt) -_⚠️ This section has changed a lot and requires a new translation: _ - -English versions: - -* [Tuto 4 - Multi Columns](https://py-pdf.github.io/fpdf2/Tutorial.html#tuto-4-multi-columns) -* [Documentation on TextColumns](https://py-pdf.github.io/fpdf2/TextColumns.html +La diferencia clave respecto al tutorial anterior es el uso del +método [`text_columns`](fpdf/fpdf.html#fpdf.fpdf.FPDF.text_column). +Este recoge todo el texto, posiblemente en incrementos, y lo distribuye entre el número de columnas solicitadas, insertando automáticamente saltos de página según sea necesario. Nota que mientras la instancia de `TextColumns` está activa como gestor de contexto, los estilos de texto y otras propiedades de la fuente pueden cambiarse. Estos cambios estarán contenidos en el contexto. Una vez se cierre, la configuración previa será reestablecida. ## Tutorial 5 - Creando tablas ## +Este tutorial explicará cómo crear dos tablas diferentes, + para demostrar lo que se puede lograr con algunos ajustes simples. + ```python {% include "../tutorial/tuto5.py" %} ``` @@ -172,12 +172,13 @@ English versions: [PDF resultante](https://github.com/py-pdf/fpdf2/raw/master/tutorial/tuto5.pdf) - [Archivo de texto con países](https://github.com/py-pdf/fpdf2/raw/master/tutorial/countries.txt) -_⚠️ This section has changed a lot and requires a new translation: _ - -English versions: +El primer ejemplo es alcanzado de la forma más básica posible, alimentando datos a [`FPDF.table()`](https://py-pdf.github.io/fpdf2/Tables.html). El resultado es rudimentario pero muy rápido de obtener. -* [Tuto 5 - Creating Tables](https://py-pdf.github.io/fpdf2/Tutorial.html#tuto-5-creating-tables) -* [Documentation on tables](https://py-pdf.github.io/fpdf2/Tables.html) +La segunda tabla trae algunas mejoras: colores, ancho de tabla limitado, altura de línea reducida, + títulos centrados, columnas con anchos personalizados, figuras alineadas a la derecha... + Aún más, las líneas horizontales han sido removidas. + Esto se hizo escogiendo un `borders_layout` entre los valores disponibles: + [`TableBordersLayout`](https://py-pdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.TableBordersLayout). ## Tutorial 6 - Creando enlaces y combinando estilos de texto ## From 40d64d4db32d4afd0cdb9e6e1d38cf06e6930e2f Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:37:51 +0100 Subject: [PATCH 4/6] Bumping ensure_exec_time_below() thresholds to stabilize the GitHub Actions pipelines --- test/test_perfs.py | 2 +- test/text/test_cell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_perfs.py b/test/test_perfs.py index d34e57410..5bc9755b3 100644 --- a/test/test_perfs.py +++ b/test/test_perfs.py @@ -7,7 +7,7 @@ HERE = Path(__file__).resolve().parent -@ensure_exec_time_below(seconds=8) +@ensure_exec_time_below(seconds=9) @ensure_rss_memory_below(mib=8) def test_intense_image_rendering(): png_file_paths = [] diff --git a/test/text/test_cell.py b/test/text/test_cell.py index 3d69b18f8..fb6a1d66b 100644 --- a/test/text/test_cell.py +++ b/test/text/test_cell.py @@ -319,7 +319,7 @@ def test_cell_deprecated_txt_arg(): pdf.cell(txt="Lorem ipsum Ut nostrud irure") -@ensure_exec_time_below(seconds=22) +@ensure_exec_time_below(seconds=24) @ensure_rss_memory_below(mib=1) def test_cell_speed_with_long_text(): # issue #907 pdf = FPDF() From fb9fd9cf4dafcbccd107ada58ef85fcab8876285 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Wed, 1 Nov 2023 00:39:24 +0100 Subject: [PATCH 5/6] Images flowing within TextColumns (#973) * first attempt at text regions * dynamic line break width * fix some existing tests * TextCollectorMixin + Paragraph * small fixes * basic TextRegion working * Regions with Paragraphs, Fragments with align instead of justify * Columns docu and FPDF integration * paragraph docs * formatting * Delete .TextRegion.md.swo * Allow initial text argument for text regions * column bottom balancing * text regions with ln() and line_height; tuto4 in en+de * remove instrumentation from tuto4 * html via text regions first round * write_html via text regions all except tables * review feedback & additional tests * more text regions documentation * formatting * remove text_column() * change html.py and tests to text_columns() * Apply suggestions from code review Co-authored-by: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> * Review feedback and other fixes. * ImageParagraph for TextColumns * rebase fixes * update changelog * file name case * remove test residue * test files update * text_columns img and img_fill_width parameters * minor fixes to Unicode.md * Back to using 72 dpi for SVG (appears to be software dependent) * Update TextRegion.md * changes based on review * Apply suggestions from code review Co-authored-by: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> * Update fpdf.py --------- Co-authored-by: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> --- CHANGELOG.md | 4 + docs/TextColumns.md | 15 +- docs/TextRegion.md | 24 +- docs/Unicode.md | 5 +- fpdf/fpdf.py | 240 +++++++++++-------- fpdf/image_parsing.py | 56 ++++- fpdf/line_break.py | 13 +- fpdf/text_region.py | 253 +++++++++++++++++--- test/image/svg_image_alt_text_title.pdf | Bin 0 -> 2930 bytes test/image/svg_image_alt_text_two_pages.pdf | Bin 0 -> 3779 bytes test/image/svg_image_fit_rect.pdf | Bin 0 -> 2122 bytes test/image/test_vector_image.py | 74 ++++-- test/text_region/tcols_3cols.pdf | Bin 5669 -> 7122 bytes test/text_region/tcols_images.pdf | Bin 0 -> 11824 bytes test/text_region/test_text_columns.py | 147 +++++++++++- 15 files changed, 657 insertions(+), 174 deletions(-) create mode 100644 test/image/svg_image_alt_text_title.pdf create mode 100644 test/image/svg_image_alt_text_two_pages.pdf create mode 100644 test/image/svg_image_fit_rect.pdf create mode 100644 test/text_region/tcols_images.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fdb1a2fc..d77c6b318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.7.7] - Not released yet ### Added * SVG importing now supports clipping paths, and `defs` tags anywhere in the SVG file +* [`TextColumns()`](https://py-pdf.github.io/fpdf2/TextColumns.html) can now have images inserted (both raster and vector). +* [`TextColumns()`](https://py-pdf.github.io/fpdf2/TextColumns.html) can now advance to the next column with the new `new_column()` method or a FORM_FEED character (`\u000c`) in the text. +### Fixed +* Inserted Vector images used to ignore the `keep_aspect_ratio` argument. ## [2.7.6] - 2023-10-11 This release is the first performed from the [@py-pdf GitHub org](https://github.com/py-pdf), where `fpdf2` migrated. diff --git a/docs/TextColumns.md b/docs/TextColumns.md index 722db44a7..e41a512b1 100644 --- a/docs/TextColumns.md +++ b/docs/TextColumns.md @@ -7,12 +7,25 @@ _New in [:octicons-tag-24: 2.7.6](https://github.com/py-pdf/fpdf2/blob/master/CH The `FPDF.text_columns()` method allows to create columnar layouts, with one or several columns. Columns will always be of equal width. +### Parameters ### + Beyond the parameters common to all text regions, the following are available for text columns: * l_margin (float, optional) - override the current left page margin. * r_margin (float, optional) - override the current right page margin. * ncols (float, optional) - the number of columns to generate (Default: 2). * gutter (float, optional) - the horizontal space required between each two columns (Default 10). +* balance (bool, optional) - Create height balanced columns, starting at the current height and ending at approximately the same level. + +### Methods ### + +Text columns support all the standard text region methods like `.paragraph()`, `.write()`, `.ln()`, and `.render()`. In addition to that: + +* `.new_column()` - End the current column and continue at the top of the next one. + +A FORM_FEED character (`\u000c`) in the text will have the same effect as an explicit call to `.new_column()`, + +Note that when used within balanced columns, switching to a new column manually will result in incorrect balancing. #### Single-Column Example #### @@ -85,7 +98,7 @@ with cols: ``` ![Balanced Columns](tcols-balanced.png) -Note that column balancing only works reliably when the font size (specifically the line height) doesn't change. If parts of the text use a larger or smaller font than the rest, then the balancing will usually be out of whack. Contributions for a more refined balancing algorithm are welcome. +Note that column balancing only works reliably when the font size (specifically the line height) doesn't change, and if there are no images included. If parts of the text use a larger or smaller font than the rest, then the balancing will usually be out of whack. Contributions for a more refined balancing algorithm are welcome. ### Possible future extensions diff --git a/docs/TextRegion.md b/docs/TextRegion.md index 0f8701149..db4e28ba0 100644 --- a/docs/TextRegion.md +++ b/docs/TextRegion.md @@ -44,12 +44,12 @@ The horizontal start position will be either at the current x position, if that In both horizontal and vertical positioning, regions with multiple columns may follow additional rules and restrictions. -### Interaction between Regions ### +### Interaction Between Regions ### Several region instances can exist at the same time. But only one of them can act as context manager at any given time. It is not currently possible to activate them recursively. But it is possible to use them intermittingly. This will probably most often make sense between a columnar region and a table or a graphic. You may have some running text ending at a given height, then insert a table/graphic, and finally continue the running text at the new height below the table within the existing column(s). -### Common parameters ### +### Common Parameters ### All types of text regions have the following constructor parameters in common: @@ -59,14 +59,17 @@ All types of text regions have the following constructor parameters in common: * print_sh (bool, optional) - Treat a soft-hyphen (\\u00ad) as a printable character, instead of a line breaking opportunity. (Default: False) * skip_leading_spaces (default: False) - This flag is primarily used by `write_html()`, but may also have other uses. It removes all space characters at the beginning of each line. * wrapmode (default "WORD") - +* image (str or PIL.Image.Image or io.BytesIO, optional) - An image to add to the region. This is a convenience parameter for cases when no further text or images need to be added to the paragraph. If both "text" and "image" arguments are present, the text will be inserted first. (Default: None) +* image_fill_width (bool, optional) - Indicates whether to increase the size of the image to fill the width of the column. Larger images will always be reduced to column width. (Default: False) All of those values can be overriden for each individual paragraph. -### Common methods ### +### Common Methods ### -* `.paragraph()` [see characteristics parameters below] - establish a new paragraph in the text. The text added to this paragraph will start on a new line. +* `.paragraph()` [see characteristic parameters below] - establish a new paragraph in the text. The text added to this paragraph will start on a new line. * `.write(text: str, link: = None)` - write text to the region. This is only permitted when no explicit paragraph is currently active. +* `.image()` [see characteristic parameters below] - insert a vector or raster image in the region, flowing with the text like a paragraph. * `.ln(h: float = None)` - Start a new line moving either by the current font height or by the parameter "h". Only permitted when no explicit paragraph is currently active. * `.render()` - if the region is not used as a context manager with "with", this method must be called to actually process the added text. @@ -87,9 +90,20 @@ For more typographical control, you can use the following arguments. Most of tho Other than text regions, paragraphs should always be used as context managers and never be reused. Violating those rules may result in the entered text turning up on the page out of sequence. -### Possible future extensions +### Possible Future Extensions ### Those features are currently not supported, but Pull Requests are welcome to implement them: * per-paragraph indentation * first-line indentation + + +## Images ## + +_New in [:octicons-tag-24: 2.7.7](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ + +Most arguments for inserting images into text regions are the same as for the `FPDF.image()` method, and have the same or equivalent meaning. +Since the image will be placed automatically, the "x" and "y" parameters are not available. The positioning can be controlled with "align", where the default is "LEFT", with the alternatives "RIGHT" and "CENTER". +If neither width nor height are specified, the image will be inserted with the size resulting from the PDF default resolution of 72 dpi. If the "fill_width" parameter is set to True, it increases the size to fill the full column width if necessary. If the image is wider than the column width, it will always be reduced in size proportionally. +The "top_margin" and "bottom_margin" parameters have the same effect as with text paragraphs. + diff --git a/docs/Unicode.md b/docs/Unicode.md index 46732db3a..14cb4384d 100644 --- a/docs/Unicode.md +++ b/docs/Unicode.md @@ -38,7 +38,7 @@ Before using a Unicode font, you need to load it from a font file. Usually you'l * Italic/Oblique: "i" * Bold-Italic: "bi" -Note that we use the same family name for each of them, but load them from different files. Only when a font has variants (eg. "narrow"), or there are more styles than the four standard ones (eg. "black" or "extra light"), you'll have to add those with a different family name. If the font files are not located in the current directory, you'll have to provide a file name with a relative or absolute path. +Note that we use the same family name for each of them, but load them from different files. Only when a font has variants (eg. "narrow"), or there are more styles than the four standard ones (eg. "black" or "extra light"), you'll have to add those with a different family name. If the font files are not located in the current directory, you'll have to provide a file name with a relative or absolute path. If the font is not found elsewhere, then fpdf2 will look for it in a subdirectory named "font". ```python from fpdf import FPDF @@ -109,6 +109,7 @@ from fpdf import FPDF pdf = FPDF() pdf.add_page() +pdf.set_text_shaping(True) # Add a DejaVu Unicode font (uses UTF-8) # Supports more than 200 languages. For a coverage status see: @@ -180,7 +181,7 @@ For your convenience, the author of the original PyFPDF has collected 96 TTF fil ["Free Unicode TrueType Font Pack for FPDF"](https://github.com/reingart/pyfpdf/releases/download/binary/fpdf_unicode_font_pack.zip), with useful fonts commonly distributed with GNU/Linux operating systems. Note that this collection is from 2015, so it will not contain any newer fonts or possible updates. -# Fallback fonts # +## Fallback fonts ## _New in [:octicons-tag-24: 2.7.0](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_ diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 2ceb429a8..99c68729d 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -79,7 +79,15 @@ class Image: from .fonts import CoreFont, CORE_FONTS, FontFace, TTFFont from .graphics_state import GraphicsStateMixin from .html import HTML2FPDF -from .image_parsing import SUPPORTED_IMAGE_FILTERS, get_img_info, load_image +from .image_parsing import ( + SUPPORTED_IMAGE_FILTERS, + get_img_info, + get_svg_info, + load_image, + ImageInfo, + RasterImageInfo, + VectorImageInfo, +) from .linearization import LinearizedOutputProducer from .line_break import Fragment, MultiLineBreak, TextLine from .outline import OutlineSection @@ -116,34 +124,6 @@ class Image: } -class ImageInfo(dict): - "Information about a raster image used in the PDF document" - - @property - def width(self): - "Intrinsic image width" - return self["w"] - - @property - def height(self): - "Intrinsic image height" - return self["h"] - - @property - def rendered_width(self): - "Only available if the image has been placed on the document" - return self["rendered_width"] - - @property - def rendered_height(self): - "Only available if the image has been placed on the document" - return self["rendered_height"] - - def __str__(self): - d = {k: ("..." if k in ("data", "smask") else v) for k, v in self.items()} - return f"ImageInfo({d})" - - class TitleStyle(FontFace): def __init__( self, @@ -3761,6 +3741,8 @@ def write( def text_columns( self, text: Optional[str] = None, + img: Optional[str] = None, + img_fill_width: bool = False, ncols: int = 1, gutter: float = 10, balance: bool = False, @@ -3795,6 +3777,8 @@ def text_columns( return TextColumns( self, text=text, + img=img, + img_fill_width=img_fill_width, ncols=ncols, gutter=gutter, balance=balance, @@ -3867,7 +3851,7 @@ def image( while preserving its original aspect ratio. Defaults to False. Only meaningful if both `w` & `h` are provided. - Returns: an instance of `ImageInfo` + Returns: an instance of a subclass of `ImageInfo`. """ if type: warnings.warn( @@ -3878,77 +3862,15 @@ def image( DeprecationWarning, stacklevel=get_stack_level(), ) - if str(name).endswith(".svg"): - # Insert it as a PDF path: - img = load_image(str(name)) - return self._vector_image(img, x, y, w, h, link, title, alt_text) - if isinstance(name, bytes) and _is_svg(name.strip()): + + name, img, info = self.preload_image(name, dims) + if isinstance(info, VectorImageInfo): return self._vector_image( - io.BytesIO(name), x, y, w, h, link, title, alt_text + img, info, x, y, w, h, link, title, alt_text, keep_aspect_ratio ) - if isinstance(name, io.BytesIO) and _is_svg(name.getvalue().strip()): - return self._vector_image(name, x, y, w, h, link, title, alt_text) - name, img, info = self.preload_image(name, dims) - if "smask" in info: - self._set_min_pdf_version("1.4") - - # Automatic width and height calculation if needed - if w == 0 and h == 0: # Put image at 72 dpi - w = info["w"] / self.k - h = info["h"] / self.k - elif w == 0: - w = h * info["w"] / info["h"] - elif h == 0: - h = w * info["h"] / info["w"] - - if self.oversized_images and info["usages"] == 1 and not dims: - info = self._downscale_image(name, img, info, w, h) - - # Flowing mode - if y is None: - self._perform_page_break_if_need_be(h) - y = self.y - self.y += h - if x is None: - x = self.x - - if keep_aspect_ratio: - ratio = info.width / info.height - if h * ratio < w: - x += (w - h * ratio) / 2 - w = h * ratio - else: # => too wide, limiting width: - y += (h - w / ratio) / 2 - h = w / ratio - - if not isinstance(x, Number): - if keep_aspect_ratio: - raise ValueError( - "FPDF.image(): 'keep_aspect_ratio' cannot be used with an enum value provided to `x`" - ) - x = Align.coerce(x) - if x == Align.C: - x = (self.w - w) / 2 - elif x == Align.R: - x = self.w - w - self.r_margin - elif x == Align.L: - x = self.l_margin - else: - raise ValueError(f"Unsupported 'x' value passed to .image(): {x}") - - stream_content = ( - f"q {w * self.k:.2f} 0 0 {h * self.k:.2f} {x * self.k:.2f} " - f"{(self.h - y - h) * self.k:.2f} cm /I{info['i']} Do Q" + return self._raster_image( + name, img, info, x, y, w, h, link, title, alt_text, dims, keep_aspect_ratio ) - if title or alt_text: - with self._marked_sequence(title=title, alt_text=alt_text): - self._out(stream_content) - else: - self._out(stream_content) - if link: - self.link(x, y, w, h, link) - - return ImageInfo(**info, rendered_width=w, rendered_height=h) def preload_image(self, name, dims=None): """ @@ -3963,8 +3885,17 @@ def preload_image(self, name, dims=None): dims (Tuple[float]): optional dimensions as a tuple (width, height) to resize the image before storing it in the PDF. - Returns: an instance of `ImageInfo` + Returns: an instance of a subclass of `ImageInfo` """ + # Identify and load SVG data. + if str(name).endswith(".svg"): + return get_svg_info(name, load_image(str(name))) + if isinstance(name, bytes) and _is_svg(name.strip()): + return get_svg_info(name, io.BytesIO(name)) + if isinstance(name, io.BytesIO) and _is_svg(name.getvalue().strip()): + return get_svg_info("vector_image", name) + + # Load raster data. if isinstance(name, str): img = None elif isinstance(name, Image): @@ -3984,7 +3915,7 @@ def preload_image(self, name, dims=None): if info: info["usages"] += 1 else: - info = ImageInfo(get_img_info(name, img, self.image_filter, dims)) + info = get_img_info(name, img, self.image_filter, dims) info["i"] = len(self.images) + 1 info["usages"] = 1 info["iccp_i"] = None @@ -4004,9 +3935,99 @@ def preload_image(self, name, dims=None): self.images[name] = info return name, img, info + @staticmethod + def _limit_to_aspect_ratio(x, y, w, h, aspect_w, aspect_h): + """ + Make an image fit within a bounding box, maintaining its proportions. + In the reduced dimension it will be centered within tha available space. + """ + ratio = aspect_w / aspect_h + if h * ratio < w: + new_w = h * ratio + new_h = h + x += (w - new_w) / 2 + else: # => too wide, limiting width: + new_h = w / ratio + new_w = w + y += (h - new_h) / 2 + return x, y, new_w, new_h + + def _raster_image( + self, + name, + img, + info: RasterImageInfo, + x=None, + y=None, + w=0, + h=0, + link="", + title=None, + alt_text=None, + dims=None, + keep_aspect_ratio=False, + ): + if "smask" in info: + self._set_min_pdf_version("1.4") + + # Automatic width and height calculation if needed + if w == 0 and h == 0: # Put image at 72 dpi + w = info["w"] / self.k + h = info["h"] / self.k + elif w == 0: + w = h * info["w"] / info["h"] + elif h == 0: + h = w * info["h"] / info["w"] + + if self.oversized_images and info["usages"] == 1 and not dims: + info = self._downscale_image(name, img, info, w, h) + + # Flowing mode + if y is None: + self._perform_page_break_if_need_be(h) + y = self.y + self.y += h + if x is None: + x = self.x + + if keep_aspect_ratio: + x, y, w, h = self._limit_to_aspect_ratio( + x, y, w, h, info.width, info.height + ) + + if not isinstance(x, Number): + if keep_aspect_ratio: + raise ValueError( + "FPDF.image(): 'keep_aspect_ratio' cannot be used with an enum value provided to `x`" + ) + x = Align.coerce(x) + if x == Align.C: + x = (self.w - w) / 2 + elif x == Align.R: + x = self.w - w - self.r_margin + elif x == Align.L: + x = self.l_margin + else: + raise ValueError(f"Unsupported 'x' value passed to .image(): {x}") + + stream_content = ( + f"q {w * self.k:.2f} 0 0 {h * self.k:.2f} {x * self.k:.2f} " + f"{(self.h - y - h) * self.k:.2f} cm /I{info['i']} Do Q" + ) + if title or alt_text: + with self._marked_sequence(title=title, alt_text=alt_text): + self._out(stream_content) + else: + self._out(stream_content) + if link: + self.link(x, y, w, h, link) + + return RasterImageInfo(**info, rendered_width=w, rendered_height=h) + def _vector_image( self, - img: io.BytesIO, + svg: SVGObject, + info: VectorImageInfo, x=None, y=None, w=0, @@ -4014,8 +4035,8 @@ def _vector_image( link="", title=None, alt_text=None, + keep_aspect_ratio=False, ): - svg = SVGObject(img.getvalue()) if not svg.viewbox and svg.width and svg.height: warnings.warn( ' has no "viewBox", using its "width" & "height" as default "viewBox"', @@ -4062,6 +4083,11 @@ def _vector_image( if x is None: x = self.x + if keep_aspect_ratio: + x, y, w, h = self._limit_to_aspect_ratio( + x, y, w, h, info.width, info.height + ) + _, _, path = svg.transform_to_rect_viewport( scale=1, width=w, height=h, ignore_svg_top_attrs=True ) @@ -4071,6 +4097,8 @@ def _vector_image( try: self.set_xy(0, 0) if title or alt_text: + # Alt text of vector graphics does NOT show as tool-tip in viewers, but should + # be processed by screen readers. with self._marked_sequence(title=title, alt_text=alt_text): self.draw_path(path) else: @@ -4080,7 +4108,7 @@ def _vector_image( if link: self.link(x, y, w, h, link) - return ImageInfo(rendered_width=w, rendered_height=h) + return VectorImageInfo(rendered_width=w, rendered_height=h) def _downscale_image(self, name, img, info, w, h): width_in_pt, height_in_pt = w * self.k, h * self.k @@ -4130,7 +4158,7 @@ def _downscale_image(self, name, img, info, w, h): ) info["usages"] += 1 else: - info = ImageInfo( + info = RasterImageInfo( get_img_info( name, img or load_image(name), self.image_filter, dims ) @@ -4458,6 +4486,7 @@ def _out(self, s): self.pages[self.page].contents += s + b"\n" @check_page + @support_deprecated_txt_arg def interleaved2of5(self, text, x, y, w=1, h=10): """Barcode I2of5 (numeric), adds a 0 if odd length""" narrow = w / 3 @@ -4514,6 +4543,7 @@ def interleaved2of5(self, text, x, y, w=1, h=10): x += line_width @check_page + @support_deprecated_txt_arg def code39(self, text, x, y, w=1.5, h=5): """Barcode 3of9""" dim = {"w": w, "n": w / 3} @@ -4964,6 +4994,8 @@ def _is_svg(bytes): "YPos", "get_page_format", "ImageInfo", + "RasterImageInfo", + "VectorImageInfo", "TextMode", "TitleStyle", "PAGE_FORMATS", diff --git a/fpdf/image_parsing.py b/fpdf/image_parsing.py index d79b4bb2f..7fdec3bf2 100644 --- a/fpdf/image_parsing.py +++ b/fpdf/image_parsing.py @@ -19,6 +19,7 @@ except ImportError: Image = None +from .svg import SVGObject from .errors import FPDFException @@ -57,6 +58,45 @@ # fmt: on +class ImageInfo(dict): + """Information about an image used in the PDF document (base class). + We subclass this to distinguish between raster and vector images.""" + + @property + def width(self): + "Intrinsic image width" + return self["w"] + + @property + def height(self): + "Intrinsic image height" + return self["h"] + + @property + def rendered_width(self): + "Only available if the image has been placed on the document" + return self["rendered_width"] + + @property + def rendered_height(self): + "Only available if the image has been placed on the document" + return self["rendered_height"] + + def __str__(self): + d = {k: ("..." if k in ("data", "smask") else v) for k, v in self.items()} + return f"self.__class__.__name__({d})" + + +class RasterImageInfo(ImageInfo): + "Information about a raster image used in the PDF document" + # pass + + +class VectorImageInfo(ImageInfo): + "Information about a vector image used in the PDF document" + # pass + + def load_image(filename): """ This method is used to load external resources, such as images. @@ -105,6 +145,20 @@ def is_iccp_valid(iccp, filename): return True +def get_svg_info(filename, img): + svg = SVGObject(img.getvalue()) + if svg.viewbox: + _, _, w, h = svg.viewbox + else: + w = h = 0.0 + if svg.width: + w = svg.width + if svg.height: + h = svg.height + info = VectorImageInfo(data=svg, w=w, h=h) + return filename, svg, info + + def get_img_info(filename, img=None, image_filter="AUTO", dims=None): """ Args: @@ -153,7 +207,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None): img_altered = True w, h = img.size - info = {} + info = RasterImageInfo() iccp = None if "icc_profile" in img.info: diff --git a/fpdf/line_break.py b/fpdf/line_break.py index bbab8eb42..b317efc87 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -20,6 +20,7 @@ SPACE = " " NBSP = "\u00a0" NEWLINE = "\n" +FORM_FEED = "\u000c" class Fragment: @@ -321,6 +322,7 @@ class TextLine(NamedTuple): height: float max_width: float trailing_nl: bool = False + trailing_form_feed: bool = False class SpaceHint(NamedTuple): @@ -460,7 +462,9 @@ def _apply_automatic_hint(self, break_hint: Union[SpaceHint, HyphenHint]): self.number_of_spaces = break_hint.number_of_spaces self.width = break_hint.line_width - def manual_break(self, align: Align, trailing_nl: bool = False): + def manual_break( + self, align: Align, trailing_nl: bool = False, trailing_form_feed: bool = False + ): return TextLine( fragments=self.fragments, text_width=self.width, @@ -469,6 +473,7 @@ def manual_break(self, align: Align, trailing_nl: bool = False): height=self.height, max_width=self.max_width, trailing_nl=trailing_nl, + trailing_form_feed=trailing_form_feed, ) def automatic_break_possible(self): @@ -610,12 +615,14 @@ def get_line(self): ) first_char = False - if character == NEWLINE: + if character in (NEWLINE, FORM_FEED): self.character_index += 1 if not current_line.fragments: current_line.height = current_font_height * self.line_height return current_line.manual_break( - Align.L if self.align == Align.J else self.align, trailing_nl=True + Align.L if self.align == Align.J else self.align, + trailing_nl=character == NEWLINE, + trailing_form_feed=character == FORM_FEED, ) if current_line.width + character_width > max_width: if character == SPACE: # must come first, always drop a current space. diff --git a/fpdf/text_region.py b/fpdf/text_region.py index e8e73a4d3..439d94d0a 100644 --- a/fpdf/text_region.py +++ b/fpdf/text_region.py @@ -3,7 +3,8 @@ from .errors import FPDFException from .enums import Align, XPos, YPos, WrapMode -from .line_break import MultiLineBreak +from .image_parsing import VectorImageInfo +from .line_break import MultiLineBreak, FORM_FEED # Since Python doesn't have "friend classes"... # pylint: disable=protected-access @@ -62,6 +63,10 @@ def __init__( self.pdf = region.pdf if text_align: text_align = Align.coerce(text_align) + if text_align not in (Align.L, Align.C, Align.R, Align.J): + raise ValueError( + f"Text_align must be 'LEFT', 'CENTER', 'RIGHT', or 'JUSTIFY', not '{text_align.value}'." + ) self.text_align = text_align if line_height is None: self.line_height = region.line_height @@ -131,6 +136,105 @@ def build_lines(self, print_sh) -> List[LineWrapper]: return text_lines +class ImageParagraph: + def __init__( + self, + region, + name, + align=None, + width: float = None, + height: float = None, + fill_width: bool = False, + keep_aspect_ratio=False, + top_margin=0, + bottom_margin=0, + link=None, + title=None, + alt_text=None, + ): + self.region = region + self.name = name + if align: + align = Align.coerce(align) + if align not in (Align.L, Align.C, Align.R): + raise ValueError( + f"Align must be 'LEFT', 'CENTER', or 'RIGHT', not '{align.value}'." + ) + self.align = align + self.width = width + self.height = height + self.fill_width = fill_width + self.keep_aspect_ratio = keep_aspect_ratio + self.top_margin = top_margin + self.bottom_margin = bottom_margin + self.link = link + self.title = title + self.alt_text = alt_text + self.img = self.info = None + + 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 = self.region.pdf.preload_image(self.name, None) + return self + + def render(self, col_left, col_width, max_height): + if not self.img: + raise RuntimeError( + "ImageParagraph.build_line() must be called before render()." + ) + is_svg = isinstance(self.info, VectorImageInfo) + if self.height: + h = self.height + else: + native_h = self.info["h"] / self.region.pdf.k + if self.width: + w = self.width + else: + native_w = self.info["w"] / self.region.pdf.k + if native_w > col_width or self.fill_width: + w = col_width + else: + w = native_w + if not self.height: + h = w * native_h / native_w + if h > max_height: + return None + x = col_left + if self.align: + if self.align == Align.R: + x += col_width - w + elif self.align == Align.C: + x += (col_width - w) / 2 + if is_svg: + return self.region.pdf._vector_image( + svg=self.img, + info=self.info, + x=x, + y=None, + w=w, + h=h, + link=self.link, + title=self.title, + alt_text=self.alt_text, + keep_aspect_ratio=self.keep_aspect_ratio, + ) + return self.region.pdf._raster_image( + name=self.name, + img=self.img, + info=self.info, + x=x, + y=None, + w=w, + h=h, + link=self.link, + title=self.title, + alt_text=self.alt_text, + dims=None, + keep_aspect_ratio=self.keep_aspect_ratio, + ) + + class ParagraphCollectorMixin: def __init__( self, @@ -142,10 +246,16 @@ def __init__( print_sh: bool = False, skip_leading_spaces: bool = False, wrapmode: WrapMode = None, + img=None, + img_fill_width=False, **kwargs, ): self.pdf = pdf self.text_align = Align.coerce(text_align) # default for auto paragraphs + if self.text_align not in (Align.L, Align.C, Align.R, Align.J): + raise ValueError( + f"Text_align must be 'LEFT', 'CENTER', 'RIGHT', or 'JUSTIFY', not '{self.text_align.value}'." + ) self.line_height = line_height self.print_sh = print_sh self.wrapmode = WrapMode.coerce(wrapmode) @@ -155,6 +265,8 @@ def __init__( super().__init__(pdf, *args, **kwargs) if text: self.write(text) + if img: + self.image(img, fill_width=img_fill_width) def __enter__(self): if self.pdf.is_current_text_region(self): @@ -225,6 +337,40 @@ def end_paragraph(self): # self._paragraphs[-1].write("\n") self._active_paragraph = None + def image( + self, + name, + align=None, + width: float = None, + height: float = None, + fill_width: bool = False, + keep_aspect_ratio=False, + top_margin=0, + bottom_margin=0, + link=None, + title=None, + alt_text=None, + ): + if self._active_paragraph == "EXPLICIT": + raise FPDFException("Unable to nest paragraphs.") + if self._active_paragraph: + self.end_paragraph() + p = ImageParagraph( + self, + name, + align=align, + width=width, + height=height, + fill_width=fill_width, + keep_aspect_ratio=keep_aspect_ratio, + top_margin=top_margin, + bottom_margin=bottom_margin, + link=link, + title=title, + alt_text=alt_text, + ) + self._paragraphs.append(p) + class TextRegion(ParagraphCollectorMixin): """Abstract base class for all text region subclasses.""" @@ -239,6 +385,19 @@ def current_x_extents(self, y, height): """ raise NotImplementedError() + def _render_image_paragraph(self, paragraph): + if paragraph.top_margin and self.pdf.y > self.pdf.t_margin: + self.pdf.y += paragraph.top_margin + col_left, col_right = self.current_x_extents(self.pdf.y, 0) + bottom = self.pdf.h - self.pdf.b_margin + max_height = bottom - self.pdf.y + rendered = paragraph.render(col_left, col_right - col_left, max_height) + if rendered: + margin = paragraph.bottom_margin + if margin and (self.pdf.y + margin) < bottom: + self.pdf.y += margin + return rendered + def _render_column_lines(self, text_lines, top, bottom): if not text_lines: return 0 # no rendered height @@ -247,42 +406,50 @@ def _render_column_lines(self, text_lines, top, bottom): last_line_height = None rendered_lines = 0 for tl_wrapper in text_lines: - text_line = tl_wrapper.line - text_rendered = False - for frag in text_line.fragments: - if frag.characters: - text_rendered = True + if isinstance(tl_wrapper, ImageParagraph): + if self._render_image_paragraph(tl_wrapper): + rendered_lines += 1 + else: # not enough room for image break - if ( - text_rendered - and tl_wrapper.first_line - and tl_wrapper.paragraph.top_margin - and self.pdf.y > self.pdf.t_margin - ): - self.pdf.y += tl_wrapper.paragraph.top_margin else: - if self.pdf.y + text_line.height > bottom: - last_line_height = prev_line_height + text_line = tl_wrapper.line + text_rendered = False + for frag in text_line.fragments: + if frag.characters: + text_rendered = True + break + if ( + text_rendered + and tl_wrapper.first_line + and tl_wrapper.paragraph.top_margin + and self.pdf.y > self.pdf.t_margin + ): + self.pdf.y += tl_wrapper.paragraph.top_margin + else: + if self.pdf.y + text_line.height > bottom: + last_line_height = prev_line_height + break + prev_line_height = last_line_height + last_line_height = text_line.height + col_left, col_right = self.current_x_extents(self.pdf.y, 0) + if self.pdf.x < col_left or self.pdf.x >= col_right: + self.pdf.x = col_left + # Don't check the return, we never render past the bottom here. + self.pdf._render_styled_text_line( + text_line, + h=text_line.height, + border=0, + new_x=XPos.LEFT, + new_y=YPos.NEXT, + fill=False, + ) + if tl_wrapper.last_line: + margin = tl_wrapper.paragraph.bottom_margin + if margin and text_rendered and (self.pdf.y + margin) < bottom: + self.pdf.y += tl_wrapper.paragraph.bottom_margin + rendered_lines += 1 + if text_line.trailing_form_feed: # column break break - prev_line_height = last_line_height - last_line_height = text_line.height - col_left, col_right = self.current_x_extents(self.pdf.y, 0) - if self.pdf.x < col_left or self.pdf.x >= col_right: - self.pdf.x = col_left - # Don't check the return, we never render past the bottom here. - self.pdf._render_styled_text_line( - text_line, - h=text_line.height, - border=0, - new_x=XPos.LEFT, - new_y=YPos.NEXT, - fill=False, - ) - if tl_wrapper.last_line: - margin = tl_wrapper.paragraph.bottom_margin - if margin and text_rendered and (self.pdf.y + margin) < bottom: - self.pdf.y += tl_wrapper.paragraph.bottom_margin - rendered_lines += 1 if rendered_lines: del text_lines[:rendered_lines] return last_line_height @@ -295,10 +462,14 @@ def _render_lines(self, text_lines, top, bottom): def collect_lines(self): text_lines = [] for paragraph in self._paragraphs: - cur_lines = paragraph.build_lines(self.print_sh) - if not cur_lines: - continue - text_lines.extend(cur_lines) + if isinstance(paragraph, ImageParagraph): + line = paragraph.build_line() + text_lines.append(line) + else: + cur_lines = paragraph.build_lines(self.print_sh) + if not cur_lines: + continue + text_lines.extend(cur_lines) return text_lines def render(self): @@ -366,6 +537,12 @@ def __enter__(self): self.pdf.x = self._cols[self._cur_column].left return self + def new_column(self): + if self._paragraphs: + self._paragraphs[-1].write(FORM_FEED) + else: + self.write(FORM_FEED) + def _render_page_lines(self, text_lines, top, bottom): """Rendering a set of lines in one or several columns on one page.""" balancing = False diff --git a/test/image/svg_image_alt_text_title.pdf b/test/image/svg_image_alt_text_title.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6414c6993090615ccc91748937c9495faac6baf GIT binary patch literal 2930 zcmcImX;f2Z8lD2j<+>mR1f0tGR1Dz4U9xbaEJ7AQ!XlwX$be?K387gaxsia>u~-DE zV?`{b5YZl6sa>cZi&DXbQl*YU@ldr2Ep$|B$59Vj^mHtAzMG%~IA!L~+#fez-tS$W z_ulV)p1U+zC|-$&a1b0|z^KSU!om<#V6+%$fFo$UR%He6ErIkk;tw4rB z3zyFgAxJ=S@DP#@cT)>;DS#%+HIx~Nh(IWVib+Q}rA?5VUX3%TjUX%xW!97m&?XB7 zozbLR$`GZbwMN5eIe?0_bSyAfbUK)u$tsZ-7%enFlMIx=sI%w|4zn3B$Wsth3Uevx zR1-y|7>y7;gAtk?umo~it}|+!fH6&t^=qIh18oKzM@)>#Xvt+zI)WX4Q7J%EO>%=d zS8k#V$^w82phZY+(kiKxm}sVpWM&7Jc4SGR%tnhzNtprPVQ?UHIGF+V3>(v&6EFtD zGhkw^j;2iTS?J_6C8U%_6@{P@%AldMVS_lvgqdj*CD$WX+r5kqgBL#g^poI+F16=> zeJeHZfXF*G)@xT;H|WR0z2>$=Y*J|Qn{S_&i`VzuzZI`&AM^?IeDzd8{%zf|fhSjn zrkl;?;gQGp_T_z+_3pL&6KP53dlo2ob0bUB8$5J+bphwvll{fNj$AAnDZa9yqcU&4 zDdbe7PwVBrJ9j^P(4EbVJX4!f`TDI(2M6agwV1B&uZkzPd~otshCSnV1h^K|2kbhYS1 z{&X_v#=lXLB9Qu5%+o4;<=dSlYJyBVczUcd;bHZUSA5V43cLjS3vyRpL6z!9< z!T0GGulca8+PZh{H|1V0*-+kaZC{9H$8zvi8$aO2-cLT=edXuqWpmHAe(fNM~)9; ztz>22lH(Omo0~fabE3bxygZ!vNcB#M`czW!;?7g*JqyYA_Vu3|^cxYYCx z*mHYY_1e-iz6~zxWZw0GM;a4<@LL(}t7MsHAiHt5n>Utx^ZrO#Q$^$2&X4bmBD#mUX50Z($uwQY!eRcZ)8x zmhFhE8a{ZTAz!(_fyI;kW2U-LoUaq_81`V<8XrVedt-I1%!yni5=f198 zJ%gB+K$d&EuFxqru8_WDDqeqAk#Q_)ah=#h(i6p=v+(7pO^Cnd{u1vvtq8mK=+igf z2Y#i4wtIcp%7S?v;V-Gadtv_3@}95%QlaRH(`>uhJK($X>6?ebE4?Gz%@&)#)IH7k zLth<>{IQQk9%GS1FR%5q{qCAxSlY|s!kf3eroWenfj7VOLQ-F|Al$Y|RLvr}t%qkO zCgkQ6OFeCy<8%N0*WYF(4$rJV9k1Q~=nRNQE8uBEE1DNe*f!ZfEZeqvNxXY~g7jc% zcf*YC+4X9Y+jiJ5u_HHUyV=LDGt#3q`6NTJqr*Q+w%q5`0jbJP$092oBc&mEK-v(% z-7#Y3P3fBk*klNXkvyk}(5WJna)5=9T&NvU_Jva%IaQn_4Z0HuDg-iFCcrYC;uzL^ zMugA%U|2au3e^X0LUxal;(+PIz9awx(Y|B@rfQ0vlubsZl%jzQg;EEf3uT2O5~qix z8$J3f82ydQbfRIqempB54`jHV)$LlukUXO^0B6(<)^j}T@dD(W%2`{PRCYl{aEIF9 z_Ewn0&oztD6{FIkhjqp$7wIUyb7E(8O|6JueI zlWZgo2XaCUl0O*>pPUp+VC+e~5jbbEAHji#Y(V{GTA?DrP@L`wV?=V z|3n;}Ui&90KFT6-kch%?HYb!#2r!XQM1~3kq<|YO;*kOz7jXF^o{)(6?;iGfGt+Vt SZGXE2hs{HlE)^w-k^caVSk0RN literal 0 HcmV?d00001 diff --git a/test/image/svg_image_alt_text_two_pages.pdf b/test/image/svg_image_alt_text_two_pages.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a0c649b5f8360846ac7856693d2506ca8bf1380 GIT binary patch literal 3779 zcmeHKYgiLk8m_XGc8YdexwVD%WXn}8Gm~T>qfr-qEsTNigDUGAVttLtJwhzGB~9}2>v$* zLFZ6rt1+2z0Rsjl1WCk86+j`!;}s;F&2$%00L?I3C!hGEI=6_jCz&`aWA_CGXo{8l1+Y758v5qFrW@UilLP2nJEdZ1r_Jsa zI{=SghoE}s%fw}}6s2Pr4&Z!bDE7iq895`(SVX}1)E;DQ=O{bp0CKNQD$6*Fc$8ja z?_X38&`j27cN7^}%5HK4R0Sn!s@Q6xbg9XF78(2u4DI!zqa2KrHBk`Z2#8)S2f&8G z@!5%x@gRngrC4c>V&P)YMvhWbCdN!5XgX!La0Nh-i1Stsj-`w?#8v&({QY(V9(d+% z@X$EfyM5zV1!~uB^B*t&DvR5v`SZE_qUkpV(`N6Wk6ilK_|=p0BD9ekT&OKB6_9J@9cIles?z8ci=Ufi@yzcSAm%Co-^aQL;b{k&QHTdt@+!T$Zujo}K zgs#yC;rk94nMZLRB1F&2j%JJ8cx8{GG7O-o74 zO~ZWIiaa?ROH`T5obf2 zxh<R`q<*hdkO z>v~im@c8Qy2e&}lW`k7n`)!M!nj4o` zDlygPt`ABtYXhM%xiS6bi;wl0Ccf3vUa`slmEC`ce4#hoIko(m$xX3-OUv+GGkz7k z@z%sQCOzF9oNc=nDf5r^e0Zz8A}4q6&_50xpH^jVIa3kpd9iJ6S){cqEbX85Ho3Gq z>dM9=k#}e09C4NYlwA5&YJO$a@4CD1m}3V{7l!n%oOC^sdE;1h;w*CVf=s+rHM{w_ zMbjc~>sp>QGd&fxj}_MJo3bDsJ3P4`lP|&pulH^0+Hr5q^4~O1KKfzxy^LM%IC=Z# z2_HWlJy0KtL|^OL$6k`Ml+U`uo!fjxv+=e$`*1_|ij%Lh2SWQBmz6)U)n8IK z_|B)(Zm!h^)aAxRPFG!auKX*ObvP}^!-mTXs((AXSXWT(els&$=MbIqV!a^xKG@7=ery7u0F zqWaPBM82>2D$dv8`0hAdt}4dYND_X`MuLAwBjG^|BYW6L9yXGPjpYBjktE(PS4cDj zGNo~Gsd^5c{P4_I!Py72O7XqA^MOnek70@gkp=mE7A7OW;uQqse@VhXWQX%y&)Y@5 z4c2_959y*B7z{Z8%MlxjdFA~~cvh9F#{i!!WM`>*9PqgaLn+`v3PTwX;_3ue#WE&6 z#Q_5f87ssC#aqXYst-2T#l0T;-uKqh&X#uG(=`v{+RxkoZc zu`4lII~FU4wcvT#F*QyvXY zd~p20K;D(%x$^%K=)snUTMyD${sN$e`yIrpr{QD*iA!ZTPGay`fRiy878560Ll;Zs zBN)IH2(RFMKmtL?2#^mR$l#vhA7H;P!!dX_7$uVu5cw#X92;Yc%ZM?yIDx?h8)++* z%g5MCNqFi$?iV5MymgIV{CddgNFvGK_?!I8sk0#YFfx z)@Y?ER=6qZtxIA3C=gV*ZTT)+!tDdFFrD5Ng1|8Cg#F;xcIhfkZ`V=B6P30dOyJ`Tzg` literal 0 HcmV?d00001 diff --git a/test/image/svg_image_fit_rect.pdf b/test/image/svg_image_fit_rect.pdf new file mode 100644 index 0000000000000000000000000000000000000000..07dbb424a07365df5e5765af634d1f3557a4007b GIT binary patch literal 2122 zcmah~X;c$e6h19IKnItiXtg4~qKF&GOg3gTf|8J+gv9^?2E>pI2!;$!CKxmxTxwB7 zl)8}$6}6`x+o~Xl2#6qH1uI&uSa-2*6;}`|)t0^qD5;)f-;bF$_ulVrZ{Bx3LZvb< zj3YoWfC4H$84-&Su7on`8Gs?&z$C2!sQ6H&j77LXq&7+IN11>Mg%TklDharu>O|6j`1l~CUdy`U*|-f!CH2!aOcJH<^$EDLBqjjpjXE84z(N)3bjrv8 zZit?gP ZzDH~TYE5-5?9X-OJVDffc~z@(>|=4QgG$)Zzod)yKc(|`PR!f$W1Rg8r2h?bnF?+IL~Z~ z8XUUt-UMB>efIF`ZoBQ<=hkZaXS;T%l*Eo!rxYEn88yn+;l*>i>Q@a{GwLSd&z2rA zuUvVH*|ITuzs9jTV`Y{uR~}d6efIf|l#HvK*7l5?oR$G4jpsV@XYH1r@0#+;DX!Hr za$aOW%QeB-~xK`k{GiFo7bEi(-@xdW$~WB(r)6il#tn||hot?oi4Gga+j2i5pf>!3 z6TR@JCVoI#&W)*eLMsNGv!q-ZuZft zgN2^Q=6F8NN*k>x{)+ghe_sE(sz1l@_GGw^((ax4WJ0EIxNh#CAe5}j%v)o=ziPz{ z0e^pfMRtsZvoMC9_-Xv)q49aGF2ne9^bIE@w{|MkQ?<>U4C~i-}O?{ZxnGShHUP8oZCut+eKA+ zuVDGD_xAQOo$$@ohsD-O=NHCU`bRS2B{w^AsNfVFu+z~-SPmN zqIJs$Y*eL{LupDAMlwLfg>we>g)~8OpOFGd)35g}>3z?LwV_dr9WbxMOCU$GLqiHX z3C2k$V}yvnczg^aP`Kt`geQu6j<-RZXfgpo0fr&0LC*prp^z^G37}5~OJo<&_sIxY z&Ra5!c-M{(J-uaz@r984|BJ==1Z>1xGJ)V-nGk&!D-^*VyoD9PU*e67q18z`lC}=) zu%yK#TQ65hQ4C;bEo9h|ilY c&lol9;" ) ) + + +def test_svg_image_fit_rect(tmp_path): + """ + Scale the image to fill the rectangle, keeping its aspect ratio, + and ensure it does overflow the rectangle width or height in the process. + """ + pdf = fpdf.FPDF() + pdf.add_page() + test_file = SVG_SRCDIR / "SVG_logo.svg" + + rect1 = 30, 30, 60, 100 + pdf.rect(*rect1) + pdf.image(test_file, *rect1, keep_aspect_ratio=True) + + rect2 = 100, 30, 100, 60 + pdf.rect(*rect2) + pdf.image(test_file, *rect2, keep_aspect_ratio=True) + + assert_pdf_equal(pdf, HERE / "svg_image_fit_rect.pdf", tmp_path) + + +IMG_DESCRIPTION = "The Mighty SVG Logo" + + +def test_svg_image_alt_text_title(tmp_path): + test_file = SVG_SRCDIR / "SVG_logo.svg" + pdf = fpdf.FPDF() + pdf.add_page() + pdf.image(test_file, alt_text=IMG_DESCRIPTION, w=50, h=50) + pdf.image(test_file, title=IMG_DESCRIPTION, w=50, h=50) + pdf.image(test_file, alt_text=IMG_DESCRIPTION, title=IMG_DESCRIPTION, w=50, h=50) + assert_pdf_equal(pdf, HERE / "svg_image_alt_text_title.pdf", tmp_path) + + +def test_svg_image_alt_text_two_pages(tmp_path): + test_file = SVG_SRCDIR / "SVG_logo.svg" + pdf = fpdf.FPDF() + pdf.add_page() + pdf.image(test_file, alt_text=IMG_DESCRIPTION, w=50, h=50) + pdf.add_page() + pdf.image(test_file, alt_text=IMG_DESCRIPTION, w=50, h=50) + assert_pdf_equal(pdf, HERE / "svg_image_alt_text_two_pages.pdf", tmp_path) diff --git a/test/text_region/tcols_3cols.pdf b/test/text_region/tcols_3cols.pdf index 956d79fa4adb45b1b175cd150922af79d81868d2..68ccfd421685bb3b82867647eeea799765120f43 100644 GIT binary patch literal 7122 zcmcgxcT`i|vX`oaB3(KWq?aTRAT%KakRnAvx?+F;K?p7MqBIc!DWUg{AXN|qDbkyC z1(gnhAShK35PZQ;uYT8W-M7|TZ{739Is43+*)y}x>@#zI!=tOLDgqIO0w6#z(9yyU zASVX^AspQ>SfDrnq=~k21)7TEA6*4Vd_PN_oUa2wS|}?tQpwR1XbQ$tB%~!o#b7`f z6e0?f#y{2fazX(?x=0(8E8yZq019JuGPM|<^QQ_OClm&5iA6hNe#{R9siLuJK#ZHc zJ$`}{(aT6LM>i}Gq=P{r9PQm4Fn^INh@DAYZ!9T(N z)O;1?>geWTiNc#A`MWJAR{v(miO!#fsG{w$C>Okw_DC#B8D;5cg#v&sqcApDTcDT} zRP02|73+dRIsiP=KALLcCM0Q_4h?6M3v~; ze}a+_$m6E@SO-@ukSJjv`=v=$LGcbH%VyMBjRV!Mn{!<@o0~XBxFAo4dArUaXA=Ns z^W^1~;4w|s_H7ee<{-ww+xIw+>n8SCAq^bbtebsT>^(vkj;k5lj;q%@nr=_+HGiIP zYvz1l@q}G+@osb2`)>sGUwrR!PDD87-4~rJ_uvNKLo_oM=6+cjEW0hgTiDqBer|HE zTHd+|Q!BqSRjH7%OA*jdtdTOt8)(_=HT6;DHtqgHY&Q7pshMwY)}PHim7#i8Y82Tv zKDr_3zuq;cwhh%<3>)Li7=@KdPHFFiBFrnKp3wl6n>CIO{iHH(1P~oozF8ZSt$e$~ z7Ene!eL3Mn=qQ0n7;=Jg6&Y!e+%LiT@y3n27^47=Fwc=-Ub2oU1DSJlWFMq~q04uj zi8Dd3pYxxtpueHRYZ5kT!!lLN&^UZPJF`pE!<&6f2z+LQXJh=8zfpi|Ouj-OQB9j& zTh?GkP49&r73|H`{yUqnBg_>&=q2Or3gbqZ3BeQ zM=11|&3Y_T<7Tb3og7k>^(mf&yhu^w{aD+Eq)vU6_|b0VE}9xnCr0dEW2oJbvqAuU zA&{X6Y>qtw+XgH^o64N%lL7&BvDQPbcC|BMUaG z^xZ^AF2anrQ-D%-)W}(FI2j=#-Jsm|vN27Tq9biiw6hAbs~>sf@lL&4KZO9OeK+6P4mN^5~nMAB0_W8$_v_ad4VL1o7QzP%S3M{}){nV8SK zBu+0OS88c4^kzd&-YZ>}oL?CHRx{rR|N6e!$y=n!;(lT3X1PI*L^S`{k}1RVfm^-k zuDNpD4cqc|)g*)W3lPrKMQ6+RGv018AC%>TClHO?V~miQnXsAm@GlgAJAy3NlIi!y z2c0wu5$XERzhybe2q0EBqAK5J_v#G8d)rmz(4gFL#<<&oH)?wADxz`JN1j(Cu0EOV z?vI74iAKI z*WWNqeYSK-jE%Iz?r&p7b0((KKO3aQPm+{*j!4%{U*AqJ+g2_d8TAouR1ekC_PGsf z)Z%PxP)IvA8Suv6pvOKI#?^4i;diRW z5AO_}Uo4h0V2)r<1FMpBFUD2RrehY znk~Y+CWa%Xr98SGSX59<>`vw&F3a1q%(PKa3k=E?zPn{*qwk$eH&q84Q>U^@+vZu5 z;(CZRty$qW6Jo86uJS3Em=m+Lw;o26YiZm+#oc$WpGDqWG%b3aayi42oR*m_M=L3) zG%ZQ~F5k_adYPE7k%#)(2sh0~C9)PZ-k(etyIt^RyIY=isAztfO6j%DyI`W)DPhtCP{x;0pmIw;W~Y|-7%!0gp^=7eQ3Mx&g&0_@G% zMw%uiLr_XMk!$?TK@Nc#!#a@***u5y=?4R1qFuS_gRu0~%%WYPru>3FYA+dLCI8o% z41pcoBscDM4a4u03k=-)GJXvSH}qlV(hLdEaLuisnNtl*@--6IuP7Ta)Hg_xqn_kv zzx_4NphPb`oS6YpVDnhT)RUV3HsegYTm)SI0#`}H%Q#3MrR95Z2+L6Ig`3e=w33f! zzm`5%q1C?jlvNx?T8Z>Fquv*>rML+sxbD{5#ySqjYR*0192rS~_3&-&XJUqFUdk?& zzo891M@K|DFiG}-RZojFMw{+zcwSmupP$FJ6%L-O*M4oaQcs^{aG2TRJ=M%agW;X! z$t0qv%n7kOW_EEcvvZVpNTRhPrDOM{Aw+70s%nlVBd63bTvpGQtCK!<4dyc|@bgQzxUVOtkZ6FZ zkwS`WU1v!s(^Iw9{=uwJ!z&~e=LypFKYwIvjIfw(Wk2G$;me??LpTsbUVC6M%SI*@ zx}AHgCeQ}GH$bCS{+0>qT39t{5*097?II@Zs|ng=QdA)t=+P{vK|^&4RiCmfgU*Ff znCXw`HGbX~PgxwfN?t)(SN-ndx1I~<4Pr@1sbal0zb?xjN1Wbf0F=jcDj!QI5G}Eh z@uoJsZ8~zGB?8cB2PUjLVB$|Rspayk@sv_pWrE0wDN1tuf~+=$v&vQh(41T9>$8RC z{`(g*iiE*gF}t+7E4-#V{%|f!i9A=}Lwo=HORp|4l|1*%-~7Pk&2gY8lq>UAUh^oNnZYgt$;)3mhn1RskR|BOw9b!pbesp<~#31Gx#pr)~Vo=G+qtI7PLNNl~?zZ>iCc+zNTpOKD6|cXOgZ(gSQ* zOethrIp%OHvIUxFXyI3QL>ZD1@lbtG@v78Z_SLgQkDkVb%`L^Q*Nkgv9tfPW$8l2Z zX)1p^9Wy2!cv9s5C}4k-`zJpPKg-{L)!{$t<^O6OexmcIA^)ikhd^Nxf2+gOlX_eh zB&qMN`vch0$w^rW=2LoKoR8NmmOK0W-bS#1dYsD0LYIzZF8?D_cb7^L<1eq5j}k4K z70yhIPsq2{jAH`+!)?H-3B|3tD>BIUPO!+3*HQK}M-myK zVIfpq#^I)Kss&B!)^ym($$cohvWd76gYJFkA3bj02~W&Ce||4a;WE+c zF45Q3U<5l0Ct1e=Ewn3N7C{K#*RO4*d)x@ChI3zdisgde4o~2nXyw3g%2yP4m+)}O zvm%s>(N%_{TC$R9=bj!`D^`p|vBJ7t72&vl_f|ZDhWkL~QzBgwbJjiw zm8HVGaCj%7U`HbJ1UsRTm@pkU%ijZ`utPp4KQ$Zw+@WqQ#WKpL@KH zL8hkX-FXwQ@=?1GRbtHADnJ+1v*qa-J~TPtl%q^!F`Kz7({0ET@i?w4Jc#_7WYv?E zHVK^HY95*6dNjbL)z;cHZlxu>j$A3YkMv1t!^U*$eOKE!WS`KY50iysufjGL^*kHU zHsMUAh#$`sWbi?$n{BYr=xk|Jo1Gi8w{GpD)!A$Ayd%tl`ngx`)|IAlvq0go`ojXl z4LU8=pM^uy=81K(2+(!lB_$5l%&kp8 z1#}vestgHNihL0a!qBr+T3ovsR89__)&wa_mGds7|zdgGwq#Id26v(=89IE|D-#1sgXL(&vA~4kPCCA&E35_GuBGrwJ zbdyr@1KEJC8k0yx+Gi3{9qpzeU7{9|H|J#~&)^D5XObt?8g?iGL)Twd%*Wltv7DW% zBERPK`u1=G)?J*qmP@CLaj&v_o5O`QfxmW3>h#sXo85YEgp9hptk40<_60m6~9R53jp&BEk?XgIXr4iLSJ6)7<+E zS{E5_Pif_xRq>dse%O4Ig*@mHCeZRqW(%zH+WSeG)sW#V(O{9+YWih#Op)GL%U=0- zb0%6hM-kHbXhpyMWdHJwFm3~!V-w)1(=o|K#pRqo)6)r(^)oB{D?P#RL_D6W!ZVDJc(9`XUgxn zn=wg;YrL+*Eg9O`iz%{BE8U^dJa1n@V?e%aqzluc;~Oa#{fu}> zoDfyeON^;-VZnikL@}gk?~+l2V7Ev2gjUugiQ&ZzpE{tSfsBjOl#xva=Qj`Ndp}_HxcQ_XXLyiuX=N2(p*HOd$Ml24 zji+Zci9`B(v#QgyYfDqqYchD%1^MkvKE{`fezrF<8osGaMj}7Z@qpoTeKWUuEaG|< z&~Kc(F@@f3zGzQZOxXUZ(0u*0*eNF5P2`?Mg4fi8yS$lCY3Ro*kB!}fn#Nk>L(iX& z+v7*2D@bbZ4!x}=8spi`(`ATb%Y?SRD z*q*TM;WchY;kQb11n*xzpXSD$4Or+evSgkt8*>VjIzuJZ#o~6I+?d=xlC=Zp`EB@A zVLA>H8sqHB!b#f2ZeyLqq0#%Q&vQ-PYv2`~*Xgbro=}ECk*A{eY;4}2Cbe(9YiWNA zgl#?WGSzacTFIZ2V`KAsQ1>YjI3dFEfbWRP9C|qE|5-DrZ7{}qYK+$JdA3M9?lxv( zEu(WU@Dk?|g1I|!a@cA6a&2O1WDsIwR#?+R>S>h-ThJw2`_DTbtX*_L(n4vN>vplkND@EFc-;G@+o6Q`C+3Z1P@^TK>cnr27kSCH{FpO!!Oy!3BdO$q6WQpLl= zK#&^B-W`QSTOt7<6$~DkMPqD$AVV|;j&Vi*Bmh8qZWh?@u=WYK4fzQ_pUm(Zwy=UO!XsL(70!=~q9t+-JlqWvc z)E)3W0;L~U#E+}`_1{D2Z;1)t5jbg*DC5^2$geB|k(QK(h>1fW(qQ~y0+HqegZTu0 zllF8$Sp)Ds!2l-=e;q(c2?=otpf&Ip4R89%0mS@^1_9&q(7(~dPMrBSnmF_iK5>X7 zJ_`QMCoYbk{oiQ#->W}lq0-X$rTx3C1Wf7=SqW)L{JQ*|PZInOw12QgQtVIPB*i5D zSOZDCBY)@+m-@puNeSq0d{`GG+8)2}P8w%=Xm8ZX+Jmk-I%0u8;z}K3?FfXNyj6UJ zsN=&^PFhk4tfC44N?K#w8`e NFaQsaijFGae*khu@lXH& delta 4572 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 Date: Wed, 1 Nov 2023 13:24:26 +0100 Subject: [PATCH 6/6] Preserve dash pattern on new page, fix #992 (#993) * Preserve dash pattern on new page, fix #992 * Update CHANGELOG.md * Update new_page_graphics_state.pdf --- CHANGELOG.md | 1 + fpdf/fpdf.py | 27 +++++++++++++-------- fpdf/graphics_state.py | 12 +++++----- fpdf/line_break.py | 2 +- test/new_page_graphics_state.pdf | Bin 0 -> 1665 bytes test/test_add_page.py | 39 +++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 test/new_page_graphics_state.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index d77c6b318..02b0d5310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * [`TextColumns()`](https://py-pdf.github.io/fpdf2/TextColumns.html) can now have images inserted (both raster and vector). * [`TextColumns()`](https://py-pdf.github.io/fpdf2/TextColumns.html) can now advance to the next column with the new `new_column()` method or a FORM_FEED character (`\u000c`) in the text. ### Fixed +* Previously set dash patterns were not transferred correctly to new pages. * Inserted Vector images used to ignore the `keep_aspect_ratio` argument. ## [2.7.6] - 2023-10-11 diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 99c68729d..3a3dff655 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -590,7 +590,7 @@ def set_text_shaping( "The uharfbuzz package could not be imported, but is required for text shaping. Try: pip install uharfbuzz" ) from exc else: - self._text_shaping = None + self.text_shaping = None return # # Features must be a dictionary contaning opentype features and a boolean flag @@ -619,7 +619,7 @@ def set_text_shaping( "FPDF2 only accept ltr (left to right) or rtl (right to left) directions for now." ) - self._text_shaping = { + self.text_shaping = { "use_shaping_engine": True, "features": features, "direction": direction, @@ -843,6 +843,7 @@ def add_page( tc = self.text_color stretching = self.font_stretching char_spacing = self.char_spacing + dash_pattern = self.dash_pattern if self.page > 0: # Page footer @@ -907,6 +908,11 @@ def add_page( self.set_stretching(stretching) if char_spacing != 0: self.set_char_spacing(char_spacing) + if dash_pattern != dict(dash=0, gap=0, phase=0): + self._write_dash_pattern( + dash_pattern["dash"], dash_pattern["gap"], dash_pattern["phase"] + ) + # END Page header def _beginpage( @@ -1203,16 +1209,17 @@ def set_dash_pattern(self, dash=0, gap=0, phase=0): if pattern != self.dash_pattern: self.dash_pattern = pattern + self._write_dash_pattern(dash, gap, phase) - if dash: - if gap: - dstr = f"[{dash * self.k:.3f} {gap * self.k:.3f}] {phase *self.k:.3f} d" - else: - dstr = f"[{dash * self.k:.3f}] {phase *self.k:.3f} d" + def _write_dash_pattern(self, dash, gap, phase): + if dash: + if gap: + dstr = f"[{dash * self.k:.3f} {gap * self.k:.3f}] {phase *self.k:.3f} d" else: - dstr = "[] 0 d" - - self._out(dstr) + dstr = f"[{dash * self.k:.3f}] {phase *self.k:.3f} d" + else: + dstr = "[] 0 d" + self._out(dstr) @check_page def line(self, x1, y1, x2, y2): diff --git a/fpdf/graphics_state.py b/fpdf/graphics_state.py index 8ba1ed891..450e259cc 100644 --- a/fpdf/graphics_state.py +++ b/fpdf/graphics_state.py @@ -52,7 +52,7 @@ def __init__(self, *args, **kwargs): sup_lift=0.4, nom_lift=0.2, denom_lift=0.0, - _text_shaping=None, + text_shaping=None, ), ] super().__init__(*args, **kwargs) @@ -326,12 +326,12 @@ def denom_lift(self, v): self.__statestack[-1]["denom_lift"] = float(v) @property - def _text_shaping(self): - return self.__statestack[-1]["_text_shaping"] + def text_shaping(self): + return self.__statestack[-1]["text_shaping"] - @_text_shaping.setter - def _text_shaping(self, v): - self.__statestack[-1]["_text_shaping"] = v + @text_shaping.setter + def text_shaping(self, v): + self.__statestack[-1]["text_shaping"] = v def font_face(self): """ diff --git a/fpdf/line_break.py b/fpdf/line_break.py index b317efc87..46cddcc6c 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -141,7 +141,7 @@ def lift(self): @property def _text_shaping(self): - return self.graphics_state["_text_shaping"] + return self.graphics_state["text_shaping"] @property def string(self): diff --git a/test/new_page_graphics_state.pdf b/test/new_page_graphics_state.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0682e3f6409b36ca48f9d3a800e7d757ce84ca0b GIT binary patch literal 1665 zcmbVMe@s1aKJW;6GcKtQImcIY5tV+knaVN#_Rd0Gh+;&Nq96x_^!v~Y3)5S%=Z z2ykc$yGL}&fJ_shN#vcy!adjl@FuOEBqmm7dC~5b25ka zp+j^^xzGXh-jq1G7atDw@L4!sh7#H<&&tpYbHzMJkSS2G%LPEG)8iSU{y$8ZCFxA!=~V~?>TN(Y+5}rcYE(#-^3&{ z_2KAC-TMyDwpBzvHQjN<)uCE3lGiiw`qXb5`}#X;u7TE=~pWk#!)a5strNVW#Z?0rFQS-xoy#Wj3(Hz;HX{EWWs z3iSzdV_@^C{4V;D|7DO_0v*UjKS^` zrFom*nL8f|lFlqBQ!DpW7e?!NBY_r*u{UNgVym7jwbo-a65RA?=0Yj%qjbYZf4 z)yjrS--id7F`}ofu%+<)>Qd(u;g|kYR^2g`-(f!E=SOeo14b^y`d$03t7<^rda(0X z|Kz5atd{4-#%pd3YuSSFXJ!I|&S=M89I7_9ZXbHgexUz4R!V`CO7bYhEh<%9M3=Vy zerb}IF4C5mPj1C+n6N#%zM-n#CrVgb9h8z>u%=F_Or7dYWJh#h set_font() + # font_style -> set_font() + pdf.set_stretching(50) + pdf.set_char_spacing(5) + # font_family -> set_font() + # font_size_pt -> set_font() + # current_font -> set_font() + pdf.set_dash_pattern(dash=2, gap=4) + pdf.set_line_width(2) + # text_mode -> applied via Fragments + # char lift/scale -> applied via Fragments + # text_shaping -> applied in the creation of Fragments + + def draw_stuff(): + pdf.cell( + text="This text is blue, italic, underlined, and squished with wide char spacing.", + new_x="LEFT", + new_y="NEXT", + ) + pdf.cell(text="The box below is green, with a thick, dashed, orange border.") + pdf.rect(50, 50, 60, 30, style="DF") + + draw_stuff() + pdf.add_page() + draw_stuff() + assert_pdf_equal(pdf, HERE / "new_page_graphics_state.pdf", tmp_path)