From 31f4e56e535783311b553b9f8b0409125f61f98e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 09:08:09 +0100 Subject: [PATCH 1/7] add Bubbu0129 as a contributor for bug (#633) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index f009e5635..c25fdefea 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -742,7 +742,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/93034081?v=4", "profile": "https://github.com/Bubbu0129", "contributions": [ - "code" + "code", + "bug" ] } ], diff --git a/README.md b/README.md index 653c7fb52..e4f9d68a4 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ This library could only exist thanks to the dedication of many volunteers around Sean
Sean

💻 Anderson Herzogenrath da Costa
Anderson Herzogenrath da Costa

💬 💻 Yi Wei Lan
Yi Wei Lan

⚠️ - CpDong
CpDong

💻 + CpDong
CpDong

💻 🐛 From e02fc576014b827d9f31a5fc76fb224779efc582 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 21 Dec 2022 19:30:44 +0100 Subject: [PATCH 2/7] Improving add_link() (#615) --- CHANGELOG.md | 7 +- docs/CombineWithPdfrw.md | 13 +++- docs/Links.md | 3 +- docs/Logging.md | 64 +++++++++++++++--- docs/Tutorial-de.md | 3 +- docs/Tutorial-es.md | 4 +- docs/Tutorial-fr.md | 2 +- docs/Tutorial-gr.md | 4 +- docs/Tutorial-he.md | 2 +- docs/Tutorial-it.md | 4 +- docs/Tutorial-pt.md | 2 +- docs/Tutorial-ru.md | 2 +- ...77\340\244\202\340\244\246\340\245\200.md" | 2 - docs/Tutorial.md | 4 +- fpdf/__init__.py | 3 +- fpdf/fpdf.py | 46 +++++++++++-- fpdf/html.py | 3 +- fpdf/output.py | 17 ++++- fpdf/syntax.py | 8 +++ test/image/image_x_align_center.pdf | Bin 0 -> 28191 bytes test/image/image_x_align_right.pdf | Bin 0 -> 28190 bytes test/image/test_image_align.py | 24 +++++++ test/image/test_oversized.py | 2 +- test/inserting_same_page_link_twice.pdf | Bin 0 -> 1267 bytes test/outline/test_outline.py | 3 +- test/test_links.py | 38 +++++++++++ tutorial/tuto6.py | 3 +- tutorial/unicode.py | 0 28 files changed, 208 insertions(+), 55 deletions(-) create mode 100644 test/image/image_x_align_center.pdf create mode 100644 test/image/image_x_align_right.pdf create mode 100644 test/image/test_image_align.py create mode 100644 test/inserting_same_page_link_twice.pdf mode change 100644 => 100755 tutorial/unicode.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d94dfc9b9..191a03900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,13 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.6.1] - not released yet ### Added +* the `x` parameter of [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) can now accepts a value of `"C"` / `Align.C` / `"R"` / `Align.R` to horizontally position the image centered or aligned right * support for `[]()` links when `markdown=True` -* support for `line-height` attribute of paragraph (`

`) in `write_html()` +* support for `line-height` attribute of paragraph (`

`) in `write_html()` - thanks to @Bubbu0129 ### Changed -* `write_html()`now generates warnings for unclosed HTML tags, unless `warn_on_tags_not_matching=False` is set +* `add_link()` creates a link to the current page by default, and now accepts optional parameters: `x`, `y`, `page` & `zoom`. + Hence calling `set_link()` is not needed anymore after creating a link with `add_link()`. +* `write_html()` now generates warnings for unclosed HTML tags, unless `warn_on_tags_not_matching=False` is set ## [2.6.0] - 2022-11-20 ### Added diff --git a/docs/CombineWithPdfrw.md b/docs/CombineWithPdfrw.md index b3458d6d9..ac65645b8 100644 --- a/docs/CombineWithPdfrw.md +++ b/docs/CombineWithPdfrw.md @@ -15,21 +15,24 @@ with numerous examples and a very clean set of classes modelling the PDF interna import sys from fpdf import FPDF from pdfrw import PageMerge, PdfReader, PdfWriter +from pdfrw.pagemerge import RectXObj IN_FILEPATH = sys.argv[1] OUT_FILEPATH = sys.argv[2] ON_PAGE_INDEX = 1 UNDERNEATH = False # if True, new content will be placed underneath page (painted first) +reader = PdfReader(IN_FILEPATH) +area = RectXObj(reader.pages[0]) + def new_content(): - fpdf = FPDF() + fpdf = FPDF(format=(area.w, area.h)) fpdf.add_page() fpdf.set_font("helvetica", size=36) fpdf.text(50, 50, "Hello!") reader = PdfReader(fdata=bytes(fpdf.output())) return reader.pages[0] -reader = PdfReader(IN_FILEPATH) writer = PdfWriter() writer.pagearray = reader.Root.Pages.Kids PageMerge(writer.pagearray[ON_PAGE_INDEX]).add(new_content(), prepend=UNDERNEATH).render() @@ -42,13 +45,17 @@ writer.write(OUT_FILEPATH) import sys from fpdf import FPDF from pdfrw import PdfReader, PdfWriter +from pdfrw.pagemerge import RectXObj IN_FILEPATH = sys.argv[1] OUT_FILEPATH = sys.argv[2] NEW_PAGE_INDEX = 1 # set to None to append at the end +reader = PdfReader(IN_FILEPATH) +area = RectXObj(reader.pages[0]) + def new_page(): - fpdf = FPDF() + fpdf = FPDF(format=(area.w, area.h)) fpdf.add_page() fpdf.set_font("helvetica", size=36) fpdf.text(50, 50, "Hello!") diff --git a/docs/Links.md b/docs/Links.md index 73591abac..cb2b4dcf4 100644 --- a/docs/Links.md +++ b/docs/Links.md @@ -70,8 +70,7 @@ pdf.add_page() # Displaying a full-width cell with centered text: pdf.cell(w=pdf.epw, txt="Welcome on first page!", align="C") pdf.add_page() -link = pdf.add_link() -pdf.set_link(link, page=1) +link = pdf.add_link(page=1) pdf.cell(txt="Internal link to first page", border=1, link=link) pdf.output("internal_link.pdf") ``` diff --git a/docs/Logging.md b/docs/Logging.md index cf54f18b7..4c5b17fff 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -9,19 +9,61 @@ Here is an example of setup code to display them: ```python import logging -logging.basicConfig(format="%(asctime)s %(filename)s [%(levelname)s] %(message)s", +logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", level=logging.DEBUG) ``` Example output using the [Tutorial](Tutorial.md) first code snippet: - 14:09:56 fpdf.py [DEBUG] Final doc sections size summary: - 14:09:56 fpdf.py [DEBUG] - header.size: 9.0B - 14:09:56 fpdf.py [DEBUG] - pages.size: 306.0B - 14:09:56 fpdf.py [DEBUG] - resources.fonts.size: 101.0B - 14:09:56 fpdf.py [DEBUG] - resources.images.size: 0.0B - 14:09:56 fpdf.py [DEBUG] - resources.dict.size: 104.0B - 14:09:56 fpdf.py [DEBUG] - info.size: 54.0B - 14:09:56 fpdf.py [DEBUG] - catalog.size: 103.0B - 14:09:56 fpdf.py [DEBUG] - xref.size: 169.0B - 14:09:56 fpdf.py [DEBUG] - trailer.size: 60.0B + 19:25:24 fpdf.output [DEBUG] Final size summary of the biggest document sections: + 19:25:24 fpdf.output [DEBUG] - pages: 223.0B + 19:25:24 fpdf.output [DEBUG] - fonts: 102.0B + +## fonttools verbose logs + +Since `fpdf2` v2.5.7, verbose **INFO** logs are generated by `fonttools`, +a library we use to parse font files: + +``` +fontTools.subset [INFO] maxp pruned +fontTools.subset [INFO] cmap pruned +fontTools.subset [INFO] post pruned +fontTools.subset [INFO] EBDT dropped +fontTools.subset [INFO] EBLC dropped +fontTools.subset [INFO] GDEF dropped +fontTools.subset [INFO] GPOS dropped +fontTools.subset [INFO] GSUB dropped +fontTools.subset [INFO] DSIG dropped +fontTools.subset [INFO] name pruned +fontTools.subset [INFO] glyf pruned +fontTools.subset [INFO] Added gid0 to subset +fontTools.subset [INFO] Added first four glyphs to subset +fontTools.subset [INFO] Closing glyph list over 'glyf': 25 glyphs before +fontTools.subset [INFO] Glyph names: ['.notdef', 'b', 'braceleft', 'braceright', 'd', 'e', 'eight', 'five', 'four', 'glyph1', 'glyph2', 'h', 'l', 'n', 'nine', 'o', 'one', 'r', 'seven', 'six', 'space', 'three', 'two', 'w', 'zero'] +fontTools.subset [INFO] Glyph IDs: [0, 1, 2, 3, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 69, 71, 72, 75, 79, 81, 82, 85, 90, 94, 96] +fontTools.subset [INFO] Closed glyph list over 'glyf': 25 glyphs after +fontTools.subset [INFO] Glyph names: ['.notdef', 'b', 'braceleft', 'braceright', 'd', 'e', 'eight', 'five', 'four', 'glyph1', 'glyph2', 'h', 'l', 'n', 'nine', 'o', 'one', 'r', 'seven', 'six', 'space', 'three', 'two', 'w', 'zero'] +fontTools.subset [INFO] Glyph IDs: [0, 1, 2, 3, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 69, 71, 72, 75, 79, 81, 82, 85, 90, 94, 96] +fontTools.subset [INFO] Retaining 25 glyphs +fontTools.subset [INFO] head subsetting not needed +fontTools.subset [INFO] hhea subsetting not needed +fontTools.subset [INFO] maxp subsetting not needed +fontTools.subset [INFO] OS/2 subsetting not needed +fontTools.subset [INFO] hmtx subsetted +fontTools.subset [INFO] cmap subsetted +fontTools.subset [INFO] fpgm subsetting not needed +fontTools.subset [INFO] prep subsetting not needed +fontTools.subset [INFO] cvt subsetting not needed +fontTools.subset [INFO] loca subsetting not needed +fontTools.subset [INFO] post subsetted +fontTools.subset [INFO] name subsetting not needed +fontTools.subset [INFO] glyf subsetted +fontTools.subset [INFO] head pruned +fontTools.subset [INFO] OS/2 Unicode ranges pruned: [0] +fontTools.subset [INFO] glyf pruned +``` + +You can easily suppress those logs with this single line of code: +``` +logging.getLogger('fontTools.subset').level = logging.WARN +``` diff --git a/docs/Tutorial-de.md b/docs/Tutorial-de.md index 1a7e92a86..56d4d3c32 100644 --- a/docs/Tutorial-de.md +++ b/docs/Tutorial-de.md @@ -199,8 +199,7 @@ Der Anfang des Satzes wird in "normalem" Stil geschrieben, dann mit der Methode Um einen internen Link hinzuzufügen, der auf die zweite Seite verweist, nutzen wir die Methode [`add_link()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), die einen anklickbaren Bereich erzeugt, - den wir "link" nennen und der auf eine andere Stelle innerhalb des Dokuments verweist. Auf der zweiten Seite verwenden wir - [`set_link()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link), um den Zielbereich für den soeben erstellten Link zu definieren. + den wir "link" nennen und der auf eine andere Stelle innerhalb des Dokuments verweist. Um einen externen Link mit Hilfe eines Bildes zu erstellen, verwenden wir [`image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). Es besteht die Möglichkeit, der Methode ein Linkziel als eines ihrer Argumente zu übergeben. Der Link kann sowohl einer interner als auch ein externer sein. diff --git a/docs/Tutorial-es.md b/docs/Tutorial-es.md index e78cce8d6..7a17f1efc 100644 --- a/docs/Tutorial-es.md +++ b/docs/Tutorial-es.md @@ -243,9 +243,7 @@ En la primera página del ejemplo usamos Para agregar un enlace interno apuntando a la segunda página, usamos el método [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) , el cual crea un área clicable a la que nombramos "link" que redirige a - otro lugar dentro del documento. En la segunda página usamos - [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) - para definir el área de destino para el enlace que acabamos de crear. + otro lugar dentro del documento. Para crear un enlace externo usando una imagen, usamos [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) diff --git a/docs/Tutorial-fr.md b/docs/Tutorial-fr.md index 9450ef58b..c6687a679 100644 --- a/docs/Tutorial-fr.md +++ b/docs/Tutorial-fr.md @@ -151,7 +151,7 @@ En revanche, son principal inconvénient est que nous ne pouvons pas justifier l Dans la première page de l'exemple, nous avons utilisé [write()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write) à cette fin. Le début de la phrase est écrit en style normal, puis en utilisant la méthode [set_font()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font), nous sommes passés au soulignement et avons terminé la phrase. -Pour ajouter un lien interne pointant vers la deuxième page, nous avons utilisé la méthode [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), qui crée une zone cliquable que nous avons nommée `link` et qui dirige vers un autre endroit du document. Sur la deuxième page, nous avons utilisé [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) pour définir la zone de destination du lien que nous venons de créer. +Pour ajouter un lien interne pointant vers la deuxième page, nous avons utilisé la méthode [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), qui crée une zone cliquable que nous avons nommée `link` et qui dirige vers une autre page du document. Pour créer le lien externe à l'aide d'une image, nous avons utilisé [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). Cette méthode a la possibilité de transmettre un lien comme l'un de ses arguments. Le lien peut être interne ou externe. diff --git a/docs/Tutorial-gr.md b/docs/Tutorial-gr.md index c6a63b202..c80460a09 100644 --- a/docs/Tutorial-gr.md +++ b/docs/Tutorial-gr.md @@ -155,9 +155,7 @@ pdf.cell(60, 10, 'Powered by FPDF.', new_x="LMARGIN", new_y="NEXT", align='C') Στην πρώτη σελίδα του παραδείγματος χρησιμοποιήσαμε την μέθοδο [write()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write) για αυτό το σκοπό. Το πρώτο κομμάτι της πρότασης είναι γραμμένο ως απλό κείμενο, ενώ στη συνέχεια, αφού χρησιμοποιήσαμε την μέθοδο [set_font()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font), αλλάξαμε το στυλ κειμένου σε υπογράμμιση και κλείσαμε την πρόταση. Για να προσθέσουμε έναν εσωτερικό σύνδεσμο ο οποίος θα κατευθύνει στην επόμενη σελίδα, χρησιμοποιήσαμε την μέθοδο - [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), η οποία δημιουργεί μία επιφανεία με όνομα "link". Αν κλικάρουμε την επιφάνεια αυτή μεταφερόμαστε σε μία άλλη τοποθεσία του αρχείου. Στην δεύτερη σελίδα χρησιμοποιήσαμε την μέθοδο - [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) - για να ορίσουμε τον προορισμό του συνδέσμου που μόλις δημιουργήσαμε. + [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), η οποία δημιουργεί μία επιφανεία με όνομα "link". Αν κλικάρουμε την επιφάνεια αυτή μεταφερόμαστε σε μία άλλη τοποθεσία του αρχείου. Για να δημιουργήσουμε έναν εξωτερικό σύνδεσμο μέσω μιας εικόνας, θα χρησιμοποιήσουμε την μέθοδο [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). Αυτή η μέθοδος μας δίνει την επιλογή να περάσουμε έναν σύνδεσμο ως τιμή σε μία από τις παραμέτρους της. Ο σύνδεσμος μπορεί να είναι εσωτερικός ή εξωτερικός. diff --git a/docs/Tutorial-he.md b/docs/Tutorial-he.md index dc5ed2ff4..59d6e7308 100644 --- a/docs/Tutorial-he.md +++ b/docs/Tutorial-he.md @@ -153,7 +153,7 @@ pdf.cell(60, 10, 'Powered by FPDF.', new_x="LMARGIN", new_y="NEXT", align='C') בעמוד הראשון של הדוגמא השתמשנו [()write](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write) למטרה זו. תחילת המשפט נכתב בסגנון טקסט רגיל ואז על ידי שימוש במתודה [()set_font](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font) החלפנו לטקסט עם קו תחתון לסיום המשפט. -כדי להוסיף קישור פנימי שמוביל לעמוד השני השתמשנו במתודה [()add_link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) שוצרת איזור ניתן להקלקה שנתנו לו את השם "קישור" שמוביל לאיזור אחר באותו המסמך. בעמוד השני השתמשנו ב[()set_link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) על מנת להגדיר את האיזור אליו הקישור שיצרנו מוביל. +כדי להוסיף קישור פנימי שמוביל לעמוד השני השתמשנו במתודה [()add_link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) שוצרת איזור ניתן להקלקה שנתנו לו את השם "קישור" שמוביל לאיזור אחר באותו המסמך. על מנת ליצור קישור חיצני באמצעות תמונה, השתמשנו במתודה [()image](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). למתודה יש אופציה לקבל קישור כאחד הפרמטרים שלה. הקישור יכול להיות פנימי או חיצוני. diff --git a/docs/Tutorial-it.md b/docs/Tutorial-it.md index 592213753..f19d0f6f9 100644 --- a/docs/Tutorial-it.md +++ b/docs/Tutorial-it.md @@ -183,9 +183,7 @@ Nella prima pagina dell'esempio, abbiamo usato Per aggiungere un link interno che puntasse alla seconda pagina, abbiamo utilizzato [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) -che crea un area cliccabile che abbiamo chiamato "link" che redirige ad un altro punto del documento. Nella seconda pagina abbiamo usato - [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) - per definire un'area di destinazione per il link creato in precedenza. +che crea un area cliccabile che abbiamo chiamato "link" che redirige ad un altro punto del documento. Per creare un link esterno utilizzando un'immagine, abbiamo usato [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) diff --git a/docs/Tutorial-pt.md b/docs/Tutorial-pt.md index 7df457f46..6064beb42 100644 --- a/docs/Tutorial-pt.md +++ b/docs/Tutorial-pt.md @@ -174,7 +174,7 @@ Por outro lado, a sua principal desvantagem é que não podemos justificar o tex Na primeira página do exemplo, usámos [write()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write) para este propósito. O início da frase está escrita no estilo de texto normal, depois usando o método [set_font()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font), trocamos para sublinhado e acabámos a frase. -Para adicionar o link externo a apontar para a segunda página, nós usámos o método [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), que cria uma área clicável à qual demos o nome de “link” que direciona para outra parte do documento. Na segunda página, usámos [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) para definir uma área de destino para o link que acabámos de criar. +Para adicionar o link externo a apontar para a segunda página, nós usámos o método [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), que cria uma área clicável à qual demos o nome de “link” que direciona para outra parte do documento. Para criar o link externo usando uma imagem, usámos [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). O método tem a opção de passar um link como um dos seus argumentos. O link pode ser interno ou externo. diff --git a/docs/Tutorial-ru.md b/docs/Tutorial-ru.md index db0bfc0f0..1712f0f6c 100644 --- a/docs/Tutorial-ru.md +++ b/docs/Tutorial-ru.md @@ -146,7 +146,7 @@ pdf.cell(60, 10, 'Powered by FPDF.', new_x="LMARGIN", new_y="NEXT", align='C') На первой странице примера мы использовали для этой цели [write()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write). Начало предложения написано текстом обычного стиля, затем, используя метод [set_font()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_font), мы переключились на подчеркивание и закончили предложение. -Для добавления внутренней ссылки, указывающей на вторую страницу, мы использовали метод [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), который создает кликабельную область, названную нами "link", которая ведет в другое место внутри документа. На второй странице мы использовали метод [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link), чтобы определить целевую зону для только что созданной ссылки. +Для добавления внутренней ссылки, указывающей на вторую страницу, мы использовали метод [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link), который создает кликабельную область, названную нами "link", которая ведет в другое место внутри документа. Чтобы создать внешнюю ссылку с помощью изображения, мы использовали метод [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image). Этот метод имеет возможность передать ссылку в качестве одного из аргументов. Ссылка может быть как внутренней, так и внешней. diff --git "a/docs/Tutorial-\340\244\271\340\244\277\340\244\202\340\244\246\340\245\200.md" "b/docs/Tutorial-\340\244\271\340\244\277\340\244\202\340\244\246\340\245\200.md" index d3fd5b425..f64e976bd 100644 --- "a/docs/Tutorial-\340\244\271\340\244\277\340\244\202\340\244\246\340\245\200.md" +++ "b/docs/Tutorial-\340\244\271\340\244\277\340\244\202\340\244\246\340\245\200.md" @@ -203,8 +203,6 @@ Logo को निर्दिष्ट करके [image](fpdf/fpdf.html#fpdf दूसरे पृष्ठ की ओर इशारा करते हुए एक आंतरिक लिंक जोड़ने के लिए, हमने [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) विधि का उपयोग किया, जो एक क्लिक करने योग्य क्षेत्र बनाता है जिसे हमने "Link" नाम दिया है जो दस्तावेज़ के भीतर किसी अन्य स्थान पर निर्देशित करता है। -दूसरे पृष्ठ पर, हमने अभी-अभी बनाए गए लिंक के लिए गंतव्य क्षेत्र को परिभाषित करने के लिए [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) का उपयोग किया। - Image का उपयोग करके बाहरी लिंक बनाने के लिए, हमने [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) का उपयोग किया। विधि में एक लिंक को इसके तर्कों में से एक के रूप में पारित करने का विकल्प होता है। लिंक आंतरिक या बाहरी दोनों हो सकता है। एक विकल्प के रूप में, फ़ॉन्ट शैली बदलने और लिंक जोड़ने का दूसरा विकल्प `write_html()` पद्धति का उपयोग करना है। यह एक HTML पार्सर है, जो टेक्स्ट जोड़ने, फ़ॉन्ट शैली बदलने और html का उपयोग करके लिंक जोड़ने की अनुमति देता है। diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 56a2be2d0..8142de057 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -243,9 +243,7 @@ In the first page of the example, we used To add an internal link pointing to the second page, we used the [add_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.add_link) method, which creates a clickable area which we named "link" that directs to - another place within the document. On the second page, we used - [set_link()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_link) - to define the destination area for the link we just created. + another page within the document. To create the external link using an image, we used [image()](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) diff --git a/fpdf/__init__.py b/fpdf/__init__.py index e471ab9c7..253089cb8 100644 --- a/fpdf/__init__.py +++ b/fpdf/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import sys -from .enums import TextMode, XPos, YPos +from .enums import Align, TextMode, XPos, YPos from .fpdf import ( FPDF, FPDFException, @@ -34,6 +34,7 @@ "__license__", # Classes "FPDF", + "Align", "XPos", "YPos", "Template", diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 37e234eaf..bec185f88 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -17,6 +17,7 @@ from functools import wraps from html import unescape from math import isclose +from numbers import Number from os.path import splitext from pathlib import Path from typing import Callable, List, NamedTuple, Optional, Union @@ -827,6 +828,7 @@ def _beginpage( contents=bytearray(), duration=duration, transition=transition, + index=self.page, ) self.pages[self.page] = page if transition: @@ -1942,18 +1944,36 @@ def set_stretching(self, stretching): if self.page > 0: self._out(f"BT {stretching:.2f} Tz ET") - def add_link(self): + def add_link(self, y=0, x=0, page=-1, zoom="null"): """ Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document. The identifier can then be passed to the `FPDF.cell()`, `FPDF.write()`, `FPDF.image()` or `FPDF.link()` methods. - The destination must be defined using `FPDF.set_link()`. + + Args: + y (float): optional ordinate of target position. + The default value is 0 (top of page). + x (float): optional abscissa of target position. + The default value is 0 (top of page). + page (int): optional number of target page. + -1 indicates the current page, which is the default value. + zoom (float): optional new zoom level after following the link. + Currently ignored by Sumatra PDF Reader, but observed by Adobe Acrobat reader. """ - link_index = len(self.links) + 1 - self.links[link_index] = DestinationXYZ(page=1, top=self.h_pt) - return link_index + link = DestinationXYZ( + self.page if page == -1 else page, + top=self.h_pt - y * self.k, + left=x * self.k, + zoom=zoom, + ) + try: + return next(i for i, l in self.links.items() if l == link) + except StopIteration: + link_index = len(self.links) + 1 + self.links[link_index] = link + return link_index def set_link(self, link, y=0, x=0, page=-1, zoom="null"): """ @@ -1990,7 +2010,7 @@ def link(self, x, y, w, h, link, alt_text=None, border_width=0): y (float): vertical position (from the top) to the bottom side of the link rectangle w (float): width of the link rectangle h (float): height of the link rectangle - link: either an URL or a integer returned by `FPDF.add_link`, defining an internal link to a page + link: either an URL or an integer returned by `FPDF.add_link`, defining an internal link to a page alt_text (str): optional textual description of the link, for accessibility purposes border_width (int): thickness of an optional black border surrounding the link. Not all PDF readers honor this: Acrobat renders it but not Sumatra. @@ -3464,8 +3484,10 @@ def image( Args: name: either a string representing a file path to an image, an URL to an image, an io.BytesIO, or a instance of `PIL.Image.Image` - x (float): optional horizontal position where to put the image on the page. + x (float, fpdf.enums.Align): optional horizontal position where to put the image on the page. If not specified or equal to None, the current abscissa is used. + `Align.C` can also be passed to center the image horizontally; + and `Align.R` to place it along the right page margin y (float): optional vertical position where to put the image on the page. If not specified or equal to None, the current ordinate is used. After the call, the current ordinate is moved to the bottom of the image @@ -3540,6 +3562,16 @@ def image( self.y += h if x is None: x = self.x + elif not isinstance(x, Number): + 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} " diff --git a/fpdf/html.py b/fpdf/html.py index 293e7e425..57cfaa8ca 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -807,8 +807,7 @@ def render_toc(self, pdf, outline): "This method can be overriden by subclasses to customize the Table of Contents style." pdf.ln() for section in outline: - link = pdf.add_link() - pdf.set_link(link, page=section.page_number) + link = pdf.add_link(page=section.page_number) text = f'{" " * section.level * 2} {section.name}' text += f' {"." * (60 - section.level*2 - len(section.name))} {section.page_number}' pdf.multi_cell( diff --git a/fpdf/output.py b/fpdf/output.py index 4cb7c819d..30807d747 100644 --- a/fpdf/output.py +++ b/fpdf/output.py @@ -226,6 +226,7 @@ class PDFPage(PDFObject): "struct_parents", "resources", "parent", + "_index", "_width_pt", "_height_pt", ) @@ -235,6 +236,7 @@ def __init__( duration, transition, contents, + index, ): super().__init__() self.type = Name("Page") @@ -247,8 +249,12 @@ def __init__( self.struct_parents = None self.resources = None # must always be set before calling .serialize() self.parent = None # must always be set before calling .serialize() + self._index = index self._width_pt, self._height_pt = None, None + def index(self): + return self._index + def dimensions(self): "Return a pair (width, height) in the unit specified to FPDF constructor" return self._width_pt, self._height_pt @@ -372,10 +378,17 @@ def bufferize(self): page_obj.parent = pages_root_obj page_obj.resources = resources_dict_obj for annot in page_obj.annots: + page_dests = [] if annot.dest: - dests.append(annot.dest) + page_dests.append(annot.dest) if annot.a and hasattr(annot.a, "dest"): - dests.append(annot.a.dest) + page_dests.append(annot.a.dest) + for dest in page_dests: + if dest.page_number > len(page_objs): + raise ValueError( + f"Invalid reference to non-existing page {dest.page_number} present on page {page_obj.index()}: " + ) + dests.extend(page_dests) if not page_obj.annots: # Avoid serializing an empty PDFArray: page_obj.annots = None diff --git a/fpdf/syntax.py b/fpdf/syntax.py index 6b07efdbf..e237a631a 100644 --- a/fpdf/syntax.py +++ b/fpdf/syntax.py @@ -275,6 +275,14 @@ def __init__(self, page, top, left=0, zoom="null"): self.zoom = zoom self.page_ref = None + def __eq__(self, dest): + return ( + self.page_number == dest.page_number + and self.top == dest.top + and self.left == dest.left + and self.zoom == dest.zoom + ) + def __repr__(self): return f'DestinationXYZ(page_number={self.page_number}, top={self.top}, left={self.left}, zoom="{self.zoom}", page_ref={self.page_ref})' diff --git a/test/image/image_x_align_center.pdf b/test/image/image_x_align_center.pdf new file mode 100644 index 0000000000000000000000000000000000000000..99a89b620e9eb6fcb571bba9d3cfad5a127ffaf7 GIT binary patch literal 28191 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 literal 0 HcmV?d00001 diff --git a/test/image/image_x_align_right.pdf b/test/image/image_x_align_right.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b076696996d519fd55a9f14653efdeaa37791894 GIT binary patch literal 28190 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 literal 0 HcmV?d00001 diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index 68be85ffb..b16c9080d 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -95,8 +95,7 @@ def render_toc(pdf, outline): pdf.y += 20 pdf.set_font("Courier", size=12) for section in outline: - link = pdf.add_link() - pdf.set_link(link, page=section.page_number) + link = pdf.add_link(page=section.page_number) p( pdf, f'{" " * section.level * 2} {section.name} {"." * (60 - section.level*2 - len(section.name))} {section.page_number}', diff --git a/test/test_links.py b/test/test_links.py index cb9410217..11ee01980 100644 --- a/test/test_links.py +++ b/test/test_links.py @@ -3,6 +3,8 @@ from fpdf import FPDF from test.conftest import assert_pdf_equal +import pytest + HERE = Path(__file__).resolve().parent @@ -123,3 +125,39 @@ def test_link_border(tmp_path): ) assert_pdf_equal(pdf, HERE / "link_border.pdf", tmp_path) + + +def test_inserting_same_page_link_twice(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.link( + x=pdf.l_margin, + y=pdf.t_margin, + w=pdf.epw, + h=pdf.eph, + link=pdf.add_link(page=2), + ) + pdf.add_page() + pdf.link( + x=pdf.l_margin, + y=pdf.t_margin, + w=pdf.epw, + h=pdf.eph, + link=pdf.add_link(page=2), + ) + assert pdf.add_link(page=2) == pdf.add_link(page=2) + assert_pdf_equal(pdf, HERE / "inserting_same_page_link_twice.pdf", tmp_path) + + +def test_inserting_link_to_non_exising_page(): + pdf = FPDF() + pdf.add_page() + pdf.link( + x=pdf.l_margin, + y=pdf.t_margin, + w=pdf.epw, + h=pdf.eph, + link=pdf.add_link(page=2), + ) + with pytest.raises(ValueError): + pdf.output() diff --git a/tutorial/tuto6.py b/tutorial/tuto6.py index a236abe3c..bf38a1dae 100644 --- a/tutorial/tuto6.py +++ b/tutorial/tuto6.py @@ -8,13 +8,12 @@ pdf.set_font("helvetica", size=20) pdf.write(5, "To find out what's new in self tutorial, click ") pdf.set_font(style="U") -link = pdf.add_link() +link = pdf.add_link(page=2) pdf.write(5, "here", link) pdf.set_font() # Second page: pdf.add_page() -pdf.set_link(link) pdf.image( "../docs/fpdf2-logo.png", 10, 10, 50, 0, "", "https://pyfpdf.github.io/fpdf2/" ) diff --git a/tutorial/unicode.py b/tutorial/unicode.py old mode 100644 new mode 100755 From ce81bb77f7ad6667e64c917b759094b07e4a5225 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 21 Dec 2022 19:44:33 +0100 Subject: [PATCH 3/7] Fix `ValueError: Incoherent hierarchy` that could be raised when using `write_html()` with some headings hierarchy (#636) --- CHANGELOG.md | 4 +++- fpdf/fpdf.py | 4 ++-- fpdf/html.py | 4 ++-- test/html/html_unorthodox_headings_hierarchy.pdf | Bin 0 -> 1359 bytes test/html/test_html.py | 10 ++++++++++ 5 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 test/html/html_unorthodox_headings_hierarchy.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 191a03900..7e015a100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,15 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.6.1] - not released yet ### Added -* the `x` parameter of [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) can now accepts a value of `"C"` / `Align.C` / `"R"` / `Align.R` to horizontally position the image centered or aligned right +* the `x` parameter of [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image) now accepts a value of `"C"` / `Align.C` / `"R"` / `Align.R` to horizontally position the image centered or aligned right * support for `[]()` links when `markdown=True` * support for `line-height` attribute of paragraph (`

`) in `write_html()` - thanks to @Bubbu0129 ### Changed * `add_link()` creates a link to the current page by default, and now accepts optional parameters: `x`, `y`, `page` & `zoom`. Hence calling `set_link()` is not needed anymore after creating a link with `add_link()`. * `write_html()` now generates warnings for unclosed HTML tags, unless `warn_on_tags_not_matching=False` is set +### Fixed +* a `ValueError: Incoherent hierarchy` could be raised when using `write_html()` with some headings hierarchy ## [2.6.0] - 2022-11-20 ### Added diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index bec185f88..b120d139f 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -4302,7 +4302,7 @@ def set_section_title_styles( } @check_page - def start_section(self, name, level=0): + def start_section(self, name, level=0, strict=True): """ Start a section in the document outline. If section_title_styles have been configured, @@ -4314,7 +4314,7 @@ def start_section(self, name, level=0): """ if level < 0: raise ValueError('"level" mut be equal or greater than zero') - if self._outline and level > self._outline[-1].level + 1: + if strict and self._outline and level > self._outline[-1].level + 1: raise ValueError( f"Incoherent hierarchy: cannot start a level {level} section after a level {self._outline[-1].level} one" ) diff --git a/fpdf/html.py b/fpdf/html.py index 57cfaa8ca..740362bad 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -305,7 +305,7 @@ def handle_data(self, data): self.put_link(data) else: if self.heading_level: - self.pdf.start_section(data, self.heading_level - 1) + self.pdf.start_section(data, self.heading_level - 1, strict=False) LOGGER.debug( "write '%s' h=%d", WHITESPACE.sub(whitespace_repl, data), @@ -322,7 +322,7 @@ def handle_data(self, data): self.put_link(data) else: if self.heading_level: - self.pdf.start_section(data, self.heading_level - 1) + self.pdf.start_section(data, self.heading_level - 1, strict=False) LOGGER.debug( "write '%s' h=%d", WHITESPACE.sub(whitespace_repl, data), diff --git a/test/html/html_unorthodox_headings_hierarchy.pdf b/test/html/html_unorthodox_headings_hierarchy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d6ec94d0fbcb902e3c81c9d0a008630adcbc044f GIT binary patch literal 1359 zcma)6&ubGw6xM^H;}1O4gPx|)(uy{_v%j({A&|{xF<41V0@a2dw#g)J+02&R38tPZ zh$q3Tf`32@g8mEAoMD-S@jOZmaOCv4*4vwCj6zIKpiiBD$sL6cd{o$(a`XZEbB*EY-a} zjZv|PDC-J1B?DXAq^#88e#j=6SaN-S53^wq06=ZX1E0|t=LH`cLoIt-;gDmg#%MVV zh69#_V=T&roP@OOaW4q_8Cp^DaW{-p#$&9dxDkA&2n`yC!>9waMix@^k--yNQ#{uX zIE|q8z~j`Rov=%hv`kr_Kfy#+#IBe})El7DJGJrahjOUuD~mrP^ziVh+LE=es$9QN zZEjw9e(}az`~qFy{=9VO)y(DXkBe((clN*64-V~5M_=rNBWHH5Qrg?AH)rQA?X1pT zd%gSe{QjGb;C+AnD)~0^VfW1Mo43yisV9UJ(e+HVwii=3gnp%#KcMkaBOG|3VueB7 zKI>y?#b+fJ`)L44&EYy9pQR8NOps_SFfxJ&vHl7o;0p!GC0H;wKme&8g`FnlxGjOf zfSZn>g6aTNwI|Q=1?!VG|cB2uu>i9Ge0fg!bUwO5T&oQr~1?MDD*ZGLjq$ zI}Cxj=$jO`eoTa4(tb1c)OzhY4c|dWYM|(Y4)=XJeNijnqN;nk-%!iggErp(? zAY#n^a)6`6=r~|Hyy)B|mSGV^CB%|ptrBZNmKSbi3LHhWhh$6$5(&w|hOVnR?&0GY z^dS~z$1w{I_e4w~aIz<3)^toWrtx$Gj^o5$sADRYSEhMQWjfDNU>Z|A9(jI1qw%%X z^q*1Duha-bj>XLbeyOrv2xB?" in caplog.text pdf.write_html("

") assert " Unexpected HTML end tag " in caplog.text + + +def test_html_unorthodox_headings_hierarchy(tmp_path): # issue 631 + pdf = FPDF() + pdf.add_page() + pdf.write_html( + """

H1

+
H5
""" + ) + assert_pdf_equal(pdf, HERE / "html_unorthodox_headings_hierarchy.pdf", tmp_path) From d3df9b0bdfaf9520f6123decb065637d9871a7be Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 09:15:38 +0100 Subject: [PATCH 4/7] add CY-Qiu as a contributor for bug (#639) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index c25fdefea..648c12745 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -745,6 +745,15 @@ "code", "bug" ] + }, + { + "login": "CY-Qiu", + "name": "CY-Qiu", + "avatar_url": "https://avatars.githubusercontent.com/u/23075447?v=4", + "profile": "https://github.com/CY-Qiu", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e4f9d68a4..fbd231382 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ This library could only exist thanks to the dedication of many volunteers around Anderson Herzogenrath da Costa
Anderson Herzogenrath da Costa

💬 💻 Yi Wei Lan
Yi Wei Lan

⚠️ CpDong
CpDong

💻 🐛 + CY-Qiu
CY-Qiu

🐛 From 4d48095b0d9acecb1a1afdd93d51ae8ae4b1ef77 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:42:11 +0100 Subject: [PATCH 5/7] Ensure fpdf2 compatibility with Python 3.11 (#567) --- .github/workflows/continuous-integration-workflow.yml | 4 ++-- README.md | 2 +- docs/Logging.md | 2 +- setup.py | 1 + test/test_perfs.py | 2 +- tox.ini | 3 ++- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index f3245ebe3..075aa8bfc 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -12,7 +12,7 @@ jobs: test: strategy: matrix: - python-version: [3.7, 3.8, 3.9, '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: @@ -30,7 +30,7 @@ jobs: run: sudo apt-get install qpdf - name: Install Python dependencies ⚙️ run: | - python -m pip install --upgrade pip setuptools + python -m pip install --upgrade pip setuptools wheel pip install --upgrade . -r test/requirements.txt -r docs/requirements.txt -r contributors/requirements.txt - name: Statically checking code 🔎 if: matrix.python-version == '3.10' && matrix.platform == 'ubuntu-latest' diff --git a/README.md b/README.md index fbd231382..4df502299 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ pip install git+https://github.com/PyFPDF/fpdf2.git@master * Usage examples with [Django](https://www.djangoproject.com/), [Flask](https://flask.palletsprojects.com), [streamlit](https://streamlit.io/), AWS lambdas... : [Usage in web APIs](https://pyfpdf.github.io/fpdf2/UsageInWebAPI.html) * 1000+ unit tests running under Linux & Windows, with `qpdf`-based PDF diffing, timing & memory usage checks, and a high code coverage -Our 300+ reference PDF test files, generated by `fpdf2`, are validated using 3 different checkers: +Our 350+ reference PDF test files, generated by `fpdf2`, are validated using 3 different checkers: [![QPDF logo](https://pyfpdf.github.io/fpdf2/qpdf-logo.svg)](https://github.com/qpdf/qpdf) [![PDF Checker logo](https://pyfpdf.github.io/fpdf2/pdfchecker-logo.png)](https://www.datalogics.com/products/pdf-tools/pdf-checker/) diff --git a/docs/Logging.md b/docs/Logging.md index 4c5b17fff..61d54b248 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -64,6 +64,6 @@ fontTools.subset [INFO] glyf pruned ``` You can easily suppress those logs with this single line of code: -``` +```python logging.getLogger('fontTools.subset').level = logging.WARN ``` diff --git a/setup.py b/setup.py index 749446bce..7809e7d02 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", "Topic :: Printing", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/test/test_perfs.py b/test/test_perfs.py index c25720808..26f41d2ad 100644 --- a/test/test_perfs.py +++ b/test/test_perfs.py @@ -9,7 +9,7 @@ @pytest.mark.timeout(40) # ensure memory usage does not get too high - this value depends on Python version: -@memunit.assert_lt_mb(171) +@memunit.assert_lt_mb(178) def test_intense_image_rendering(): png_file_paths = [] for png_file_path in (HERE / "image/png_images/").glob("*.png"): diff --git a/tox.ini b/tox.ini index b0cb3eb7b..2d487a449 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ # [tox] -envlist = py37, py38, py39, py310 +envlist = py37, py38, py39, py310, py311 [gh-actions] python = @@ -15,6 +15,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] deps = -rtest/requirements.txt From 22f5e1f2a183419c732f399d07c0f585630e8145 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:38:39 +0100 Subject: [PATCH 6/7] add Markovvn1 as a contributor for code (#645) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 648c12745..80bc8ca2b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -754,6 +754,15 @@ "contributions": [ "bug" ] + }, + { + "login": "Markovvn1", + "name": "Markovvn1", + "avatar_url": "https://avatars.githubusercontent.com/u/32509100?v=4", + "profile": "https://github.com/Markovvn1", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4df502299..fcb5c2133 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,9 @@ This library could only exist thanks to the dedication of many volunteers around CpDong
CpDong

💻 🐛 CY-Qiu
CY-Qiu

🐛 + + Markovvn1
Markovvn1

💻 + From a961d742cb04f3831a63078bad1e46a7437af315 Mon Sep 17 00:00:00 2001 From: Markovvn1 <32509100+Markovvn1@users.noreply.github.com> Date: Sun, 25 Dec 2022 20:33:18 +0300 Subject: [PATCH 7/7] Fix performance issue with adding large images with `FlateDecode` image filter (#644) Co-authored-by: Vladimir Markov Fixes https://github.com/PyFPDF/fpdf2/issues/643 --- CHANGELOG.md | 1 + fpdf/image_parsing.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e015a100..004ab4140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * `write_html()` now generates warnings for unclosed HTML tags, unless `warn_on_tags_not_matching=False` is set ### Fixed * a `ValueError: Incoherent hierarchy` could be raised when using `write_html()` with some headings hierarchy +* performance issue with adding large images with `FlateDecode` image filter ## [2.6.0] - 2022-11-20 ### Added diff --git a/fpdf/image_parsing.py b/fpdf/image_parsing.py index 8d3684bf3..cc54ac82b 100644 --- a/fpdf/image_parsing.py +++ b/fpdf/image_parsing.py @@ -172,15 +172,17 @@ def _to_zdata(img, remove_slice=None, select_slice=None): data = data[select_slice] # Left-padding every row with a single zero: if img.mode == "1": - loop_incr = ceil(img.size[0] / 8) + 1 + row_size = ceil(img.size[0] / 8) else: channels_count = len(data) // (img.size[0] * img.size[1]) - loop_incr = img.size[0] * channels_count + 1 - i = 0 - while i < len(data): - data[i:i] = b"\0" - i += loop_incr - return zlib.compress(data) + row_size = img.size[0] * channels_count + + data_with_padding = bytearray() + for i in range(0, len(data), row_size): + data_with_padding.extend(b"\0") + data_with_padding.extend(data[i : i + row_size]) + + return zlib.compress(data_with_padding) def _has_alpha(img, alpha_channel):