From cccc07269ae60226f0bb6475970d6fab255538b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:23:24 +1000 Subject: [PATCH 1/3] Do not justify a single word --- .../multiline_text_justify_single_word.png | Bin 0 -> 2436 bytes Tests/test_imagefont.py | 12 ++++++ src/PIL/ImageDraw.py | 36 +++++++++--------- 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png new file mode 100644 index 0000000000000000000000000000000000000000..e124e91f5e34d64b002b06f5c66fc72ddb80a31c GIT binary patch literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: + im = Image.new("RGB", (185, 65)) + draw = ImageDraw.Draw(im) + draw.multiline_text( + (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + ) + + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + + def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b0298..e865f4516 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -772,24 +772,26 @@ class ImageDraw: if align == "justify" and width_difference != 0: words = line.split(" " if isinstance(text, str) else b" ") - word_widths = [ - self.textlength( - word, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - for word in words - ] - width_difference = max_width - sum(word_widths) - for i, word in enumerate(words): - parts.append(((left, top), word)) - left += word_widths[i] + width_difference / (len(words) - 1) - else: - parts.append(((left, top), line)) + if len(words) > 1: + word_widths = [ + self.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word)) + left += word_widths[i] + width_difference / (len(words) - 1) + top += line_spacing + continue + parts.append(((left, top), line)) top += line_spacing return font, anchor, parts From b955cee725da2613b34145eea56227c57ec414d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:36:52 +1000 Subject: [PATCH 2/3] Do not justify last line --- .../images/multiline_text_justify_last_line.png | Bin 0 -> 3581 bytes .../multiline_text_justify_single_word.png | Bin 2436 -> 0 bytes Tests/test_imagefont.py | 11 +++++++---- src/PIL/ImageDraw.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 Tests/images/multiline_text_justify_last_line.png delete mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc1afd72969c4a476cabc5a074154994a222a94 GIT binary patch literal 3581 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 literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png deleted file mode 100644 index e124e91f5e34d64b002b06f5c66fc72ddb80a31c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: - im = Image.new("RGB", (185, 65)) + im = Image.new("RGB", (280, 60)) draw = ImageDraw.Draw(im) draw.multiline_text( - (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + (0, 0), + "hey you you are awesome\nthis\nlooks awkward", + font=font, + align="justify", ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e865f4516..47ae575c9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -770,7 +770,7 @@ class ImageDraw: msg = 'align must be "left", "center", "right" or "justify"' raise ValueError(msg) - if align == "justify" and width_difference != 0: + if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: word_widths = [ From bc05a88ce664dff5eee1bb024e4922d86ad86f96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 20:56:02 +1000 Subject: [PATCH 3/3] Anchor left when justifying words --- .../images/multiline_text_justify_anchor.png | Bin 0 -> 11282 bytes .../multiline_text_justify_last_line.png | Bin 3581 -> 0 bytes Tests/test_imagefont.py | 20 +++++----- src/PIL/ImageDraw.py | 37 ++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 Tests/images/multiline_text_justify_anchor.png delete mode 100644 Tests/images/multiline_text_justify_last_line.png diff --git a/Tests/images/multiline_text_justify_anchor.png b/Tests/images/multiline_text_justify_anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3fb421d1832e82dcfba255891240cf2aa5b92d GIT binary patch literal 11282 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 ^Ow{8V4_rXU6&3`dV6!`l3PEJl{WwF}! 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 HzUTh}m8~Dn literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png deleted file mode 100644 index bcc1afd72969c4a476cabc5a074154994a222a94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3581 zcmZu!c{G%L`=1`uLzYMrlV!-Rh$hB5B*vDVG`8%^V+&Cj%hPz#V3H&;jKr8owwbYp zWRPr0)*(?E8q$z`>Aib?=XcI~&ij7`Y|nveTXl;kW|o|KqEZK)IV;7RySR=>|_f|ZGvAGr}P@D^^W_%C>r)YTf-%JIv? zwQoR9+rBQ%PfNEBKKIApP^R8SlhlV;)yy8I-q0SAN&JW$#}GNOKpp?AA#Mw~5kE?? zLVjUk>$EfIZx30zALT+`e?>*bg5w_`@P#vd${zO=m*EICDXml6$)C3LZKj$ z$Q$hk^<wK$UOX4(^Hy=r~@`~(MHHk7;n^zrp=ay<`0 zRl7FLm)}R%%53=f`@6Tsjt0^O`}@_@)V?s8i9d%;LJ`-e=3giCD5QO#ZI?*#OzmXP z+@3`l8za=!Cs#Jz6AyB%$SSP~XV0A*4UBSga|;d*Mx!6wD7?gMg?f2O4S!`-x3@*J z=Qmdvc@}c1omqVghK7bW+IHkZYa$lR!3I*&(m0dkl$6b%KmV1I5_|5j{r*qs9P<<4 z$ySU;Qiq2sR7*>X3u{WUYgmuK;2sndSZuuJQ$vQ?xghSlB8zjq5zRau{WT+ZA5{W$a3`Va;}O-%;E4b+p}+yC~h zh?tm?ii-Tyc5r%retuR~mRrlM~dnc!u!~GZ$ zdWv_Dc6Q_KtHAW)Om!y$)HXLZWMyUN6!rIi zB$>*1!Sm1$jZ01m35|`7U52oih3x`?H>w{+;GI%PRIxtdo=ocwa~^0hzq_jNy-DU9+@ zN)Q(p7w_FW{vipA#pb#voYuIhU3fMp@Ymr1T1`i1Z*8H^@p+Dq;+KOk%>qWl4Gb)x z7VYKb^-M_EHTJQ^S6a|GT50TKD@IyMs?oLTa`85^C{aj_SX2bmdR;Z$hp>yv>|2P? zF7)*DG&3_ZE(s3^2pB1|3`Su5{JIw+Y~SBp|2lxgY?hXmQmIr)uJ<25UUzgfUZx%f z@|bT>9+Zv@4-aQ#WXvZ1njtSNjgVVfTG|gb8JzquG0iZK*|zvEr0lJWaJZnbFf+!% z)pZ)^=)F0w5Yn!kN~6&#D|xLi}i42t5C=1 zwDR($PJVq0E33oxA^p{v7Lz(I^8^u5(JSWW)5IicY3VChu8an%6X(M;uSd@veLC>G zx=my`cI<`A7OXhT^9`3Tmz^fCr0CrLyukA340EW4$05LW;hC?&xbo%_f7c_<4A!Vz^ z+i7WODXI}FQ_VQJn&1OJjg zV%EPt;M_X$R9;#6MN11|hzWq)7=`Lxh$tDpEBOZK0Ezd6ZqBF05%k)K8?5oI%}s=c zhPN%vIYbreNp`#O|CepsY)^~KZ z_%TpV%2A5K+>Eucaf%h0!dSW0;s&renT(lwn3HpGur+%;)v$W}3hDjBhY$z~=RM-_ zK|0G+035De2ta{sRhWju*_JyP=@T?=bRRBB!7p5>jUerH>h2)o#gI^MZ||c=kD}2^ zLMob7>*Y*fG|aACVJ$CH87mhrUW{!`=$K2&t>0cE7u9gb{5sgWz*b1}a_{Zx=_yf| z8XNmMyX&OwKSU($er4iz*}`|qIDaU0&V@OZB#}tzQ8MtG-;ezL2|HCXv%FmCa290? z!Ajzd?QLzd2n66L6t!YL<29^^y5++GH31mRUPlt=c%_qJ?=g^ojSb6`$kMjSZ^Ral z28xP`_Vo17oRtwbV5<(GOZV$|d5m@oZt{b`;@00@6bi=;HgSTm_e^e=HZ^JenZ%Av z`>FN2Ds7nCyOEKRNbS~V&+c}4)lM!C5HuvTBEB56^un;agjp;W?&kNz!yW$|^9`A| ze}ISx*YjHqvk3$uv7WIKA0IzDI!Z^qdQ-`I0cTE7zDpvRw(NtGg_a;DYwWkF(Z+ojn9Qy>|+X{z@ zy{%OjrAPh!7Eb}KZHZz%8d;*Em3)1D9U3M%be&#iWR=<}VkKK{uXf)u=v343r+=g)*B(+qocv9+?Yl1hE}k`HVkiU@ueNIRSJjgC8+-ur2NCn+gO5GNxgggvi$*L%Qp+@6?zvPeqn4V3(n|&bjHfc*eyUui8%|kKRmp0H*LB9^bk# z<^l*9j}S9AO8kEbW*DR4!$O2s;b2WoO=+nNK(NZp*M1a#KR?&=!Z8Cr`}c>FF6!&+ zBay8hLb-h?0lIQ{XlQ81TTWf^H&v6WaCqG(7&SaRyriV$ONc6Px|uL!#c)7AfiN4x zEW~VmZxWvea+liu6%=%?%~Kt0AeRjou!(_w57pqwI+OyWVl*(fPgW2InA^LIvFs1a zBm8Day}W{gFJYPh5z>uB@b#fH+7(qG>$EP|%F~jPy}iAeZhgSg(OdXVi!QcvZ1wl| z12PkRUMEyd1ZQm|0KNdt0Ptt0o%{B!dBeRPKMDqenSqhk*4BoW{Cbj8_lN&U3svP+xpiGC>&;E!pRZqE{^4r~np=2LXd!1P0Eh@DMmm>3&{0*D z%nXxdN4) z0Ef4PjaR$RG8l~M=?oNL%R4;o;_;-4NwUO)Sk;%2G9q|KclX(`F*yM)C~tQ+UNloe zKY=}uf~FXlI9r)k(Le?^|0O!H>!F`_W}N-~Ao+$^?!ej5IlRsNnSTwel@Ekj)sG=ePg+3+__5eQs q{09Nv@5MOF7DA%#9sf_lI%FmuiQ}bCQ2&hh9X3N*8C6~O!2TOFndV0T diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f99275925..fd622c945 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -267,19 +267,21 @@ def test_render_multiline_text_align( assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_render_multiline_text_align_justify_last_line( +def test_render_multiline_text_justify_anchor( font: ImageFont.FreeTypeFont, ) -> None: - im = Image.new("RGB", (280, 60)) + im = Image.new("RGB", (280, 240)) draw = ImageDraw.Draw(im) - draw.multiline_text( - (0, 0), - "hey you you are awesome\nthis\nlooks awkward", - font=font, - align="justify", - ) + for xy, anchor in (((0, 0), "la"), ((140, 80), "ma"), ((280, 160), "ra")): + draw.multiline_text( + xy, + "hey you you are awesome\nthis looks awkward\nthis\nlooks awkward", + font=font, + anchor=anchor, + align="justify", + ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_anchor.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 47ae575c9..d35cda602 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -702,8 +702,7 @@ class ImageDraw: font_size: float | None, ) -> tuple[ ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - str, - list[tuple[tuple[float, float], AnyStr]], + list[tuple[tuple[float, float], str, AnyStr]], ]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" @@ -753,13 +752,7 @@ class ImageDraw: left = xy[0] width_difference = max_width - widths[idx] - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter + # align by align parameter if align in ("left", "justify"): pass elif align == "center": @@ -773,6 +766,12 @@ class ImageDraw: if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + word_widths = [ self.textlength( word, @@ -784,17 +783,23 @@ class ImageDraw: ) for word in words ] + word_anchor = "l" + anchor[1] width_difference = max_width - sum(word_widths) for i, word in enumerate(words): - parts.append(((left, top), word)) + parts.append(((left, top), word_anchor, word)) left += word_widths[i] + width_difference / (len(words) - 1) top += line_spacing continue - parts.append(((left, top), line)) + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(((left, top), anchor, line)) top += line_spacing - return font, anchor, parts + return font, parts def multiline_text( self, @@ -819,7 +824,7 @@ class ImageDraw: *, font_size: float | None = None, ) -> None: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -834,7 +839,7 @@ class ImageDraw: font_size, ) - for xy, line in lines: + for xy, anchor, line in lines: self.text( xy, line, @@ -949,7 +954,7 @@ class ImageDraw: *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -966,7 +971,7 @@ class ImageDraw: bbox: tuple[float, float, float, float] | None = None - for xy, line in lines: + for xy, anchor, line in lines: bbox_line = self.textbbox( xy, line,