@@ -557,21 +557,6 @@ def _multiline_check(self, text: AnyStr) -> bool:
557557
558558 return split_character in text
559559
560- def _multiline_split (self , text : AnyStr ) -> list [AnyStr ]:
561- return text .split ("\n " if isinstance (text , str ) else b"\n " )
562-
563- def _multiline_spacing (
564- self ,
565- font : ImageFont .ImageFont | ImageFont .FreeTypeFont | ImageFont .TransposedFont ,
566- spacing : float ,
567- stroke_width : float ,
568- ) -> float :
569- return (
570- self .textbbox ((0 , 0 ), "A" , font , stroke_width = stroke_width )[3 ]
571- + stroke_width
572- + spacing
573- )
574-
575560 def text (
576561 self ,
577562 xy : tuple [float , float ],
@@ -699,29 +684,30 @@ def draw_text(ink: int, stroke_width: float = 0) -> None:
699684 # Only draw normal text
700685 draw_text (ink )
701686
702- def multiline_text (
687+ def _prepare_multiline_text (
703688 self ,
704689 xy : tuple [float , float ],
705690 text : AnyStr ,
706- fill : _Ink | None = None ,
707691 font : (
708692 ImageFont .ImageFont
709693 | ImageFont .FreeTypeFont
710694 | ImageFont .TransposedFont
711695 | None
712- ) = None ,
713- anchor : str | None = None ,
714- spacing : float = 4 ,
715- align : str = "left" ,
716- direction : str | None = None ,
717- features : list [str ] | None = None ,
718- language : str | None = None ,
719- stroke_width : float = 0 ,
720- stroke_fill : _Ink | None = None ,
721- embedded_color : bool = False ,
722- * ,
723- font_size : float | None = None ,
724- ) -> None :
696+ ),
697+ anchor : str | None ,
698+ spacing : float ,
699+ align : str ,
700+ direction : str | None ,
701+ features : list [str ] | None ,
702+ language : str | None ,
703+ stroke_width : float ,
704+ embedded_color : bool ,
705+ font_size : float | None ,
706+ ) -> tuple [
707+ ImageFont .ImageFont | ImageFont .FreeTypeFont | ImageFont .TransposedFont ,
708+ str ,
709+ list [tuple [tuple [float , float ], AnyStr ]],
710+ ]:
725711 if direction == "ttb" :
726712 msg = "ttb direction is unsupported for multiline text"
727713 raise ValueError (msg )
@@ -740,11 +726,21 @@ def multiline_text(
740726
741727 widths = []
742728 max_width : float = 0
743- lines = self ._multiline_split (text )
744- line_spacing = self ._multiline_spacing (font , spacing , stroke_width )
729+ lines = text .split ("\n " if isinstance (text , str ) else b"\n " )
730+ line_spacing = (
731+ self .textbbox ((0 , 0 ), "A" , font , stroke_width = stroke_width )[3 ]
732+ + stroke_width
733+ + spacing
734+ )
735+
745736 for line in lines :
746737 line_width = self .textlength (
747- line , font , direction = direction , features = features , language = language
738+ line ,
739+ font ,
740+ direction = direction ,
741+ features = features ,
742+ language = language ,
743+ embedded_color = embedded_color ,
748744 )
749745 widths .append (line_width )
750746 max_width = max (max_width , line_width )
@@ -755,6 +751,7 @@ def multiline_text(
755751 elif anchor [1 ] == "d" :
756752 top -= (len (lines ) - 1 ) * line_spacing
757753
754+ parts = []
758755 for idx , line in enumerate (lines ):
759756 left = xy [0 ]
760757 width_difference = max_width - widths [idx ]
@@ -766,18 +763,81 @@ def multiline_text(
766763 left -= width_difference
767764
768765 # then align by align parameter
769- if align == "left" :
766+ if align in ( "left" , "justify" ) :
770767 pass
771768 elif align == "center" :
772769 left += width_difference / 2.0
773770 elif align == "right" :
774771 left += width_difference
775772 else :
776- msg = 'align must be "left", "center" or "right "'
773+ msg = 'align must be "left", "center", "right" or "justify "'
777774 raise ValueError (msg )
778775
776+ if align == "justify" and width_difference != 0 :
777+ words = line .split (" " if isinstance (text , str ) else b" " )
778+ word_widths = [
779+ self .textlength (
780+ word ,
781+ font ,
782+ direction = direction ,
783+ features = features ,
784+ language = language ,
785+ embedded_color = embedded_color ,
786+ )
787+ for word in words
788+ ]
789+ width_difference = max_width - sum (word_widths )
790+ for i , word in enumerate (words ):
791+ parts .append (((left , top ), word ))
792+ left += word_widths [i ] + width_difference / (len (words ) - 1 )
793+ else :
794+ parts .append (((left , top ), line ))
795+
796+ top += line_spacing
797+
798+ return font , anchor , parts
799+
800+ def multiline_text (
801+ self ,
802+ xy : tuple [float , float ],
803+ text : AnyStr ,
804+ fill : _Ink | None = None ,
805+ font : (
806+ ImageFont .ImageFont
807+ | ImageFont .FreeTypeFont
808+ | ImageFont .TransposedFont
809+ | None
810+ ) = None ,
811+ anchor : str | None = None ,
812+ spacing : float = 4 ,
813+ align : str = "left" ,
814+ direction : str | None = None ,
815+ features : list [str ] | None = None ,
816+ language : str | None = None ,
817+ stroke_width : float = 0 ,
818+ stroke_fill : _Ink | None = None ,
819+ embedded_color : bool = False ,
820+ * ,
821+ font_size : float | None = None ,
822+ ) -> None :
823+ font , anchor , lines = self ._prepare_multiline_text (
824+ xy ,
825+ text ,
826+ font ,
827+ anchor ,
828+ spacing ,
829+ align ,
830+ direction ,
831+ features ,
832+ language ,
833+ stroke_width ,
834+ embedded_color ,
835+ font_size ,
836+ )
837+
838+ for xy , line in lines :
779839 self .text (
780- ( left , top ) ,
840+ xy ,
781841 line ,
782842 fill ,
783843 font ,
@@ -789,7 +849,6 @@ def multiline_text(
789849 stroke_fill = stroke_fill ,
790850 embedded_color = embedded_color ,
791851 )
792- top += line_spacing
793852
794853 def textlength (
795854 self ,
@@ -891,69 +950,26 @@ def multiline_textbbox(
891950 * ,
892951 font_size : float | None = None ,
893952 ) -> tuple [float , float , float , float ]:
894- if direction == "ttb" :
895- msg = "ttb direction is unsupported for multiline text"
896- raise ValueError (msg )
897-
898- if anchor is None :
899- anchor = "la"
900- elif len (anchor ) != 2 :
901- msg = "anchor must be a 2 character string"
902- raise ValueError (msg )
903- elif anchor [1 ] in "tb" :
904- msg = "anchor not supported for multiline text"
905- raise ValueError (msg )
906-
907- if font is None :
908- font = self ._getfont (font_size )
909-
910- widths = []
911- max_width : float = 0
912- lines = self ._multiline_split (text )
913- line_spacing = self ._multiline_spacing (font , spacing , stroke_width )
914- for line in lines :
915- line_width = self .textlength (
916- line ,
917- font ,
918- direction = direction ,
919- features = features ,
920- language = language ,
921- embedded_color = embedded_color ,
922- )
923- widths .append (line_width )
924- max_width = max (max_width , line_width )
925-
926- top = xy [1 ]
927- if anchor [1 ] == "m" :
928- top -= (len (lines ) - 1 ) * line_spacing / 2.0
929- elif anchor [1 ] == "d" :
930- top -= (len (lines ) - 1 ) * line_spacing
953+ font , anchor , lines = self ._prepare_multiline_text (
954+ xy ,
955+ text ,
956+ font ,
957+ anchor ,
958+ spacing ,
959+ align ,
960+ direction ,
961+ features ,
962+ language ,
963+ stroke_width ,
964+ embedded_color ,
965+ font_size ,
966+ )
931967
932968 bbox : tuple [float , float , float , float ] | None = None
933969
934- for idx , line in enumerate (lines ):
935- left = xy [0 ]
936- width_difference = max_width - widths [idx ]
937-
938- # first align left by anchor
939- if anchor [0 ] == "m" :
940- left -= width_difference / 2.0
941- elif anchor [0 ] == "r" :
942- left -= width_difference
943-
944- # then align by align parameter
945- if align == "left" :
946- pass
947- elif align == "center" :
948- left += width_difference / 2.0
949- elif align == "right" :
950- left += width_difference
951- else :
952- msg = 'align must be "left", "center" or "right"'
953- raise ValueError (msg )
954-
970+ for xy , line in lines :
955971 bbox_line = self .textbbox (
956- ( left , top ) ,
972+ xy ,
957973 line ,
958974 font ,
959975 anchor ,
@@ -973,8 +989,6 @@ def multiline_textbbox(
973989 max (bbox [3 ], bbox_line [3 ]),
974990 )
975991
976- top += line_spacing
977-
978992 if bbox is None :
979993 return xy [0 ], xy [1 ], xy [0 ], xy [1 ]
980994 return bbox
0 commit comments