From dff0ba2bda9f87a492255e8353766f0e474ff077 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Fri, 24 Feb 2023 13:47:25 +0100 Subject: [PATCH] Implementing table centering --- docs/Tables.md | 14 ++++-- docs/table-with-fixed-column-widths.jpg | Bin 23447 -> 18152 bytes fpdf/table.py | 40 ++++++++++++++++-- test/table/table_with_fixed_width.pdf | Bin 1636 -> 1636 bytes ...ith_an_image.pdf => table_with_images.pdf} | Bin ... table_with_images_and_img_fill_width.pdf} | Bin test/table/test_table.py | 17 +++++++- test/table/test_table_with_image.py | 26 +++++++++--- 8 files changed, 82 insertions(+), 15 deletions(-) rename test/table/{table_with_an_image.pdf => table_with_images.pdf} (100%) rename test/table/{table_with_an_image_and_img_fill_width.pdf => table_with_images_and_img_fill_width.pdf} (100%) diff --git a/docs/Tables.md b/docs/Tables.md index 616aad6d3..9ad8c2a83 100644 --- a/docs/Tables.md +++ b/docs/Tables.md @@ -28,11 +28,14 @@ Result: ![](table-simple.jpg) ## Features +* support cells with content wrapping over several lines * control over column & row sizes (automatically computed by default) -* allow to style table headings, or disable them +* allow to style table headings (top row), or disable them +* control over borders: color, width & where they are drawn * handle splitting a table over page breaks, with headings repeated * control over cell background color - +* control table width & position +* control over text alignment in cells, globally or per row * allow to embed images in cells ## Setting table & column widths @@ -47,16 +50,19 @@ Result: ![](table-with-fixed-column-widths.jpg) +`table.align` can be used to set the table horizontal position relative to the page, +when it's not using the full page width. It's centered by default. + ## Setting text alignment This can be set globally, or on a per-column basis: ```python ... with pdf.table() as table: - table.align = "CENTER" + table.text_align = "CENTER" ... pdf.ln() with pdf.table() as table: - table.align = ("CENTER", "CENTER", "RIGHT", "LEFT") + table.text_align = ("CENTER", "CENTER", "RIGHT", "LEFT") ... ``` Result: diff --git a/docs/table-with-fixed-column-widths.jpg b/docs/table-with-fixed-column-widths.jpg index 240261a96b5b3c4558cdbbe4563b9f16b6db25ea..6634b26fe82fce339d09e93f80f80782a3bed617 100644 GIT binary patch literal 18152 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 DdD;i* literal 23447 zcmc$`1zc3!)-Zm6p@xQ`bU+%0kVaC5ZWy|iZYco)5r%H0OS(H15CutTBm_l38j(&V z6#k>n^IV_j-v4*sd%yR+?|0_>POP(Puf6x$Yp*l=T)n?q1c;U8mE-|5Gyp(D{eY`k zKnB1-NB^;-HV|sZ#KFV_fiS_?*jPCDV0?T$FdiNOAt^BdAqgQK9x(+m2^oZ(oE)Es zl8ORCMG7H@Ty+6>IA}~5)M#iVz*P?b{lOmt4YdJ(Y*;A1*yw0DS04bJpJ24h@;{#a z>t`s`48Ta40suk{1L$r909@=L0RVTC7(k=80RR#&WGad)*;_gQK!4c3Df(#M#8hKp zgnM*)^gT&}e{M9>y*Jkj5BfK3cfJGB@hAQ)?>tGkk`5Ot#{qQT5R@Vs0k_Z7f-Y2b zza@vI+Vb9cjETP*rKt&^V1>|*xS_L?6(PuB?%BM1_Jd)%@M3Co_5 zd@%`tPH<4V=Y(u@o7mlX(1Au>-5d2PVrR@34ltA~T><$IZ~|^MEsonP?l~ww{L=Az zS>5^EzxrNC)B*Ayf~PbeA1o*r0km%VM<0m|>$e^Vd=a^uz9)qI&7421BX5WVK&E-j z74Y9BMw|lxsbB|y#%O{&$OE9ot^P2f=aHW+0K}9Hr2{~8ZFep`*N279tj?_8i~Kd_ z?aA%^_YdG`)YQcR<)zczGX(&V#LDo)kCub2A0JH|52z>K&EG}*QIV0Xxfz>>?QsCk zC<~41!*8vecTxdb%ktUgQ7MbW(({P?v68KKzhnD`xQdA&|FJo+ZUcal9!mdMS5~$T z0E+|pe>Aj;=Zg)^*i;8lqUDocdM&g(5Pv8=cc9$roM~;a{Dc`jqm6_9oXr3zTA2RGbOWSe*b0c zx4Qp&_lRiWbwHM}7yz>7p2hyOtF{QpzWshLl5qOn)~ByT7}<_z7e_CDtKHa{-)&8Q z2e$$Npq1O9Z2f}~$Qos5xew?@MJU@NP{qRC!9%rw{(4ExNdVPMgg%3s-ua=LM?Q*x zY|N|PRN}dYLLq?D|JX91`|NtV3AXxK?LF0bUZ@DWF(}N z%)Gad@xS{VG)(j>U?Qnhf>uLE%~ST>zjGL8l*N+SjI1fk{5KEzHWB23Q)H%^g1-)+ zdfefYe@{M2S?QdMN$~-Ry`C2rMA0_iZRef6t^kr(bv}s?XJq6IPsj(zR`L~Q0-Yz_ zqH8lAAl}FCh($#w%NBO$t0FyogbyCW)97cbWXMIlupA;o3gMbX&jcSoT6q6RLqK|- zl=aJs&6A+p%_`kCf_;7+I1s<-ep#@Zif0qly*71otjn=L~L2D>Q6ghq)f> zwivcMX;Hb|?d4J5@2+TNJ)Pw}Ep*1EjVQPc^vn`jE-bRjHSMAE-mF!(P$(N2vgO4u zZaZDPC(p&L!m3Hk^-W!Ti}NCR4Et@_Ji=8#f0!uyr={nmm!`C1gtiR&bKezNo+yO% z7%}$RFq3!2ZM`InlrC~ow(pa@eBU21)lZUR5hd*W!@ze!Y1K@?p?%`rmKjtncPk4m z(?^CWtfVMX z`0%fkWtfBX>0$`oXPCzS&Os(phUvj@(#LNquj)>lT&5pQe{wmveu16A+LrSC8nH}> z-K^QnzVl@l;)V2EMxCMD)9_mvYNNtqXA2UwED@>k7qQQKMxZgckIh)i;#i|=;?fSC zUYLbHev%M=bn6QdpT@%%my_r^;k!!wxy+we%O?h@vfwTCW~{^AD@q|^g>$ho_X>VV4jDiWBwu$+h4{v%F-lKZ#&J&9q@T+@T zOW9mC!05Bq#4Mb(%${gFammcRZ6=Us<1+eEkjaeN(LF`X;h-HO&I#Tji_(fPq6nLI zM_>Mi?RBgxfD|9sX;{(<2a?=eJnNU>AXh1~M*URSCU8j@X)xcM<=XplM*eu6V=Y=G zwaEUhI}5)_UEgo=|IuTF<=q!=Z#J6VGMaZZu6FdSc79ep{j7ERpMgw;zTdjE^aRK&BfZ=zNu`T`NTcket&zR z>T)o^vLVAV(_y(aKGBk@2r}6P36gU1P|XPjf2>O%LMJ+ z^6uAG?5mGajp2q?x#LOScji4(+Z<$(srWcwbcJQL=6yl>?2`0de5UOyAaf=~RSa%< z^O^q~$sAI%#(wS9(3&Hz&4;CxN#~fBxhAUiyZ(>zd7^jJ*H4!7<9A)w)h1yR_%Ju8B$z-RG74Or9g5B=8-{~5?*lpA64uOk1ArC!nU z@$AfB_>NJ(hK2qO;h)O<8|UOEW%$)N`&Ez2Hmz08*UV(+#igKn zJR!a7wbLH=osHM5ByNgt9G+P{g>g%|M#q#@hQm&GFS{QmWip6QGzR7*YeCtI$U{lG zOIj+XLo{&Z@3}Px{XWRhU*-ZAqH3)bsN8`Y%mN78-Drxt$f0hn0=!ugTgS z6%JKil0UmItn)N6e(41y#&A4CNaCvr2SK?2Nm}n-O8AthG9%H$-x z?qK;MbBRK_)<*UI(WT?9Eg@YKUADz6SzaC(g#Yk%4O{agF?#Z7Mh@8*?W!zyIh+S` zW*k!$1m#;ZGh9>!hxoYf7}*~%1SkauaX@wOQl{RNnaxuepeZkqaDV*Gb)AkVJB#a? zH+P)QY0a^1KUX|`%3>*aJ*{G;O9O2f^HN3+QO`&pwXCy--K`2rqFd;p4RPjcwi;)zBHtUF67t!Tc$6HiVu2htxXd8bs1}&7wLAY z(2cc2{h9;JpNo9TMSQf0#GkyWcQhcKY#67QDzp_WYQE*noAEkwX3|R0cCFFBQ6?$> zE^GVVcC*-{fy+(mC!qsP*rYZ@&j-?TO?!(&a0*OcTdRE= z=Zs%HpXMB8D}5rLIw0oQJm99+N532i8H>0AxE9{4VZ?ZeNn78S$NPSnx*+roNuwmn z<@LVwiCYaA6T8Eq-S}@q8f(y2%tK6t8iXQo{YrYhQ5?b0!>^>FeC1kJv(Drdp4F-w zk2^gF7~~qRVfX)RkN-H7CCMKM&dE8F81?AYlUMx<7v-s+$X1p)45FqPo<&q!{7V;z z>OqCxzX&Ls{;K%#UuR$t^_DkFHsK2Bc!Qd|Xrrc#AaqPD99%F61j0tmUeM4nKmZ9Q zDZOAa8L!m%#4?0I0Iq4Cn1sd1!{>J2Bd~#vUs}sOA-}${i=4^gDREHezKr(L0X7A* zCo;a^@b?)GmLzJ1vs&mu?5sOlMm1;6uB^4W6$g)F^%6lB&VIj&v;4p^o-uO%e-acd zECEf3Lf7ht9;ah$p7hZvkiCEgLe;n3L2hF8{UklR|KlK>R`oc=A_NO?^Pd+7Ki_T@ zi6%$-Y&0=T<;1t}f4b;@HKV!S?saku623BYUB^)i390cn2EN6wYz6hF8PAK?hi~^^ zS{_b)>AO1z8!@@yyGxpN5?*2aMMjxgR7sl8RW&b6Nwl|a8kpJ)caBtDW7{RH2UjLX zGU>xQ@)(%eRK@pNsalHdZH8!+kS~}c%lM%Sd$OW{|C~3QnhO5X(I0>$qTIOI#8-gLyIxBH<(LDI@0JOp9XA7~JBLpN=~=}1fHB3fY?I==2)au~g17XMpr#bXfD z;;43iHJxhjwHb{@&i%@sWNc+UND_U(x(AVLzTku~v;=<*r3PIfAxe$cPwR-HcIa&+ z4=UZvEtnPyD(DSmUw0?p9`(iyh-w;$tN_*oI;k;MF=uwz;Sc5cR9e*-k$OBhXZaHn zXrB*Ne^?!7oMpLQlQW%4M)YIu6>vLUbeEigEpd`YDjr1s{4R4B!Fqvth1*wrSNB#` z-746i&&Y}G6(SmFPag)A@ZX zJe8b28P_VzLqzmUZ`GQvcC*6|EGYD}NM!f4*oOeH70J^@kzzcC#5_+;YBt2^9kWj} ziKoe*swA`)8rW7-dBD;UkU5`6WHZ9W)WjcQLc0#C9@kos{=BlGX1Tc6C5Ma9{B&s_ zm_@;N_`(A6ZY~5j@>3U1n z)B*AP!6}rg>;050@z-PWi_JjZ((}Y>Px7spT^rU(h6q$}G6LT1KgJ%nRl;C5e?G_d zq3%Fl{%PUY@D)P!gI4<&%1Hy99)43GJobgpyCKQW<$CI6nC?c@g{qXw-TFucZiX zP}`jOil3=g!09Y;W9`0FhtOt>36Z6TrN-2l(_Qkcr%A_Kr(rP!sbz{_;ZD!rF z`jRYmi*vym{1)JQu0}c`as|9jwvwvwO!bz2e8*Ht`l)vQVr?e7!v;sMK-f$#Z~SdU z2s$Mjty!#V9k!}Bs5}Npz|XeaAmXE*9LC2sdaOgv9y+0PNiPBy(Mh575~o|EAseP$ zo+5g{bANllPx<~dZMn+~x}^ZTODH1BoSMHxyYvK~LhcSY7SUwL6I7g_!9pFW*Ad9T z#uAA&4g&)iaSNoRQvm_i%>fJwE{wX>QJ4IiHpT@8pv?QePR_eID^dq zH8FLN1-$aB)Ie9lCAb2fF^enQImxYPxVL`uQ3iLU`a^~6*<_=5Z3AEk3cevz!W~AH z=oq~e#+12yY97hb(oSWeRP&zzk~8QOG7}}MMCoIaoOPY_?st=_x%u(*B$?dB=Vf|F z*^RFx_#Xqh4hY)IE0tkVC+~vMDb6kai=sj>CaNODVFN|BAUw?b5%wGZ8wUDm^l*8* z&y;Y2Bs7Q)#&^c!-@43*?n%UDxlbcegJk-*XOHUNeFfjwzdfARQz~J;nfg6`AT!p4 zy|7VNQM;1kuLk?z+HpKo2kUG2+uTEzf8qq_CDyjQz$h} z!;T3F4?@Ze7snF^2Z?H2rJ~?xptro@-pXb-RK_6>Em`t=KUc8iGwZo$HyvR)6SBqtt@^^1)+~ArlB3!5Fe? zmlp?7TnDN@623Zp1U*G^a=8Y!s+nRO^9nHmU^a1MuL1_HB@+0cFNgKf z-KIJ25iOFoWnXsH+Xj`E##`wKwXiAjas>~CK?7pA3 zCQVJ6Re1Mtay2ZneFJM_R5!+rWWo}^Z>VyMv%$n5K!@5AU-bl78^j-qs<;>dN` zrDQ7dA^==fr^6_oK`lJCLJWOaNbw4O@x5%~^Ms+SPhOa?Nn&umzQD^D0^{yp4!BN+ zJZQu#o?XsR_M0Xwn)LW|0A`L4b}L$IDK8jIqLg;7CV70uoe6zSujjg*5}nm9p<+(4 zcbD^Xfeq}?76#WL9x;G(h1Y@)x|n^_S(ZqZ72*{IR5>@I=w3=#UIF-L6}?5PPx(?o{YfI!vTrr5YZaFwMv|us6{i5Rzq`hu2UCQfOvYje_Yaw3cUJiXL(t zDPC*lMM7J!)0I#^2gg-HD2S{}nKdgBddzX_EX( zi7~Q6VIRkRifrU2&I5<^S2`FGv`2o7v_vUbVJ2GzkxVLJ!$b>9LS&TT*i$kN1vXek z0wkR<19`DzB&AHNhMUAgWdiCc8RY{S^klqxR4d43LoLg&kI)C~n(lwY{>_W@0-lOk zY+4b>1KZvvb9`C^%<4n)Aj|ho+$8$#y2J^3NMJ<2dJ>bAc3JtMncoV7p|f29iBv`! zSc#u4VJpy@VY+!QO0lS*!*qQ8U?4hbc&%TOCR-_2lN>I%#4^CWy#aa_<@Te9Rs%0}`k}4Nfr783m(-x8GGS9y~-Va8~ zn6%2dR~9K(hLVGq%VWr>J!DDvK2?MCAJl-o<;MHktkD7-RO);FJNP{4D6W{ zdB~$zCP1#h#2z(Br5LYxUoUtor|A$0We^oX+&b!Ktnut;9g4F0no z9$U1-xL@N1LyFoYBL=o234l;Fw!Y(~9-w@Khq}FpQgLLE&I~|TR8~D_c4=qB`uvm( z!zl8lV+u@8I?y`DJ^5H6WF)F1zq%GI3nKRh}deq2k2Z2bZpz7368v!R_H4B)2lVF8>HIj3AY7P1;>hnvU@X}5X zk#H@`OI+|E=Y!x#hamIQF@EczM>aRC0(j3HE-Y(zA?r22Pi&&)dp!IyCt*_q-_`S@ zv+=!FO*Sl>89vF-Jf{?;+Ub8j*5@oY8Ybp`(}Q<5;eLMH-2W|-4x}JKHAA+g<1pX= z?>p~*ht}=o#SYcnK=R7s zYvLlqa@l_70Px9eR+u=%)Se?1v2d?>om)iN74-VepT%SsRpf&(BvNr67F6tN$yyu0 z*OP-w&;+yd5_+C(X2a1cCpW_1hC&YbqmjtIQxy+xuL?K$THz)!BPsymFHfAyh)K!Slg!z0Riz0n?bG`` zx@{vvB3i8xfQRaBnMtO+qI?r^UUdM&*;GB<;cR(EH!b}!1i*-)x5~qBC{_enk%tR+ zX0a?W<{3^S2+5@lKVh+RkjRkgMrl4}eew-P5r>Tj8CUO`rmiNNUOQl3tPnSmfn#Yk`M5amJzkwB~B zb!8Iab{FtNng}75+0h>|&a^cDMC_bC(C6HOY zrzi{L@Ohn8q9r45eXS!%PW=%dQGy~)43grZ-T4|r+@Oe(XtAI}ACLyMhm?+Wg?iO)A2P*WB3C6{c z4ryS4w_{MPOV^16O8kcYrw}=op%hIL614F==F`sfiMNU6;!5|eGp5;KDYdt`Qo`r4 z2i`aDI|9D$^$HA!2?lqBL}U&1*x2#X@|caqm%cL$e{L^~Tg^KN%JG3Lr&`hG3caEG zlON!7ERst)ub?m|Czg#!+AJ~3Czoo~b<|IRAGMmPXX3`M97iG0f_0rD9c>fP-xDh_ zFk#pQ;<1TF7Gw|x2={V^n5}dsW_;VFb1SHjWw$;r#>Gzlncs~XqRT9ta0mJ#pp@Ak2mm%vM=K z)C>~Yi_xBjr3Qwixyg@L=_pQhjG))|_)lpK@|{5r&l*ysn-{2^f_(1Q>To>T=LmuY zb!Wrb^e$F7;x#yO+TgkIzhzcGi0{r7yba3iz%p1woYV0BujSRsNsFo3{0dC8TG$?s zQvOF0hyUkfWSCN=GM^fpZMf7GLEJp~U!)LS^Jr77q?Xjx7=`H!II?6kWd9cukH2L& zMfnvJTLGtZoPxX?pKlcA{FvB`I{X}pM$#W8eCu1Q{4p=m&ivCnJCqISunH|vEsqg& z(EWV$yx`|l%Q_o|C~yd60}bD|W5Y@a7WI_7E!Xl$gvH($PGCjdjBNnM4a)HGu5TVW zJ`(z;2|HW)^osp(-3kJAZU~Fex0IiGMHvbe+KN3;N4zgarpT{JfA|$};pQ(~l%k}m zWHrl{kFVKe&AHM^j@G%pTdq$4_I`fprc@R@*{I73zU89L3rbb(EU`S5A`u(Ev1Z%( zAtafc^(b5JxEEKcEOV4tu(pNGdq7dzKk@@#NSf3Fq2jVR`v+?SD~yei-NV73A^Bpb z-s*&olK71Bt1E!VBbkkcKUvu#E7;7@5!&trd~ zS-?F*qf4raXC7o&LZXznWGO=7lI$!;^a)p5n(X4+&%A4wxQGCS+A@|>L;o-)`%81S zbIZ=VGOs3y5`P9civA}Mljt5@*!v1ud}ctUw#xC6xR|H&z7Rqa6X=QW_efpvLzGi* z)@T)x=(A-a$(5PeaB!U(#kGBI+1^uW^3*KU8j8!MsD=1#PS2vrmRRc!Ji{Dy710*y zQKio@VlFIUTXa2Q*|!Ick;Q|Z3Ty;Zs@`mb4c=`0ARSYapH-n`O_o)0*03CYjac`V zi6B2{@YOpa_CNVtisGaqJ*BSl)es-jvHq3Z_-*EI~92uX9c>5?}NYQ-t zH}NBBPjo4m%19Gb)KO8)N`a;3g@-D+x^vd_!@L`GpS>F4*UJ*MD%`Q0mTX(EG0dNL z5jZAiNGD5|w?}**vr0^VhZur(7rmx_ochxz2Nf;Xt7bGrvZqgp+I&Vss*SKUh8!b_ zi^zOsB_|LL2GA$$T&F_nWxACjm#*s(l+j>92syqFrlWk_C9izDSmByMAQFj*d#z5< zmWaf8NpFWHUA+P`CFr)N=D6gAGSy2ZZ3g^Ah!#AL+#yDSw8pc=Xx-I!k%G!syz!qW z=%^;dnR>)T&-l=!YpIy{&u%6#ymq#1^)e|JUp_tImnOjR?T9kVP^Hh~m-)a}o3)3Vn#GSov)O&7u6@>xvDEzJ1kOtBN1u4Z1L7?r+|h$H zDCzL+bO?7AfeLxTHZqn^5>0C4%;U3V!tA@$aaOevkx>>#jj&QK+6}b=vVlZP=LC}U zOUAW7y?wv0`^ZAF;*L}I_TzO)uDed2-@CG=-ZU~Fz}{%;b2fmP&;b|@mXAYp?H?1t^$<)bFk^p|(Mh8iiOM`ye&4t!>B4&UJxv!3~HqeArjj|U=zi zFKc!FXL$Y_nyR&Oe@tPp0k8d+B{5UXrrQUz@_sw~#2eCm>?{8tH_fkNn9Ch*-OL{Z zWfowVX;ju;0eK5Q7X_e}7NMhIpksnCQ0tFSiwscfk?47)G|k-(=-`0~`#gyJPGYlq zWc>KjABzGc>8}7(YF;V-WcmsieVBXu)0^i<2}goy ztJFeF4S8@;dywbIGY?w3r1koG4?v~szMswHwSA$xn51p>9yFp>RYKiPf=RVRk6UsJ z*KZ*vd}3QqGUT+Ub#6v}vt`&$S?nusy4I6uXD@!8YujXw^iBlkUAkA!?vEuV+BDkT zBxZz0a%wOYi?-bFeARwZHGlsecnai`zP!ZT2D#K|C-(zdVd5jhc6>+o!o57rI+*NE zBvlYHV;R<^n$zvL1fDXs%1T^9^e;>Of;HKNnLb=YErk%pkbWP01@Ojxc@(If zVntu7j;^x{UdHWN62V%>Q?QMeFRwAX|G{Jrgb`2UOdffEs4n2%52LXF7AYYlG`W6Q z6jO|xFY`2Razv>$-wS^i%ExGAgpo7fXw8lFq3_l+HVM5sI!=b_g!h}HHs`D1n3g5q zhge|7@7`$&Pu@ubYlHyA!j>aB+1dxe`I)GfIIgMd5>1yA-}$7vKZ{&r4?4BDHGWGA z$^KHSu<-o0e4bxG5gbBg%}BlJZ|rM6AC{OG7jWs1I&9mwutwoTtK(e3D}nztxhSpenWlo45Q zv(_y|eV`>!L?g?=HUy7mW9CKc$TUAbgzJUNyg#s^t*GzB^X816PdlmJU_g8`oxX8~AC#JT9lXCB3Es9?->KGb| z-*77FeD9xd()*qLsSjc#Y#sAOxA5vcHSf7mH-VAoG*0d~^-6R6E-aXXGL|Ouo^NcU9#^S|15aBAyLaTeyJn=p6QMazW0 z+DSy!@weM{fK(aR=-2}k|HPtVBah?0ka*w2{5-f@MQbGQ`|&cP+X)f^XYar@gs~w; zLXz}rE*?wjWM#*aA76_kqXkQ4+RbN?fR^F6+yJ9irjV>9Exx7g_{GR4cf<3@;uFVi znWQ|Vze~=%BfG!I_<{L|u6WYuyX&o+nq42_nsr}ZpCS|*5RM;IqX9x_$8Uivkxq%`L|a-cswqr879l?GKm) zC$3G0(hf{A(7U{&`V{EcnUE8j&af?5Mo)K?CBKPlM#%J*eg#&nw4-t%TRZZgqs%k{ zj=_#jnDR+G{qE97#j;`<;kd(iW97J3PvLggV8Y}(tg!LVnR5FW&e_#4vW(aD=SB;L z-tZ3_E34u)uY3^8;-0G=yZ`G&~XlFk3*L z7Fw^gXzJzISh&&OLev2f{``vTfKnlE6*Ucl#(dLWv`6+gUTQlYG6~~{+e=>V8>OW6< z_(hoUx4u2-U-W#~f`X{r$Ga0Lw&A+k>1akK6kC4PQ8ST=70z+#RJ&jL;Oz zCDK?CZbAJSHcqRB`1}=`x+6sx+S<;A398C^aWhT2AE-&Hzc*fSQJMACNaCf*qe66d z>)^4 z#A^HYB{ZZ&O!LgzjBUl4?v$Ci{L_yq*z{61LpP|Nn~bX@F0tPHW`06`JwH&h%jXk0u>>2a`Cd|>YsDc{^Hz1U zc3ei8^InIMR{8C-R&T)&ruWH|U*pElMx|4w*gHzoLA*}NKHV3pD3f#*szMK?DJ(@j zIyZC@u?p1*9Y-F$=j(D-F6YL*c`r3)sKx_k;0>kZB_A)>3J`gPmLqw;i`(*$R^<~P zw3DE=G;6BL+sSjhyY0I^5ew~mY_A2pvEW!*jd4}adz>3tt=*sB!mi~CeVt?xs-UNP zfrF+p#YdkK`e9fTaos~dYhfsaS}GD}do}EpAU}c#nLM>i6ED?!xtU<*9)pLTGTFu+q5lM|r9( z;@oNcl5sb45A}X8wv5a{kkUXX#0d*27EtRmHKEOc38Hj7EN#Eo`_sNVYF6n%>SKO# z=JM$4!k>4Q;`j4yHy>R}{s3ZB(-27OL+&(ezDfBj+9NAYUyRiA1+lZ5D`5MvXshzK zcYavb6qIBJ31!|%HJ-1)aZh1NosIW;?2(%->Yv{VZ)MG2+b=oX$pp%MT2E`{uIJIl zYI)RWuk#$xV6(N-jPgJbO4GeS1i;B*lztksyY_IGR-nlrpMe8P4vo*vUGi)JV6K$&I` zkwwEM$aOnx(tT|vqKUb5{E;eKwNXw4iHj|@^Gn3H`++02DYz%etS-di5v$|s7)ssC zPmTu_knIWI{cCfTdme??Tt%s^%uYY??(IiurkgO@oH>DMAFK4aT^YIHpnLCMe`I{1 z;@piSXp!_BzbWSkht*)1m{F;J4tLXPc{q+}1=h#dxh1otYCCz$@Uu4{-ev`GXG<3r-%brx{ zK8?%uaVhe1TRe&Yt&`CZ#jp1sR4)a6@B2bDb*LyF6u;BR{Q$JLlTB}Z!*KW@mNOv> zD@*WrPk5C_t8-{x(DklZbiy{5K_^U?)Eh>-Pf!CaJUZk?`UdZtE>-ly8ywhzVL zq@&+;PJTlscB?5(yu0oGnb`|(7)_1=a*ntB;Y@&aa+Wlqb5OhfpCYWE;nS1rU?~Lk z_!!>t*}cCH!JKABq?{fz9yI;H?4eOikX^vUIU!r~`i=5eSXMXuLD>7l!`#1gO7!?p zmcWPZnQ484XQ0It!Ok5qDzuW9!BH;dg3~s8ujL>zE_kdrSZ!-z?npx2z^z0G_BqzD zw%+<{Y1JHKZ0+a)+uq40ILX|{d5qS}71TI-aCUn*K|`GCh8Vt3_DOzgk_Nl#n5ez$ zglCi}F=nOl3mOkHGglmPwMJZJQ6{*wYbMb|h7OxaqTO{8t;-h!G`257&T}V)uK?;8 zS^qdWJv)^;AIIlIF_!Y8bI;k{63Dy^%_2TUTNUvfd61da#H5xer~W0$u&b9^?SokD zEfWPFeAl@xI#ehR3HlqJje<=v*babG+spV^y2%;`C?6p7uwNBv&V2;!?c*>F-)J_*l4 zBCnaV{rHOuogYDyETWDL$*TP%hgvU)bdKDykna43baXa>UhCVBGLt7;u`0f0ch>p! zt@7pj8uGJPus)(=QK3l@G@WGj9N3~S%Sg3h!@md6f2Mk{Qle45f<-xY_x)D_z{sU zA#a?=)YscZ<-+yY7f7G0(?Vfi)#D=f6Q83y&6~O}BFnlEj*)SY)DJNk&4JyJ8D z*b$V(-gt)c?EzmDvQ3%q#QU=>70w(sFexT5$VQU>?G%3`)mbm;dS?F%^gBYlnLMA` z!ujdy4~ihw*Pis}xha1oM%{%U%~M6hqhj%mTYvl93h@>2pZ}fy_^bmOIsg#;l6CRp za_`su6>z>K`IzgHJKz^=mE$aM;rrazcX?mOy*!gG{JKcyRgfRI1*{;zHJ3Z)o|`KLd~ew@EcUIC#2 zm;TnjfNxPYL)q=IvzwIv72jxSw1rUS56SsTL$DRd@8z&`>mLLjh;D7Rq9p$>13-7qcC1BU3?_? zC*bcW?oS*UWxv+e{KVJShjCm15RH)If8bFOjuha(hrS>rn!Ct(`vj*xq~P#5ZDav) z!PJACchE_*@d^x6SH#OQJ_b7vm1_j1MRv~=)o#%{3X5(9feP430Ofd?yk47Fzp^$( z9u63ZPF#XxjHtDZ_(qsd`nLZW2x4}aOIwG>7-K?gOz1h^1uY#|A6pRc+~ZQS9fl(XwNqbApj0 zT=u*mNIo$~*I)?yQ{Sk={9A!&OcsWD#EiHKMf4N(RYCq*PZ3)0CTbBn6`=eT zdY8>@Js0{$nE>Gs6&H>eFVx%Q0!XKrf1n1gXUWc)KT?ahhUhU-r1s=%=qPv{ zz|zbg8zX@9=cc~bi-AUfg9tCOByCl&>LUP=VFbTA>a^Gl-OBVh#3C7741#Ow49VsB zO?6!Xu*j$d$+-GZiUg9(uoREptGa4?5p}pEQA9*ct~M|onEO%e~bS0b(QZ($#|&WfkSqn7?YWY_;q4(^n80* z^NW{rb{z7offi|x=R*?>@@Ra;U*a=!E(2y*XkQ+E-cn`giqN$k>bga@dX2%HRxhp1 zESE~lkTSV1QW3Di6{!${9>T@s+6TOpycu%?cUTt2qw@At?W5x7U6-ScSs}K%ui=2M9trJ;_cs>_-p<+~ z`^ld&Gr!|KJ|SO&gDgaS&VOcZ_%8>7sqiM2F-eYF{w|fX@|y0-r9{ra4rzR-t(_LhDQWNWxx(Ji#;_#$_znS!gFtsfdms>mX3H#QhE}$ zvgr-fo($iT_6PPXd?uUX(x3l4NZ3ws6w$Z z;br+)%F1X)SsZ>|7($p>02(-lw62o{p`-YW8#nxzTB{yTETDOGr+~*f&=ns+lG?6M zs0YM_-rDPput=3a{acNjIvxngk9TvqQ{*4SwH~O_{uL)o1x?@~Q2}4*@|eEC9Xo<$ z8|w#=W3s>ogs?ECDV{ieiHTvKERGajM+)Y$;73&H1b=uCXMr@4y7Be&36%yr=y*fc2)pGb8;4Kk zH7xnS3YK|gFAy<1Bm_bhv&dAa@6-d4F@G)m-W5I2jSE<;dmfLB$A<&J8>t;x<>c2I zm}t^t*b#YF&&e#jq{G78ybKuFH9vP6B{#+|UID^%_R{FQv(Ao-WB?AJrkQ2-1bU|v ztAkuqnf_f-D;$s>EEY-HGj&-UYg`T)_xOaMPcc{`#{nTFT8M5sGXZPSjK{U5W>Q*9 zz@t7;UPKBOghV>?Rtb$8Mn4z0nq$PGq&0m^F-f^Z?k*0iJ0MUY^?C+RPy~iaw|&Cu zXJOAFqij#Vcp(2N%#c%5IzEIC8)jO!8i><0Us=O=!6Qzeh>k@=%Jtp?9*l|Je%>_lx-?GNZxPFT-1c{mymn^x-r9zdW0~H$ zZ2v*`emmtne)a)R>jm_`Aa}o+viT&5)u+k4P-*!K&#E7j} zEx}}0A-k=iubi&ycTq@@@b)_fi(_}U z5qye81kB#{}s?$*62!_hslVH z7Li`U3q0(4@@@>P@Bb_2+M}6Z*!VUZ+uSyWAdt5MW^2SeCRfNp^jc({BUSEcu-F4^tQVzCw%$rkE^~i+!B*?IIVk_E zn%YN$CJMht2;_kcGCW2E(txWhdw68ax^mzkIly)>R+r@dWRDu;G_#A=B**4B<`eu{ z#H^E7cDJbe*=Qw?ZX!lF4c2wm!Z8T%Saod36>5n>D%tHXfXWeA^&>+Ea2n1-cq*_I z`|$NL)$W^^bXjuwAQY+#DGE_RhcrPpy7%!n&^p#$<)Wx5>j!wUA`~z;fDaD35%jbR z)i0dk%#DT$x}rCt^m3bzMMQ+SjA$lvcVARMC=VS&SndRtlyDQ&DN?y#*St(ST+jYZWXLo zsmdnZ8wD=xNk1%281me+4P=&WzcJkfR;K2!eoV`);3{*1mP~>Me3~=}^*_Q=wUJZa$lAvXigQC(8C>)~Yx>N?X^J91D`NmT!dCe6>3X0isOfH~XJ(}J~gc+XJ zH?fnlLL4{syo7@*d4KU}g1EFJ_k|6y`TS)5+N%@d_W`|nPc=9ZMbZ|L5Q-OCKyRTOqGmEOc#{`+83mFR7BTYyfBKW7D+R)@%5GE_ ztHaAs))jE~BiFa3z0Agr_&Ca6=HY4%*b{!HANdgAO^|vh?rs6?Xs;VdikfKbX?e$n zdL!}{y0~Uv1YDbv#lCBh;_Jd0XcR*!^%B`17b*vJb6iE$tT}N{)7hb=|$*>zoQcAEzrAZJTl9`qbJ?DHW=V0XA<#Qt7N_XdK zM}NR#Q5_Nxq7>r>B6z8^S!ASmmYYMR(M)PdJ&t9Q749Y5Q1dYg_i*kZz>-Iid9gG0 zedb7tLtRnlpA*oyn@!T&I%Qf`S8kh?!8If>9%vlzv;Wcd zA_np;v=ZZnxom#$k>1fDw!t2?zXb zE=08M^wY?d*-!B5IM^}HND0Gn{OF! zka@#X5*s?CTAJQWluu+eZr*=XK0%2FoS){l^lKP<7@-x$qtU0S$yHKJbySt13Avz$)+gz9|2&NMJt$Lf@oS5j<`R;SYeS z?nYD;hTdEdV=a7*ifWC#Cjr$oc2#GKM3V`8=dT!Oyz#6!WNj`y`0QYRt3A25P!dKw zlPRrZ60sK*XFWR=hUpDR@^C@MmOQ?6RmKCVh1`0dFRNEyf#vq;tv)stT%#xDMc6EP zQvol`!Ku;BJtPsjq%nE&kN+hf?G5@SqTOK$7h|TqlVOM+ma1Ww<<^7}TU8{nLdqU* zn3w&MLcrv!DRrxhsl}3FSE_chf|F0|lWOwGks`|=O w-#?@O!T51zQFkUg?)*(q->>=?;5ck;h4u^oCsLYRvkPYJ7k}pOwY6ve1|G(bsQ>@~ diff --git a/fpdf/table.py b/fpdf/table.py index 4c5444c47..a164f30fa 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -15,8 +15,11 @@ class Table: def __init__(self, fpdf): self._fpdf = fpdf self._rows = [] - self.align = "LEFT" - "Control text alignment inside cells" + self.align = "CENTER" + """ + Sets the table horizontal position relative to the page, + when it's not using the full page width + """ self.borders_layout = TableBordersLayout.ALL "Control what cell borders are drawn" self.cell_fill_color = None @@ -31,6 +34,8 @@ def __init__(self, fpdf): "Defines the visual style of the top headings row: size, color, emphasis..." self.line_height = 2 * fpdf.font_size "Defines how much vertical space a line of text will occupy" + self.text_align = "JUSTIFY" + "Control text alignment inside cells. Justify by default" self.width = fpdf.epw "Sets the table width" @@ -43,6 +48,22 @@ def row(self): def render(self): "This is an internal method called by `FPDF.table()` once the table is finished" + if self.width > self._fpdf.epw: + raise ValueError( + f"Invalid value provided .width={self.width}: effective page width is {self._fpdf.epw}" + ) + table_align = Align.coerce(self.align) + if table_align == Align.J: + raise ValueError("JUSTIFY is an invalid value for table .align") + prev_l_margin = self._fpdf.l_margin + if table_align == Align.C: + self._fpdf.l_margin = (self._fpdf.w - self.width) / 2 + self._fpdf.x = self._fpdf.l_margin + elif table_align == Align.R: + self._fpdf.l_margin = self._fpdf.w - self.width + self._fpdf.x = self._fpdf.l_margin + elif self._fpdf.x != self._fpdf.l_margin: + self._fpdf.l_margin = self._fpdf.x for i in range(len(self._rows)): with self._fpdf.offset_rendering() as test: self._render_table_row_styled(i) @@ -59,6 +80,8 @@ def render(self): self._render_table_row_styled(i) if prev_fill_color: self._fpdf.set_fill_color(prev_fill_color) + self._fpdf.l_margin = prev_l_margin + self._fpdf.x = self._fpdf.l_margin def get_cell_border(self, i, j): """ @@ -159,14 +182,18 @@ def _render_table_cell( self._fpdf.set_xy(x, y) if not fill: fill = self.cell_fill_color and self.cell_fill_logic(i, j) - align = self.align if isinstance(self.align, (Align, str)) else self.align[j] + text_align = ( + self.text_align + if isinstance(self.text_align, (Align, str)) + else self.text_align[j] + ) lines = self._fpdf.multi_cell( w=col_width, h=row_height, txt=cell.text or "", max_line_height=cell_line_height, border=self.get_cell_border(i, j), - align=align, + align=text_align, new_x="RIGHT", new_y="TOP", fill=fill, @@ -224,6 +251,11 @@ def cell(self, text=None, img=None, img_fill_width=False): img_fill_width (bool): optional, defaults to False. Indicates to render the image using the full width of the current table column. """ + if text and img: + raise NotImplementedError( + "fpdf2 currently does not support inserting text with an image in the same table cell." + "Pull Requests are welcome to implement this 😊" + ) self.cells.append(Cell(text, img, img_fill_width)) diff --git a/test/table/table_with_fixed_width.pdf b/test/table/table_with_fixed_width.pdf index 37d74b0394069663887a4a020405bf4818d635dd..f66f13526e470e2c43d63eeaea3ad76e5ae23300 100644 GIT binary patch delta 683 zcmaFD^Mq$ZAY=XMyxRr>uHS!h>CNW#?ph*uTE#=mt&6F*t|8;jm9?tOlX{iv<0UVB zFjVe}V>RBnqc~yS5!TyxIrFr>y??hcH9lTtvZ{E~*8Mk%t=D&Dgi7m~tX}%aZFAq* zGV2(l(E0{&-B)N(Z71nlX|c--rKfzRDrSa_{a z(`&6IwLc2x{FJsxT=Ky6h-7|DQ<;U|tQi(tH7Z35?PSl*XV|sKSSVYvw0LvHkBy0H zqMws=AKcYtO?a<&{BOsWyF2P9hV$7QpV`9fkg0Ux+T?wu?Vt0Pyg4>?Pu`D(iOXai zyo4J!%?rs)d2_6DPwvNK>KW__ms}EZJ~ZB%_f~Hkd&HCTMyFn-?f-E2kdM~0qfrO6 zqplqId6sYfjSF8o_r(4P1)6cdQ@C-V+?V#Ze#&MhwW|5zbrCZ%*(U&vS)qQlo=tj| z-NfzM_kOUvW$>BB!Si*-{r1Ig@B4cMXv{ob^s8`n?2p*#GedN`w>AiG$?^KW{yyLF zk5`i2FNc4sdsrTucI#n4<{@jT4R*Kw=@$Zxe=lBlD|MM{-xReyiy2quY*_qKxN`p^ zP?)uQ3g5fP@cFCBruKiy@-@9mozpI^2%oKbIG@{irGb{{hk4U(EsU#+`V+gqP&r?s zE^qp_Q_v4y3Hv7x!6skyn6xq*wZtBIWrAr+H7*`xus C!#&mj delta 683 zcmaFD^Mq$ZAY=X6yxRr>E$=J2LW%6&ZN<8pJ2E2fPTLcI| z{pHO21AI&O+p*X_=CsIQ`?92OCS&XerB^>USl8`Z#BHIxVNJ-}s}CL~Tl*^MHLEtK zZ`X=>dp2|bT!C%c_12$GJT$iUHI4~<#j)Y5@BH6)Kk7S`rFzSm*JLcuY|HkNW!$uH zWk|=3siKv)9(9&q;0d2>#gx>{p2fD!awg-v%rpH{7wwy+UlXz1^GNE3ln)*A)^2<5 zZ(Nt;bXogT$-~uFK(qZ{ad3Qft)KnRAkpb^G*I*~NHiEITL0B|>amt(;ZI(6so!%n zj`0PlG_CKgHr^vAu%KY7ex&KItX&^s*QyDYZAf8oSiP-jf1UBW<%b(2ee+L#ZhfC+ zdbVNJm4}kNyY9_?*laNC-lXT;^RH#iV)R{k*jUO$R5#6JuhRK&aG=cssmrpHRVy>! zliPeF#GiHXHz%s|#9zyt1r!wwcs1MZP1XPE zQ~4)_+xlOdoV{T5ltbgI?^1^;>(-nQ7W3cJoR%@Oh-J#JS;h;M@BFjh{qvIY<|WL@ vOit#e#%`9*hQ^j=MyBRYW=@W7MwUi~Ce8*HW+uj#u10n?gj7uSWRnH}mP0sp diff --git a/test/table/table_with_an_image.pdf b/test/table/table_with_images.pdf similarity index 100% rename from test/table/table_with_an_image.pdf rename to test/table/table_with_images.pdf diff --git a/test/table/table_with_an_image_and_img_fill_width.pdf b/test/table/table_with_images_and_img_fill_width.pdf similarity index 100% rename from test/table/table_with_an_image_and_img_fill_width.pdf rename to test/table/table_with_images_and_img_fill_width.pdf diff --git a/test/table/test_table.py b/test/table/test_table.py index 8b8db4bdf..80dabc388 100644 --- a/test/table/test_table.py +++ b/test/table/test_table.py @@ -132,6 +132,19 @@ def test_table_with_fixed_width(tmp_path): assert_pdf_equal(pdf, HERE / "table_with_fixed_width.pdf", tmp_path) +def test_table_with_invalid_width(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Times", size=16) + with pytest.raises(ValueError): + with pdf.table() as table: + table.width = 200 + for data_row in TABLE_DATA: + with table.row() as row: + for datum in data_row: + row.cell(datum) + + def test_table_without_headings(tmp_path): pdf = FPDF() pdf.add_page() @@ -267,14 +280,14 @@ def test_table_align(tmp_path): pdf.add_page() pdf.set_font("Times", size=16) with pdf.table() as table: - table.align = "CENTER" + table.text_align = "CENTER" for data_row in TABLE_DATA: with table.row() as row: for datum in data_row: row.cell(datum) pdf.ln() with pdf.table() as table: - table.align = ("CENTER", "CENTER", "RIGHT", "LEFT") + table.text_align = ("CENTER", "CENTER", "RIGHT", "LEFT") for data_row in TABLE_DATA: with table.row() as row: for datum in data_row: diff --git a/test/table/test_table_with_image.py b/test/table/test_table_with_image.py index 9c461dcc1..cba902e32 100644 --- a/test/table/test_table_with_image.py +++ b/test/table/test_table_with_image.py @@ -1,9 +1,10 @@ from pathlib import Path +import pytest + from fpdf import FPDF from test.conftest import assert_pdf_equal, LOREM_IPSUM - HERE = Path(__file__).resolve().parent IMG_DIR = HERE.parent / "image" @@ -38,7 +39,7 @@ ) -def test_table_with_an_image(tmp_path): +def test_table_with_images(tmp_path): pdf = FPDF() pdf.add_page() pdf.set_font("Times", size=16) @@ -50,10 +51,10 @@ def test_table_with_an_image(tmp_path): row.cell(img=datum) else: row.cell(datum) - assert_pdf_equal(pdf, HERE / "table_with_an_image.pdf", tmp_path) + assert_pdf_equal(pdf, HERE / "table_with_images.pdf", tmp_path) -def test_table_with_an_image_and_img_fill_width(tmp_path): +def test_table_with_images_and_img_fill_width(tmp_path): pdf = FPDF() pdf.add_page() pdf.set_font("Times", size=16) @@ -67,7 +68,7 @@ def test_table_with_an_image_and_img_fill_width(tmp_path): row.cell(datum) assert_pdf_equal( pdf, - HERE / "table_with_an_image_and_img_fill_width.pdf", + HERE / "table_with_images_and_img_fill_width.pdf", tmp_path, ) @@ -85,3 +86,18 @@ def test_table_with_multiline_cells_and_images(tmp_path): else: row.cell(datum) assert_pdf_equal(pdf, HERE / "table_with_multiline_cells_and_images.pdf", tmp_path) + + +def test_table_with_images_and_text(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Times", size=16) + with pytest.raises(NotImplementedError): + with pdf.table() as table: + for i, data_row in enumerate(TABLE_DATA): + with table.row() as row: + for j, datum in enumerate(data_row): + if j == 2 and i > 0: + row.cell(datum.name, img=datum) + else: + row.cell(datum)