diff --git a/.all-contributorsrc b/.all-contributorsrc index f009e5635..80bc8ca2b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -741,6 +741,25 @@ "name": "CpDong", "avatar_url": "https://avatars.githubusercontent.com/u/93034081?v=4", "profile": "https://github.com/Bubbu0129", + "contributions": [ + "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" + ] + }, + { + "login": "Markovvn1", + "name": "Markovvn1", + "avatar_url": "https://avatars.githubusercontent.com/u/32509100?v=4", + "profile": "https://github.com/Markovvn1", "contributions": [ "code" ] 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/CHANGELOG.md b/CHANGELOG.md index 58b491914..36ea9a454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,17 @@ 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) 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 +* `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 * image (``) without `height` attribute overlaps with the following content [#632](https://github.com/PyFPDF/fpdf2/issues/632) +* 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/README.md b/README.md index 653c7fb52..fcb5c2133 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/) @@ -219,7 +219,11 @@ 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

💻 🐛 + CY-Qiu
CY-Qiu

🐛 + + + Markovvn1
Markovvn1

💻 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..61d54b248 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: +```python +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 0656ea329..4b6b5e769 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} " @@ -4272,7 +4304,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, @@ -4284,7 +4316,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 3199ca378..acb774217 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), @@ -809,8 +809,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/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): 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/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/html/html_unorthodox_headings_hierarchy.pdf b/test/html/html_unorthodox_headings_hierarchy.pdf new file mode 100644 index 000000000..d6ec94d0f Binary files /dev/null and b/test/html/html_unorthodox_headings_hierarchy.pdf differ diff --git a/test/html/test_html.py b/test/html/test_html.py index 2cc8a3a36..a6e43a6b7 100644 --- a/test/html/test_html.py +++ b/test/html/test_html.py @@ -645,3 +645,13 @@ def test_warn_on_tags_not_matching(caplog): assert " Unexpected HTML end tag

" 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) diff --git a/test/image/image_x_align_center.pdf b/test/image/image_x_align_center.pdf new file mode 100644 index 000000000..99a89b620 Binary files /dev/null and b/test/image/image_x_align_center.pdf differ diff --git a/test/image/image_x_align_right.pdf b/test/image/image_x_align_right.pdf new file mode 100644 index 000000000..b07669699 Binary files /dev/null and b/test/image/image_x_align_right.pdf differ diff --git a/test/image/test_image_align.py b/test/image/test_image_align.py new file mode 100644 index 000000000..11bfcd18b --- /dev/null +++ b/test/image/test_image_align.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from fpdf import Align, FPDF +from test.conftest import assert_pdf_equal + + +HERE = Path(__file__).resolve().parent +IMAGE_PATH = HERE / "png_images/ba2b2b6e72ca0e4683bb640e2d5572f8.png" + + +def test_image_x_align_center(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.image(IMAGE_PATH, x="C") + pdf.image(IMAGE_PATH, x=Align.C) + assert_pdf_equal(pdf, HERE / "image_x_align_center.pdf", tmp_path) + + +def test_image_x_align_right(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.image(IMAGE_PATH, x="R") + pdf.image(IMAGE_PATH, x=Align.R) + assert_pdf_equal(pdf, HERE / "image_x_align_right.pdf", tmp_path) diff --git a/test/inserting_same_page_link_twice.pdf b/test/inserting_same_page_link_twice.pdf new file mode 100644 index 000000000..bb45808ce Binary files /dev/null and b/test/inserting_same_page_link_twice.pdf differ 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/test/test_perfs.py b/test/test_perfs.py index 475dba6fc..7d2003654 100644 --- a/test/test_perfs.py +++ b/test/test_perfs.py @@ -9,7 +9,7 @@ @pytest.mark.timeout(60) # 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 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