From 225f3b184c71c44745d7c46dc1eeb79e0ade6db7 Mon Sep 17 00:00:00 2001 From: Georg Mischler Date: Sat, 6 Jul 2024 12:41:41 +0200 Subject: [PATCH] Fixing bullet/top_margin combo & fixing width of indented paragraphs --- fpdf/line_break.py | 35 ++++++----- fpdf/text_region.py | 22 +++---- test/text_region/tcols_bullets_indent.pdf | Bin 0 -> 2808 bytes test/text_region/test_text_columns.py | 69 ++++++++++++++++++++++ 4 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 test/text_region/tcols_bullets_indent.pdf diff --git a/fpdf/line_break.py b/fpdf/line_break.py index ab4b88636..0a133ba8c 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -50,7 +50,7 @@ def __init__( self, characters: Union[list, str], graphics_state: dict, - k: float, + k: Number, link: Optional[Union[int, str]] = None, ): if isinstance(characters, str): @@ -352,11 +352,11 @@ def render_pdf_text_core(self, frag_ws, current_ws): class TextLine(NamedTuple): fragments: tuple - text_width: float + text_width: Number number_of_spaces: int align: Align - height: float - max_width: float + height: Number + max_width: Number trailing_nl: bool = False trailing_form_feed: bool = False @@ -389,7 +389,7 @@ class SpaceHint(NamedTuple): original_character_index: int current_line_fragment_index: int current_line_character_index: int - line_width: float + line_width: Number number_of_spaces: int @@ -398,16 +398,16 @@ class HyphenHint(NamedTuple): original_character_index: int current_line_fragment_index: int current_line_character_index: int - line_width: float + line_width: Number number_of_spaces: int curchar: str - curchar_width: float + curchar_width: Number graphics_state: dict - k: float + k: Number class CurrentLine: - def __init__(self, max_width: float, print_sh: bool = False): + def __init__(self, max_width: Number, print_sh: bool = False): """ Per-line text fragment management for use by MultiLineBreak. Args: @@ -444,12 +444,12 @@ def width(self): def add_character( self, character: str, - character_width: float, + character_width: Number, graphics_state: dict, - k: float, + k: Number, original_fragment_index: int, original_character_index: int, - height: float, + height: Number, url: str = None, ): assert character != NEWLINE @@ -573,12 +573,13 @@ class MultiLineBreak: def __init__( self, fragments: Sequence[Fragment], - max_width: Union[float, callable], + max_width: Union[Number, callable], margins: Sequence[Number], + indent: Number = 0.0, align: Align = Align.L, print_sh: bool = False, wrapmode: WrapMode = WrapMode.WORD, - line_height: float = 1.0, + line_height: Number = 1.0, skip_leading_spaces: bool = False, ): """Accept text as Fragments, to be split into individual lines depending @@ -593,6 +594,7 @@ def __init__( lateral boundaries of the enclosing TextRegion() are not vertical. margins (sequence of floats): The extra clearance that may apply at the beginning and/or end of a line (usually either FPDF.c_margin or 0.0 for each side). + indent (float): How much left edge is moved to the right, shortening the line. align (Align): The horizontal alignment of the current text block. print_sh (bool): If True, a soft-hyphen will be rendered normally, instead of triggering a line break. Default: False @@ -608,6 +610,7 @@ def __init__( self.get_width = max_width else: self.get_width = lambda height: max_width + self.indent = indent self.margins = margins self.align = align self.print_sh = print_sh @@ -629,7 +632,7 @@ def get_line(self): current_font_height = 0 - max_width = self.get_width(current_font_height) + max_width = self.get_width(current_font_height) - self.indent # The full max width will be passed on via TextLine to FPDF._render_styled_text_line(). current_line = CurrentLine(max_width=max_width, print_sh=self.print_sh) # For line wrapping we need to use the reduced width. @@ -659,7 +662,7 @@ def get_line(self): if current_fragment.font_size > current_font_height: current_font_height = current_fragment.font_size # document units - max_width = self.get_width(current_font_height) + max_width = self.get_width(current_font_height) - self.indent current_line.max_width = max_width for margin in self.margins: max_width -= margin diff --git a/fpdf/text_region.py b/fpdf/text_region.py index 658ba6d4c..2f3cfa489 100644 --- a/fpdf/text_region.py +++ b/fpdf/text_region.py @@ -156,8 +156,9 @@ def generate_bullet_frags_and_tl(self, bullet_string: str, bullet_r_margin: floa bullet_fragments, max_width=self._region.get_width, margins=( - self.pdf.c_margin + (self.indent - fragments_width - bullet_r_margin), - self.pdf.c_margin, + self._region.h_margins[0] + + (self.indent - fragments_width - bullet_r_margin), + self._region.h_margins[1], ), align=self.text_align or self._region.text_align or Align.L, wrapmode=self.wrapmode, @@ -182,7 +183,8 @@ def build_lines(self, print_sh) -> List[LineWrapper]: multi_line_break = MultiLineBreak( self._text_fragments, max_width=self._region.get_width, - margins=(self.pdf.c_margin + self.indent, self.pdf.c_margin), + margins=self._region.h_margins, + indent=self.indent, align=self.text_align or self._region.text_align or Align.L, print_sh=print_sh, wrapmode=self.wrapmode, @@ -222,7 +224,7 @@ def __init__( title=None, alt_text=None, ): - self.region = region + self._region = region self.name = name if align: align = Align.coerce(align) @@ -246,7 +248,7 @@ def build_line(self): # We do double duty as a "text line wrapper" here, since all the necessary # information is already in the ImageParagraph object. self.name, self.img, self.info = preload_image( - self.region.pdf.image_cache, self.name + self._region.pdf.image_cache, self.name ) return self @@ -261,11 +263,11 @@ def render(self, col_left, col_width, max_height): if self.height: h = self.height else: - native_h = self.info["h"] / self.region.pdf.k + native_h = self.info["h"] / self._region.pdf.k if self.width: w = self.width else: - native_w = self.info["w"] / self.region.pdf.k + native_w = self.info["w"] / self._region.pdf.k if native_w > col_width or self.fill_width: w = col_width else: @@ -281,7 +283,7 @@ def render(self, col_left, col_width, max_height): elif self.align == Align.C: x += (col_width - w) / 2 if is_svg: - return self.region.pdf._vector_image( + return self._region.pdf._vector_image( name=self.name, svg=self.img, info=self.info, @@ -294,7 +296,7 @@ def render(self, col_left, col_width, max_height): alt_text=self.alt_text, keep_aspect_ratio=self.keep_aspect_ratio, ) - return self.region.pdf._raster_image( + return self._region.pdf._raster_image( name=self.name, img=self.img, info=self.info, @@ -519,7 +521,6 @@ def _render_column_lines(self, text_lines, top, bottom): if ( text_rendered and tl_wrapper.first_line - and not cur_bullet and cur_paragraph.top_margin and self.pdf.y > self.pdf.t_margin ): @@ -631,6 +632,7 @@ def __init__( self.balance = balance total_w = self.extents.right - self.extents.left col_width = (total_w - (ncols - 1) * gutter) / ncols + self.h_margins = (pdf.c_margin, pdf.c_margin) # We calculate the column extents once in advance, and store them for lookup. c_left = self.extents.left self._cols = [Extents(c_left, c_left + col_width)] diff --git a/test/text_region/tcols_bullets_indent.pdf b/test/text_region/tcols_bullets_indent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cea176ca9d8d51751d4be2d6134c6c4580ecc250 GIT binary patch literal 2808 zcmb_eYgiN47L`_B5rqm^(0Une0Ran>nIt5ENFjj`AV?ZeQG5k5!C;s{5)>6K6%Z;n z?G?pBkX8|-pd!X6s2}AaXfdFIf>N!}`q`=o7I_NZ+!-Vkp|}3&`7!fl&Dm@3bI!_H z+g{+qb%9(dBnTh_SVT05#Ug~t_&vrHFSF{0)PTh6e1^idXf-SOi;u4OkIlLSOoQsR7f#&f*%06 zQUwn{<7G0OfC%M_wqx-M01QSE4kn9_LEpe~fGAa{OTrN;L^3RDN*bbET^bICDiBm5 z2dHmi1fn?nr;b60$g%jiNCc-ORx!y*WS>Gv#OesSQkepY!;6)P6o?NJiHQ*s$Vbp9 z#U_9XLqwEZ5r>FkND296*)?CalFht&UuQC1Gic|d8r0>y>w3=vfLcSJE3(v03-f&* z*mNcP2e#OiTtd|4i<>t86i8j@DJ$wUnRjp-^Kw$TSJvM7eOPP`cGmLRUVo=-J?_%G zH7;i?)67_jo;`=>H>RjpGTz^AOt<;BfR%pa-uc*)i!P5w4gQh&BJ2(ZX#aKPziHcp zTPnVTZTHxITHW;8b9ixR(2u>HfrbNB&)FN;majVf3=-D}@BWaL!am~eor0x2e08zE z;OXI#;}5Judy-AGv&z7z4o#y*+_EipsQ(WAo8G#!)Umd~_zOF7?-cU(dwr3=+`&in zz_LU!S@u;b=^60u-wQ|tZhq?1> z`=i>RLHLK|5B282k9`&%sh;!G@r@tbdnD^6`+48K`s{3l-!o?($Miz3cGcH=_I;+v zF-F9pxyGEw0jm0>OJS04uV#x1Lk@^eJ2jG94#48y>fx8HViPrgMf%MmN6wlJPN`#> zFRhIUFk;inuFbP2q&ab36Cx#Kt~=~W^|-x5J#Q&;ntF&>9pPWA z$8EZL(0sC4dhe)(oNu}%=7th#=RGNiKDzR#>F1dT?Hg;`tYX#|^@i_zPqM--I^v}A zah*0}gqC>Uzs5R$D_#Gx_AsHI%tNu^-G8Wuk+E5_)TqW|c<^j$fa9EoHCc-sohx1m zU-dQTTun0k&b_;R($P-f8l1>^XGpd*I|k=E-AxUWDWF*n!f4o>>FxXQnTf zd9`k>8M3@xHH3<*+bJX!p`RV;3ETS7-n_;UcdvW5Gps|;nbs{jEMHONyWpu4-==f$ z_Y4n%_ED)cp=}_D;i2hsl=U0z%I%iyvU0LFv+e-Q)b~FzEATf?Ymk{4t;QA@-KuiG zd1`Fvt4$P8>lt{xa^qU1JnKwS%-s)asTsI@z;5?F#XGB0l-nKV3Oje-kE-Qnev4IK zw5We<@3dvi!HHLQ;l_vOhPEy>cP_MZF_)mN$(K6?n>kzLuI=^8G)6E~r@Gh3?{2lD zs0E;^pu0AiI{#-@Jzv%;rl^JcHJ^0!r>MempGj~~b6Q~Dbj33WPR(1{G^W=qIQS4Dk$(MmV4Wys~y zx{akr9ykm)Y7#YC(TT2KxTNNd-&bDmdTpqmnU=j*n>LzQSmu?^4=6Apuc?`bs>*=0 z&aNLt1_uNSl;>)#uLV|&2-AMHwRnC$HOcbAUb>4#yz#amWmC`MNB=SGXQ^&6Mk9R&ASbFt5xb0{$p>gx?Ke6v*Ff6lMX zPq7wQSiGp``Fd`4*6}M!D|nSAR^pncdbU|}>QoXxoy7vV5I`gr z;${X4;;_gNL;-|@c&7k>p-2K=fPW0`knD*!XX5R@ZYuB8eqy}pzlnEh6Y+QQzSTA0 z22QjlKDfsNjy~=X(~Swi6bNFH@v#PCE+LbbI89k?LL4F?kpYNGA||{UfE$fQp#c)$ z4-DT_VgTqQ29fdq_B0HJ@V%RkG4XXz!{`j$tEOQLH{5RC#vqCt-oQ-5sMHy;5R*0| zFHEM+s1L>or}M!uJR=9bUsGyU#EGObByPOD43X|Yh`j=Z7^VOwE