From fc2c0b314e333d786a3735375c9fd026e6619e3c Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 20 Feb 2023 23:13:37 +0100 Subject: [PATCH] Implement FPDF.table() - close #701 --- docs/Images.md | 2 +- fpdf/fpdf.py | 5 + fpdf/table.py | 88 ++++++++++++ test/table/table_simple.pdf | Bin 0 -> 1507 bytes test/table/table_with_fixed_col_width.pdf | Bin 0 -> 1507 bytes test/table/table_with_fixed_row_height.pdf | Bin 0 -> 1489 bytes test/table/table_with_fixed_width.pdf | Bin 0 -> 1503 bytes test/table/table_with_multiline_cells.pdf | Bin 0 -> 3146 bytes ...h_multiline_cells_and_fixed_row_height.pdf | Bin 0 -> 3118 bytes test/table/table_with_varying_col_widths.pdf | Bin 0 -> 1504 bytes test/table/test_table.py | 128 ++++++++++++++++++ test/text/test_cell.py | 23 ++-- test/text/test_multi_cell.py | 4 +- test/text/test_multi_cell_markdown.py | 9 +- 14 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 fpdf/table.py create mode 100644 test/table/table_simple.pdf create mode 100644 test/table/table_with_fixed_col_width.pdf create mode 100644 test/table/table_with_fixed_row_height.pdf create mode 100644 test/table/table_with_fixed_width.pdf create mode 100644 test/table/table_with_multiline_cells.pdf create mode 100644 test/table/table_with_multiline_cells_and_fixed_row_height.pdf create mode 100644 test/table/table_with_varying_col_widths.pdf create mode 100755 test/table/test_table.py diff --git a/docs/Images.md b/docs/Images.md index ea9050650..06bfb140d 100644 --- a/docs/Images.md +++ b/docs/Images.md @@ -56,7 +56,7 @@ When you want to scale an image to fill a rectangle, while keeping its aspect ra and ensuring it does **not** overflow the rectangle width nor height in the process, you can set `w` / `h` and also provide `keep_aspect_ratio=True` to the [`image()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.image) method. -The following unit test illustrates that: +The following unit tests illustrate that: * [test_image_fit.py](https://github.com/PyFPDF/fpdf2/blob/master/test/image/test_image_fit.py) * resulting document: [image_fit_in_rect.pdf](https://github.com/PyFPDF/fpdf2/blob/master/test/image/image_fit_in_rect.pdf) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index d1c9e3066..7e4764253 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -84,6 +84,7 @@ class Image: from .sign import Signature from .svg import Percent, SVGObject from .syntax import DestinationXYZ +from .table import Table from .util import ( escape_parens, format_date, @@ -4532,6 +4533,10 @@ def _apply_style(self, title_style): self.text_color = prev_text_color self.underline = prev_underline + @check_page + def table(self, *args, **kwargs): + return Table(self, *args, **kwargs) + def output( self, name="", dest="", linearize=False, output_producer_class=OutputProducer ): diff --git a/fpdf/table.py b/fpdf/table.py new file mode 100644 index 000000000..374b523ae --- /dev/null +++ b/fpdf/table.py @@ -0,0 +1,88 @@ +from numbers import Number + + +class Table: + def __init__(self, fpdf, line_height=None, width=None): + self.fpdf = fpdf + self.line_height = line_height or 2 * self.fpdf.font_size + self.width = width or fpdf.epw + self.col_widths = None + self.rows = [] + + def __enter__(self): + return self + + def row(self): + row = Row(self) + self.rows.append(row) + return row + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + for i, row in enumerate(self.rows): + lines_count_per_cell = self._get_lines_count_per_cell(i) + row_height = max(lines_count_per_cell) * self.line_height + for j in range(len(row.cells)): + cell_line_height = row_height / lines_count_per_cell[j] + self._render_table_cell( + i, j, h=row_height, max_line_height=cell_line_height + ) + self.fpdf.ln(row_height) + + def _render_table_cell(self, i, j, h, **kwargs): + row = self.rows[i] + col_width = self._get_col_width(i, j) + return self.fpdf.multi_cell( + w=col_width, + h=h, + txt=row.cells[j], + border=1, + new_x="RIGHT", + new_y="TOP", + **kwargs, + ) + + def _get_col_width(self, i, j): + if not self.col_widths: + cols_count = len(self.rows[i].cells) + return self.width / cols_count + if isinstance(self.col_widths, Number): + return self.col_widths + if j >= len(self.col_widths): + raise ValueError( + f"Invalid .col_widths specified: missing width for table() column {j + 1} on row {i + 1}" + ) + # pylint: disable=unsubscriptable-object + col_ratio = self.col_widths[j] / sum(self.col_widths) + return col_ratio * self.width + + def _get_lines_count_per_cell(self, i): + row = self.rows[i] + lines_count = [] + for j in range(len(row.cells)): + lines_count.append( + len( + self._render_table_cell( + i, + j, + h=self.line_height, + max_line_height=self.line_height, + split_only=True, + ) + ) + ) + return lines_count + + +class Row: + def __init__(self, table): + self.table = table + self.cells = [] + + def __enter__(self): + return self + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + pass + + def cell(self, text): + self.cells.append(text) diff --git a/test/table/table_simple.pdf b/test/table/table_simple.pdf new file mode 100644 index 0000000000000000000000000000000000000000..174442a4f732c5848989855d31329e42ae98abc2 GIT binary patch literal 1507 zcmah}eM}Q)7_V*)ICLM%<_E*QQxQ!D*Xtc^&o!{p(u_evZ5;^Z!-Kw*BjpZz*H|cM zGFhDrH8Po|6SHZkpK(r2bQ)3AuuR5+1IJ9GlMUGb-7pb#V8Y%jAkF+^??3O|?|q-& z^Lw8>zbDqDH^dVOBt`%^;ET#IjRwPYyo(b7f#K<_O#oIB9hvhn+z4$flft`!RgREE zIx#_^25O2(P}AsiO=SfHxQVet0ZU54AZL^46d|;k6_C>wi!9Ggasu4Iithu?zcKBCQTagnC%a+aQK#K+Z0fq9UlJC_!{WrW|wc-PYK_ z1>~>R{=E@f{6*x_y5xrKX`OG@zc_2|1=)4=;pp_2mPM!DPRfdJXwSDiT>tatn=>At z%0D@BX9{OVfE#GuLQ2_-qg8gf6kHqvTMVzY58k^w8rXWH=>UJK6*A#|H<`_ zV=7yQjZOZA$5*&y23=PV=XtXEN!~9F_Q=8;tgqK+4!c8AjacuuCzkBU(nP%M>n$^f zU7DvkbpPgPsi*c(a(hnQU|GvBGs*qlLtB$4DbO1^&sVVX-~>sPISf!yHQ(IYc(vAl zrf=Z#7MHK&tiO9k_#9(nTdhC4Z=f!&RZHd>2W7Qh-$ifJ{a+u2n>IC$|I|HNc5XP| zZd}se8oN2Hn`#^XF~F$)^88R*mA((J?#bzzUD&v4=7BGVF5FL!I4}^8p2OOWWVK~5 zy#6Eq{?>ZL{!d-7EJg|PWsdE;JsNkI$Le+%aFW(qw~8nkq4*J5v||8hF&-_!_nx3h`#XoI$QhpWsdR1~PGWmpYC5{jSNalEE0Eu3s!3_kEGMiDu<0dCxY=I)M;%I=N+|Z2_ zq?IEVrA+R+$vrJUgjN+ot3#fVhJha03arqtB4{N|C`f{!2N~=}KNrO(8LMl}$ gBtcVZrCgo#|2KkF7DUD=2Frx1sKjDpQ!@?NzwgBwPyhe` literal 0 HcmV?d00001 diff --git a/test/table/table_with_fixed_col_width.pdf b/test/table/table_with_fixed_col_width.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5d4ac8ad909f37f95cd7fdfb5e1c1715f3be87a0 GIT binary patch literal 1507 zcmah}YfKbZ6gGjfGSbScB}K5eraS~;c6N7QN01`>h$|wnND&dp0GDN9cLru=13qfo zgfu>Y6kBTrCGt#4R8k)$mViYK&{EtAR)VMvAyTlSpe0Zmuy=Uqru^vrGc(`0=R4n= zobUL=skK2wupA?R6tEe&Sa>*wt5^#o00P6KjafWMk)x146~kj-mXVHPi$IDLAtXfx z%alN=AcBDO&L+*YxRr(8G#3i#hf^rL!ZkGddRaD zE)((~#FkPlZo?<0F7s)PCINCttBDq%8fLOt5W{ys#vtUNCMd-$Uf>{Y#)?jtp6Fo) zrOw6A4q@vn*90D|J5;9o)vLt)4bRq9x0T6o#tViJ}5Y;rD1gVT*oE5aYLBL zV2rPM<26dZ zkoLkU{i`tN+Ul|BmJjEA4_+|oE@_$`lh%ott9Fy`R^+NC_9mBW^C-u+s)q77$#u8Z zxc(#K0~Y&uWw)mBd3M_<-J0!kvYk{#3YTwfxz;lOrK@LpdBd@^-}0)44senU3ir9O zd&7~ArL3+`v-ZzLY8TH6&uGWerfq#!PTNbY6XaDp7th@J(X|&2Y;lbo3U~?Q2P(&E z3eS+ozLE4ht;&h_m@0av{EY%LD^d7w3cdNn> znecRVNlimzLrF(c-QRn5{@5-*>nq;t`C}M7ZJG9-UFYN6)BCvc{o3ZK3e9eBx&6*H zc8S(&UUN<91LeLYX?OFgXv;63`M(+S-n4lpqQ1G#v9@!n!m!($2isK%=U3MU2KE*Q z`L(yp&y`Wm_0=QloVCp_B_Ut@lr36mX~33DMcPbf6wN~|i{=~OWi&%RNY9#STuT74vK|dQ9>=kn z2~YqjI9ed6HY`F4bY|qDsHLZB>8VS#p$)a6RUyxa%Rr561^m^U2`YplWO9O_r0DD+ zC_kyxZ=+3e5eKs|DNtf!eDQz~l9ZDm8@$FSun+>wYm7pFBOV)!pjO_Kg|5UX#R`m& zDwO}_BM7A8RSkiojV8z~?9v3|euyOFdX^P{xDC-3(=pjB5L+R>t3gV*hSDk(p&B(M t(`czEN<}JU8U?9T$>bWTTuX#R{Qr%GF7pD-2@7pPT@1l|d^E9I>|gY*7SsR$ literal 0 HcmV?d00001 diff --git a/test/table/table_with_fixed_row_height.pdf b/test/table/table_with_fixed_row_height.pdf new file mode 100644 index 0000000000000000000000000000000000000000..81b59a94d6f661cbe1f6c1507bf6248176c4f64a GIT binary patch literal 1489 zcmah}ZA?>V6gIlJctzr(G8naI~4X_K1Mfx?EHD}ea?HH zbKaBloKTHIbr6pdAUMDPlbMT1BnT>JoHPq?1bxe5asYz>y0k_FO{Pp1QpOYj0|pU7 zOhoZTKopHfiNw>9{7(R70962ND;DP_&7o(8Ls-`J9V|Ci-HP`{T^0XUCiV!O*9l_2{kcSbJHKDe}Zc<@L{lV|7ds`~yLro&w+zdX!~dG*xI;PY`KMcARS zlHQK5T)KpbiZAMyju#hcWcRv0Qmp8TXJh(nZl8NQXtpD8sNPz)@+W9`!F%jphRZmX zCXYSmA8_4oN^mSUC{!O=*IQGy?^*Yg`Oe1a>cD56OW}Y0IMIGzdAIp9)p)?gT&a$)KiX1e9_sz!rYGA10e#qUQe$8-g zf1I=$sGjRv?75xLc5-xd=}um8Q-rcI`cs_>4UVqF(zli7W_|t8+gZP(HS&pdMOd%EOoZJJ04oVqxjos?sKz8|;hZQxI?49i9 z?Pz&j+A~K#sNC=J9LNm%+`L6`_4|dZjmIOJz5A&C8U4${f=u+azT$+?dA@P>wb%kx$2TDrn#*2W?2|vdm$o8lh1GMe{uyIg_DfND z6{X|E(~+fFj2HjqjJuY9V)0|?g5O2wme$L^4e(Yps=jgWLMz_IT?)e$ECUyKw_}O+E%WXqe*2gc z&iXj5ONj89MH%U!R1BPEG}U6G90#?Gjf5Ie(r_v*v>Bjj7FtR>EE@oV>YSPEsxmo! zLP@X5l;iLjcTNibj5|j!k)SFZaGAAmyitvv$;tRTS#H6vR;>iIZD6dOv{)(o>gLp0 ziYQnzs%01!aJvugtOPon0bDD%Zv`+&lmeMtp->71QlW_8V>l)yV6n28n-LZ@&YzZj%h7F2?a(nwlyA(EBs3JB%%cq^N3tUR2y_ekHM(lu~ z*6>XxVK~3KMF&IFbQ>c$4P?uRGL%6#79ui0rxRR27_bp^xc3$q-Tblh=e_qi?|II7 zPtJ3K5@S?6EL@0SfDfqb{fJzSpi0U@G5|x+ctfrk=!DRv*@vKNBG-T`s3M@_LqseS zhYO@YD#F60GU!b{SV#ag5!VxDBq|Ca$Xu3A;6|HJNRZJvjDaFoIRUCNFuMS0F&ZHO z8@dNSNLd(wCXj@ZGFnXJ3Ty^!d78B(N}R!sl-?~3TjF~4Btwvl8HiS55^);-t}$o` zGi9N32s4nlQ?kk3_}I`jK9#}95HzgSh%-bCkwfJY2)c(L^-Ml&f{2YWGcjwR!TCKJr8V z{I_&X=?=tDFiR~AR66>1x9ooP=tODdr(}vmqjQGvE^#`gH?AbLjZ~&Zccl&YAoIow z?)bZ$8{_qNlH)_q`z3WWU96oL?LPisO6uP|zSMVSV)>?euDa!%=%`VhF0q##(Z8*m zmbRx{X$)?Ce0SIFC&3Q~nj4A1br-L;(K7hQvcL7$1BvCIf3*Fueb#fE+`~#=zgfo1 zV7vmFIfA8#oFmDi`~t`-g3vqo^!7x416OjR!?u4@d{|!+PyTf`EbbT z`(Y_1!mRnvGTSQV#_ONsL}nbfkBuxwzo2&X{21$aDDIz${OUE}Q@vdHSLs<>bH}~O zv*imX^^46T9PcW1ZIi7z?cQV+cVgVPEBJt?ExYg0FWqt@ZrIj{U)|DODT z5c(+&@MuWyU>$>hY<<=#3u zKL17LoYw_yU75zXba`s_&xWyjZcV^YfgQK>auTc5?ST!M8OOL`_K%O~)6dYJ4cUDs z1~}x??Qh6?H?uNY9j;YDiQOhsftv{x1!o&gHkb%APeYk-sF+w1&ZvRZ12oM*Mw4d4 z8i1f$OE%-uDXUf}@D z!Us}>^{*I^h{ZxN$OG$`47gl?T*qYaHLST|nC#hmf{14^nFv1l-?1291Xsgt}dGyoMv2Yf?X6KK|!$zD&%hz3?GjG literal 0 HcmV?d00001 diff --git a/test/table/table_with_multiline_cells.pdf b/test/table/table_with_multiline_cells.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3cc55e00af20a79887964daa03068a1f345d7b0c GIT binary patch literal 3146 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 literal 0 HcmV?d00001 diff --git a/test/table/table_with_multiline_cells_and_fixed_row_height.pdf b/test/table/table_with_multiline_cells_and_fixed_row_height.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4ba56376b9b06384c9fae8487593b62d10408811 GIT binary patch literal 3118 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 kk}yKrPz{+bMNCClJ-jCP;hGppP4BZ)Eml(jN zfntk-BBj(C3srnkTUwJw5e=0#JeoqOq!tQFqzK8DvZ97ZOYiWg+wi0J&&+)1p6{GF z`Mx7cP;341011Kv46xY+h*F85Dz=E>0gj+CbS?)nBrv4QLeMxWmnI`w3&_AAA|qr0 zfk7ZhiU$M{Fq&vBqyU;g8Ym744Miv>SD*`Ypp7r2n1~#nW|?(PfNE*}0APxYMo1u} z#*$XHhzDpqL#bF}k%?J@IUuyB+bE(UdD6%l9LfkSzUZFeDTe2ObS)==G{esg1|7w* zMdloe19C@7p|}H|kh;OArHwpghP@g|o>EgeY%YbMu@qzA^MxS@SsZVsNE2c?Q**kH z8Nu8%&;LdOMqU;-ZHw;n&3{dM%&DfVB7gMfHj(qm8t?MMTZd~~cIoHMkKYe*J$uR` zF8(<3SKac6Qa;i5)%M<=X15>Ex$F0h;!AB8>l=pOOV0ehT{mQLzxK|N&o8GBXEe6n zv5S0DyO#^aST-$667`mfV|kS)bgFKd~bCcE6H?M08YUtf#&W1AAxYViKHhgL|_TW)< z-H`j~MA`30g3p9)sZW)jxX?KgSen+D*8TKhGB`Gayg8b6eCcS=jdq_|k6ZitJDkI< zPK0Ol;gN3_6muLEGv;5K)M$0BwY%6QCks^bnQ3(?Z!K)!(cz3)ou;ch6UM85>n>3r z%Ks=TY0T!a|MmW>6N`mnx1C-sk}1V_d4g(4Y%dd-`Q&(b?~cJk>dOT) z`K6k%Ym+et{ybjM1EF^p%$|2u&pV^STGY+fuNRq~$0jmwXL764px)p#GuhgD`>8m@ zUOCtY`@VWmRC~T|etO`$+pWtD9pwBs*ppYT=(pz5zW0u$-e~t`f3b`9SgP(_a%q=l zAFO=#@Y%{s>d*T23O-s_*Se!*4$_PyIZDeyaifW}iQ@cqtciq{(J)Y?G-Cj03e7|? z9K8V`sJKB~xulfagr4rTRK&Y&PYDW{yY>uAdK?Vx92dqt5 zU z9)f`&M2N2)AeYG`GLQ#eU<6nV0p$ zHZa~y(niX>x=8hODFs_bbu7yRVHLs^7R}_bKp2Jat_B&(K#fYHl@JPz0*k;jkqQkF lA(v|jTrMTl5t2wO^#5