From 2d51797d93dba1fde4d4cae1baafec3c05126bcd Mon Sep 17 00:00:00 2001 From: Jan Rydzewski Date: Mon, 8 Jan 2024 02:11:55 +0100 Subject: [PATCH] Colspan and rowspan support in PDF --- example.docx | Bin 846072 -> 846064 bytes example.html | 31 +++++++++++-------------------- example.pdf | Bin 6743 -> 6850 bytes example.py | 14 +++++++++----- poetry.lock | 13 ++++++++----- pyproject.toml | 3 ++- red_tape_kit/pdf.py | 16 +++++++++++----- 7 files changed, 41 insertions(+), 36 deletions(-) diff --git a/example.docx b/example.docx index f48528a47c0fc940dcfa3605f81d875d5cd3d658..8b60bced19cb88c19f8ae98fb52a5b80e559ce38 100644 GIT binary patch delta 472 zcmexy(fGqf;|(8#IonUY;1@GsU=Z8DVs8}9}YLY79YJlQUHSC*jsT(tKDQG0;=M)zt=4pbB zRM7gXRGyKqkepbQT5PYNQIuZ}l2lMj%q_51KvU{HSwO{Vvb8{F=s{b zOz)545uMKej7N(xXu8!i9(%^x=~d5ooS3sG@l2mEiAQ95>T@1}={qL#2yin%K?CEW z>HnVbcr&t2_j=Cb$~brWgy%qwVbd=>=dop+IGyhWkUBHn2Sgp4-UXzXPaNWz{`L@$ Ygjj$#D;vldQ6P+DWngH0%md^B08Eac$p8QV delta 368 zcmexx(fG$j;|(8#IhlLDsmmEKFvxBGDICNszSUESQx&KeIHC%b5bOj6~V?5FAuQU#WS$V*S|Q+J)rtdlr-iN>wT9=gjX_vt z+KO=S^odV+0vPqDKYGIB$!I=Z=P8d3%f^?GkxPz9y`XO=^vi* zI5I{}H+sfn&)7P>=oya_OYtNghRKZ{!qZPbeP@)5V_y_3=)3dd}m@ zxO95QbD)N}=_j7^*fP$X&h`RGU779zqk9mMX0MYP*FaQ7m diff --git a/example.html b/example.html index 5c17d73..04f48de 100644 --- a/example.html +++ b/example.html @@ -93,7 +93,7 @@

- n + number addition @@ -104,22 +104,22 @@

- 0 + of 0 - 1 + of 1 - 2 + of 2 - 0 + by 0 - 1 + by 1 - 2 + by 2 @@ -141,24 +141,21 @@

0 - 0 + 0 (colspan) 1 - - 1 - - - 2 + + who cares? (rowspan & colspan) 3 - 0 + 0 (rowspan) 1 @@ -171,12 +168,6 @@

2 - - 2 - - - 3 - 4 diff --git a/example.pdf b/example.pdf index 75dd4bdbbca94187b1fafdfd996722329b623026..0cddb4669d01bce4e2ea69edf3e3b43b710a53a6 100644 GIT binary patch delta 2321 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 diff --git a/example.py b/example.py index 3be7b78..db0fe19 100644 --- a/example.py +++ b/example.py @@ -68,16 +68,20 @@ Table( head=[ [ - 'n', + 'number', 'addition', TableCellSpan.COLUMN, TableCellSpan.COLUMN, 'multiplication', TableCellSpan.COLUMN, TableCellSpan.COLUMN, ], - [TableCellSpan.ROW, '0', '1', '2', '0', '1', '2'], + [ + TableCellSpan.ROW, + 'of 0', 'of 1', 'of 2', + 'by 0', 'by 1', 'by 2', + ], ], body=[ - ['0', '0', '1', '2', '0', '0', TableCellSpan.COLUMN], - ['1', '1', '2', '3', '0', '1', '2'], - ['2', '2', '3', '4', TableCellSpan.ROW, '2', '4'], + ['0', '0', '1', '2', '0', '0 (colspan)', TableCellSpan.COLUMN], + ['1', 'who cares? (rowspan & colspan)', TableCellSpan.COLUMN, '3', '0 (rowspan)', '1', '2'], + ['2', TableCellSpan.ROW, TableCellSpan.ROW, '4', TableCellSpan.ROW, '2', '4'], ], ), ], diff --git a/poetry.lock b/poetry.lock index c125cfd..2cb7e90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "beautifulsoup4" @@ -173,13 +173,12 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "fpdf2" -version = "2.7.6" +version = "2.7.7" description = "Simple & fast PDF generation for Python" optional = false python-versions = ">=3.7" files = [ - {file = "fpdf2-2.7.6-py2.py3-none-any.whl", hash = "sha256:2a2fd8f1416cbdad49cc2e54e69c52bbc8ee3339576492effd2e57d48ced48dd"}, - {file = "fpdf2-2.7.6.tar.gz", hash = "sha256:116eb372660d3da55aa0200c09ddca8cd09dae20084e87f08ebc82c9bfe8466e"}, + {file = "f099a32bdc5f21e468183a83ff700d5dd7570c1f.zip", hash = "sha256:c040eb37a73dff70c1206bf055b1703b45bf315e7908d966f39881b0716b5e78"}, ] [package.dependencies] @@ -187,6 +186,10 @@ defusedxml = "*" fonttools = ">=4.34.0" Pillow = ">=6.2.2,<9.2.dev0 || >=9.3.dev0" +[package.source] +type = "url" +url = "https://github.com/py-pdf/fpdf2/archive/f099a32bdc5f21e468183a83ff700d5dd7570c1f.zip" + [[package]] name = "freezegun" version = "1.2.2" @@ -547,4 +550,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "6bffb5919dba812f7bce445b813225cbb8a9e5780f178ea41e00a88b7e82e3a6" +content-hash = "77e8c62cf7d7fedb189835c15ed1a82f05f578110b275c65a6cd33d68c8b64fb" diff --git a/pyproject.toml b/pyproject.toml index 48ab048..a12c692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" -fpdf2 = {version = "^2.7.6", optional = true} +# TODO: update to official release once PR https://github.com/py-pdf/fpdf2/pull/1088 is released +fpdf2 = {url = "https://github.com/py-pdf/fpdf2/archive/f099a32bdc5f21e468183a83ff700d5dd7570c1f.zip", optional = true} python-docx = {version = "^1.1.0", optional = true} beautifulsoup4 = {version = "^4.12.2", optional = true} diff --git a/red_tape_kit/pdf.py b/red_tape_kit/pdf.py index f480737..d72e3c0 100644 --- a/red_tape_kit/pdf.py +++ b/red_tape_kit/pdf.py @@ -118,19 +118,25 @@ def add_paragraph(self, paragraph): return self.add_inline_element(paragraph.text) def add_table(self, table_data): - with self.table() as table: + with self.table( + num_heading_rows=len(table_data.head.rows), + ) as table: self.add_elementary_table(table, table_data.head) self.add_elementary_table(table, table_data.body) return True def add_elementary_table(self, pdf_table, elementary_table): - for row in elementary_table.rows: + for ri, row in enumerate(elementary_table.rows): pdf_row = pdf_table.row() - for cell in row: + for ci, cell in enumerate(row): if isinstance(cell, TableCellSpan): - pdf_row.cell('') + pass else: - pdf_row.cell(cell.plain_string) + pdf_row.cell( + cell.plain_string, + colspan=elementary_table.get_column_span(ri, ci), + rowspan=elementary_table.get_row_span(ri, ci), + ) def add_unordered_list(self, unordered_list): orig_left_margin = self.l_margin