Module fpdf.text_region
Classes
class Bullet (bullet_fragments, text_line, bullet_r_margin)
-
Expand source code Browse git
class Bullet: def __init__( self, bullet_fragments, text_line, bullet_r_margin, ): self.fragments = bullet_fragments self.text_line = text_line self.r_margin = bullet_r_margin self.rendered_flag = False def get_fragments_width(self): fragments_width = 0 for frag in self.fragments: fragments_width += frag.get_width() return fragments_width
Methods
def get_fragments_width(self)
class Extents (left: float, right: float)
-
Extents(left, right)
Expand source code Browse git
class Extents(NamedTuple): left: float right: float
Ancestors
- builtins.tuple
Instance variables
var left : float
-
Alias for field number 0
var right : float
-
Alias for field number 1
class ImageParagraph (region, name, align=None, width: float = None, height: float = None, fill_width: bool = False, keep_aspect_ratio=False, top_margin=0, bottom_margin=0, link=None, title=None, alt_text=None)
-
Expand source code Browse git
class ImageParagraph: def __init__( self, region, name, align=None, width: float = None, height: float = None, fill_width: bool = False, keep_aspect_ratio=False, top_margin=0, bottom_margin=0, link=None, title=None, alt_text=None, ): self.region = region self.name = name if align: align = Align.coerce(align) if align not in (Align.L, Align.C, Align.R): raise ValueError( f"Align must be 'LEFT', 'CENTER', or 'RIGHT', not '{align.value}'." ) self.align = align self.width = width self.height = height self.fill_width = fill_width self.keep_aspect_ratio = keep_aspect_ratio self.top_margin = top_margin self.bottom_margin = bottom_margin self.link = link self.title = title self.alt_text = alt_text self.img = self.info = None 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 ) return self def render(self, col_left, col_width, max_height): if not self.img: raise RuntimeError( "ImageParagraph.build_line() must be called before render()." ) is_svg = isinstance(self.info, VectorImageInfo) # pylint: disable=possibly-used-before-assignment if self.height: h = self.height else: 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 if native_w > col_width or self.fill_width: w = col_width else: w = native_w if not self.height: h = w * native_h / native_w if h > max_height: return None x = col_left if self.align: if self.align == Align.R: x += col_width - w elif self.align == Align.C: x += (col_width - w) / 2 if is_svg: return self.region.pdf._vector_image( name=self.name, svg=self.img, info=self.info, x=x, y=None, w=w, h=h, link=self.link, title=self.title, alt_text=self.alt_text, keep_aspect_ratio=self.keep_aspect_ratio, ) return self.region.pdf._raster_image( name=self.name, img=self.img, info=self.info, x=x, y=None, w=w, h=h, link=self.link, title=self.title, alt_text=self.alt_text, dims=None, keep_aspect_ratio=self.keep_aspect_ratio, )
Methods
def build_line(self)
def render(self, col_left, col_width, max_height)
class LineWrapper (line: Sequence, paragraph: Paragraph, first_line: bool = False, last_line: bool = False)
-
Connects each TextLine with the Paragraph it was written to. This allows to access paragraph specific attributes like top/bottom margins when rendering the line.
Expand source code Browse git
class LineWrapper(NamedTuple): """Connects each TextLine with the Paragraph it was written to. This allows to access paragraph specific attributes like top/bottom margins when rendering the line. """ line: Sequence paragraph: Paragraph first_line: bool = False last_line: bool = False
Ancestors
- builtins.tuple
Instance variables
var first_line : bool
-
Alias for field number 2
var last_line : bool
-
Alias for field number 3
var line : Sequence
-
Alias for field number 0
var paragraph : Paragraph
-
Alias for field number 1
class Paragraph (region, text_align=None, line_height=None, top_margin: float = 0, bottom_margin: float = 0, indent: float = 0, bullet_r_margin=None, bullet_string: str = '', skip_leading_spaces: bool = False, wrapmode: WrapMode = None)
-
Expand source code Browse git
class Paragraph: # pylint: disable=function-redefined def __init__( self, region, text_align=None, line_height=None, top_margin: float = 0, bottom_margin: float = 0, indent: float = 0, bullet_r_margin=None, bullet_string: str = "", skip_leading_spaces: bool = False, wrapmode: WrapMode = None, ): self._region = region self.pdf = region.pdf if text_align: text_align = Align.coerce(text_align) if text_align not in (Align.L, Align.C, Align.R, Align.J): raise ValueError( f"Text_align must be 'LEFT', 'CENTER', 'RIGHT', or 'JUSTIFY', not '{text_align.value}'." ) self.text_align = text_align if line_height is None: self.line_height = region.line_height else: self.line_height = line_height self.top_margin = top_margin self.bottom_margin = bottom_margin self.indent = indent self.skip_leading_spaces = skip_leading_spaces if wrapmode is None: self.wrapmode = self._region.wrapmode else: self.wrapmode = WrapMode.coerce(wrapmode) self._text_fragments = [] if bullet_r_margin is None: # Default value of 2 to be multiplied by the conversion factor # for bullet_r_margin is given in mm bullet_r_margin = 2 * get_scale_factor("mm") / self.pdf.k if bullet_string: self.bullet = Bullet( *self.generate_bullet_frags_and_tl(bullet_string, bullet_r_margin), bullet_r_margin, ) else: self.bullet = None def __str__(self): return ( f"Paragraph(text_align={self.text_align}, line_height={self.line_height}, top_margin={self.top_margin}," f" bottom_margin={self.bottom_margin}, skip_leading_spaces={self.skip_leading_spaces}, wrapmode={self.wrapmode}," f" #text_fragments={len(self._text_fragments)})" ) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self._region.end_paragraph() def write(self, text: str, link=None): if not self.pdf.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") normalized_string = self.pdf.normalize_text(text).replace("\r", "") # YYY _preload_font_styles() should accept a "link" argument. fragments = self.pdf._preload_font_styles(normalized_string, markdown=False) if link: for frag in fragments: frag.link = link self._text_fragments.extend(fragments) def generate_bullet_frags_and_tl(self, bullet_string: str, bullet_r_margin: float): if not bullet_string: return None bullet_string = self.pdf.normalize_text(bullet_string) if not self.pdf.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") bullet_fragments = self.pdf._preload_font_styles(bullet_string, markdown=False) fragments_width = 0 for frag in bullet_fragments: fragments_width += frag.get_width() bullet_line_break = MultiLineBreak( bullet_fragments, max_width=self._region.get_width, margins=( self.pdf.c_margin + (self.indent - fragments_width - bullet_r_margin), self.pdf.c_margin, ), align=self.text_align or self._region.text_align or Align.L, wrapmode=self.wrapmode, line_height=self.line_height, skip_leading_spaces=self.skip_leading_spaces or self._region.skip_leading_spaces, ) bullet_text_line = bullet_line_break.get_line() return bullet_fragments, bullet_text_line def ln(self, h=None): if not self.pdf.font_family: raise FPDFException("No font set, you need to call set_font() beforehand") if h is None: h = self.pdf.font_size * self.line_height fragment = self.pdf._preload_font_styles("\n", markdown=False)[0] fragment.graphics_state["font_size_pt"] = h * fragment.k self._text_fragments.append(fragment) def build_lines(self, print_sh) -> List[LineWrapper]: text_lines = [] multi_line_break = MultiLineBreak( self._text_fragments, max_width=self._region.get_width, margins=(self.pdf.c_margin + self.indent, self.pdf.c_margin), align=self.text_align or self._region.text_align or Align.L, print_sh=print_sh, wrapmode=self.wrapmode, line_height=self.line_height, skip_leading_spaces=self.skip_leading_spaces or self._region.skip_leading_spaces, ) self._text_fragments = [] text_line = multi_line_break.get_line() first_line = True while text_line is not None: text_lines.append(LineWrapper(text_line, self, first_line=first_line)) first_line = False text_line = multi_line_break.get_line() if text_lines: last = text_lines[-1] last = LineWrapper( last.line, self, first_line=last.first_line, last_line=True ) text_lines[-1] = last return text_lines
Methods
def build_lines(self, print_sh) ‑> List[LineWrapper]
def generate_bullet_frags_and_tl(self, bullet_string: str, bullet_r_margin: float)
def ln(self, h=None)
def write(self, text: str, link=None)
class ParagraphCollectorMixin (pdf, *args, text=None, text_align='LEFT', line_height: float = 1.0, print_sh: bool = False, skip_leading_spaces: bool = False, wrapmode: WrapMode = None, img=None, img_fill_width=False, **kwargs)
-
Expand source code Browse git
class ParagraphCollectorMixin: def __init__( self, pdf, *args, text=None, text_align="LEFT", line_height: float = 1.0, print_sh: bool = False, skip_leading_spaces: bool = False, wrapmode: WrapMode = None, img=None, img_fill_width=False, **kwargs, ): self.pdf = pdf self.text_align = Align.coerce(text_align) # default for auto paragraphs if self.text_align not in (Align.L, Align.C, Align.R, Align.J): raise ValueError( f"Text_align must be 'LEFT', 'CENTER', 'RIGHT', or 'JUSTIFY', not '{self.text_align.value}'." ) self.line_height = line_height self.print_sh = print_sh self.wrapmode = WrapMode.coerce(wrapmode) self.skip_leading_spaces = skip_leading_spaces self._paragraphs = [] self._active_paragraph = None super().__init__(pdf, *args, **kwargs) if text: self.write(text) if img: self.image(img, fill_width=img_fill_width) def __enter__(self): if self.pdf.is_current_text_region(self): raise FPDFException( f"Unable to enter the same {self.__class__.__name__} context recursively." ) self._page = self.pdf.page self.pdf._push_local_stack() self.pdf.page = 0 self.pdf.register_text_region(self) return self def __exit__(self, exc_type, exc_value, traceback): self.pdf.clear_text_region() self.pdf.page = self._page self.pdf._pop_local_stack() self.render() def _check_paragraph(self): if self._active_paragraph == "EXPLICIT": raise FPDFException( "Conflicts with active paragraph. Either close the current paragraph or write your text inside it." ) if self._active_paragraph is None: p = Paragraph( region=self, text_align=self.text_align, skip_leading_spaces=self.skip_leading_spaces, ) self._paragraphs.append(p) self._active_paragraph = "AUTO" def write(self, text: str, link=None): # pylint: disable=unused-argument self._check_paragraph() self._paragraphs[-1].write(text) def ln(self, h=None): self._check_paragraph() self._paragraphs[-1].ln(h) def paragraph( self, text_align=None, line_height=None, skip_leading_spaces: bool = False, top_margin=0, bottom_margin=0, indent=0, bullet_string="", bullet_r_margin=None, wrapmode: WrapMode = None, ): """ Args: text_align (Align, optional): the horizontal alignment of the paragraph. line_height (float, optional): factor by which the line spacing will be different from the font height. (Default: by region) top_margin (float, optional): how much spacing is added above the paragraph. No spacing will be added at the top of the paragraph if the current y position is at (or above) the top margin of the page. (Default: 0.0) bottom_margin (float, optional): those two values determine how much spacing is added below the paragraph. No spacing will be added at the bottom if it would result in overstepping the bottom margin of the page. (Default: 0.0) indent (float, optional): determines the indentation of the paragraph. (Default: 0.0) bullet_string (str, optional): determines the fragments and text lines of the bullet. (Default: "") bullet_r_margin (float, optional): determines the spacing between the bullet and the bulleted line skip_leading_spaces (float, optional): removes all space characters at the beginning of each line. (Default: False) wrapmode (WrapMode): determines the way text wrapping is handled. (Default: None) """ if self._active_paragraph == "EXPLICIT": raise FPDFException("Unable to nest paragraphs.") p = Paragraph( region=self, text_align=text_align or self.text_align, line_height=line_height, skip_leading_spaces=skip_leading_spaces or self.skip_leading_spaces, wrapmode=wrapmode, top_margin=top_margin, bottom_margin=bottom_margin, indent=indent, bullet_string=bullet_string, bullet_r_margin=bullet_r_margin, ) self._paragraphs.append(p) self._active_paragraph = "EXPLICIT" return p def end_paragraph(self): if not self._active_paragraph: raise FPDFException("No active paragraph to end.") # self._paragraphs[-1].write("\n") self._active_paragraph = None def image( self, name, align=None, width: float = None, height: float = None, fill_width: bool = False, keep_aspect_ratio=False, top_margin=0, bottom_margin=0, link=None, title=None, alt_text=None, ): if self._active_paragraph == "EXPLICIT": raise FPDFException("Unable to nest paragraphs.") if self._active_paragraph: self.end_paragraph() p = ImageParagraph( self, name, align=align, width=width, height=height, fill_width=fill_width, keep_aspect_ratio=keep_aspect_ratio, top_margin=top_margin, bottom_margin=bottom_margin, link=link, title=title, alt_text=alt_text, ) self._paragraphs.append(p)
Subclasses
Methods
def end_paragraph(self)
def image(self, name, align=None, width: float = None, height: float = None, fill_width: bool = False, keep_aspect_ratio=False, top_margin=0, bottom_margin=0, link=None, title=None, alt_text=None)
def ln(self, h=None)
def paragraph(self, text_align=None, line_height=None, skip_leading_spaces: bool = False, top_margin=0, bottom_margin=0, indent=0, bullet_string='', bullet_r_margin=None, wrapmode: WrapMode = None)
-
Args
text_align
:Align
, optional- the horizontal alignment of the paragraph.
line_height
:float
, optional- factor by which the line spacing will be different from the font height. (Default: by region)
top_margin
:float
, optional- how much spacing is added above the paragraph. No spacing will be added at the top of the paragraph if the current y position is at (or above) the top margin of the page. (Default: 0.0)
bottom_margin
:float
, optional- those two values determine how much spacing is added below the paragraph. No spacing will be added at the bottom if it would result in overstepping the bottom margin of the page. (Default: 0.0)
indent
:float
, optional- determines the indentation of the paragraph. (Default: 0.0)
bullet_string
:str
, optional- determines the fragments and text lines of the bullet. (Default: "")
bullet_r_margin
:float
, optional- determines the spacing between the bullet and the bulleted line
skip_leading_spaces
:float
, optional- removes all space characters at the beginning of each line. (Default: False)
wrapmode
:WrapMode
- determines the way text wrapping is handled. (Default: None)
def write(self, text: str, link=None)
class TextColumnarMixin (pdf, *args, l_margin=None, r_margin=None, **kwargs)
-
Enable a TextRegion to perform page breaks
Expand source code Browse git
class TextColumnarMixin: """Enable a TextRegion to perform page breaks""" def __init__(self, pdf, *args, l_margin=None, r_margin=None, **kwargs): super().__init__(*args, **kwargs) self.l_margin = pdf.l_margin if l_margin is None else l_margin left = self.l_margin self.r_margin = pdf.r_margin if r_margin is None else r_margin right = pdf.w - self.r_margin self._set_left_right(left, right) def _set_left_right(self, left, right): left = self.pdf.l_margin if left is None else left right = (self.pdf.w - self.pdf.r_margin) if right is None else right if right <= left: raise FPDFException( f"{self.__class__.__name__}(): " f"Right limit ({right}) lower than left limit ({left})." ) self.extents = Extents(left, right)
Subclasses
class TextColumns (pdf, *args, ncols: int = 1, gutter: float = 10, balance: bool = False, **kwargs)
-
Abstract base class for all text region subclasses.
Expand source code Browse git
class TextColumns(TextRegion, TextColumnarMixin): def __init__( self, pdf, *args, ncols: int = 1, gutter: float = 10, balance: bool = False, **kwargs, ): super().__init__(pdf, *args, **kwargs) self._cur_column = 0 self._ncols = ncols self.balance = balance total_w = self.extents.right - self.extents.left col_width = (total_w - (ncols - 1) * gutter) / ncols # 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)] for i in range(1, ncols): # pylint: disable=unused-variable c_left += col_width + gutter self._cols.append(Extents(c_left, c_left + col_width)) self._first_page_top = max(self.pdf.t_margin, self.pdf.y) def __enter__(self): super().__enter__() self._first_page_top = max(self.pdf.t_margin, self.pdf.y) if self.balance: self._cur_column = 0 self.pdf.x = self._cols[self._cur_column].left return self def new_column(self): if self._paragraphs: self._paragraphs[-1].write(FORM_FEED) else: self.write(FORM_FEED) def _render_page_lines(self, text_lines, top, bottom): """Rendering a set of lines in one or several columns on one page.""" balancing = False next_y = self.pdf.y if self.balance: # Column balancing is currently very simplistic, and only works reliably when # line height doesn't change much within the text block. # The "correct" solution would require an exact precalculation of the hight of # each column with the specific line heights and iterative regrouping of lines, # which seems excessive at this point. # Contribution of a more reliable but still reasonably simple algorithm welcome. page_bottom = bottom if not text_lines: return tot_height = sum(l.line.height for l in text_lines) col_height = tot_height / self._ncols avail_height = bottom - top if col_height < avail_height: balancing = True # We actually have room to balance on this page. # total height divided by n bottom = top + col_height # A bit more generous: Try to keep the rightmost column the shortest. lines_per_column = math.ceil(len(text_lines) / self._ncols) + 0.5 mult_height = text_lines[0].line.height * lines_per_column if mult_height > col_height: bottom = top + mult_height if bottom > page_bottom: # Turns out we don't actually have enough room. bottom = page_bottom balancing = False for c in range(self._cur_column, self._ncols): if not text_lines: return if c != self._cur_column: self._cur_column = c col_left, col_right = self.current_x_extents(0, 0) if self.pdf.x < col_left or self.pdf.x >= col_right: self.pdf.x = col_left if balancing and c == (self._ncols - 1): # Give the last column more space in case the balancing is out of whack. bottom = self.pdf.h - self.pdf.b_margin last_line_height = self._render_column_lines(text_lines, top, bottom) if balancing: new_y = self.pdf.y + last_line_height if new_y > next_y: next_y = new_y if balancing: self.pdf.y = next_y def render(self): if not self._paragraphs: return text_lines = self.collect_lines() if not text_lines: return page_bottom = self.pdf.h - self.pdf.b_margin first_page_top = max(self.pdf.t_margin, self.pdf.y) self._render_page_lines(text_lines, first_page_top, page_bottom) # Note: text_lines is progressively emptied by ._render_column_lines() while text_lines: page_break = self.pdf._perform_page_break_if_need_be(self.pdf.h) if not page_break: # Can happen when rendering a footer in the wrong place - cf. issue #1222 break self._cur_column = 0 self._render_page_lines(text_lines, self.pdf.y, page_bottom) def current_x_extents(self, y, height): left, right = self._cols[self._cur_column] return left, right
Ancestors
Methods
def new_column(self)
def render(self)
Inherited members
class TextRegion (pdf, *args, text=None, text_align='LEFT', line_height: float = 1.0, print_sh: bool = False, skip_leading_spaces: bool = False, wrapmode: WrapMode = None, img=None, img_fill_width=False, **kwargs)
-
Abstract base class for all text region subclasses.
Expand source code Browse git
class TextRegion(ParagraphCollectorMixin): """Abstract base class for all text region subclasses.""" def current_x_extents(self, y, height): """ Return the horizontal extents of the current line. Columnar regions simply return the boundaries of the column. Regions with non-vertical boundaries need to check how the largest font-height in the current line actually fits in there. For that reason we include the current y and the line height. """ raise NotImplementedError() def _render_image_paragraph(self, paragraph): if paragraph.top_margin and self.pdf.y > self.pdf.t_margin: self.pdf.y += paragraph.top_margin col_left, col_right = self.current_x_extents(self.pdf.y, 0) bottom = self.pdf.h - self.pdf.b_margin max_height = bottom - self.pdf.y rendered = paragraph.render(col_left, col_right - col_left, max_height) if rendered: margin = paragraph.bottom_margin if margin and (self.pdf.y + margin) < bottom: self.pdf.y += margin return rendered def _render_column_lines(self, text_lines, top, bottom): if not text_lines: return 0 # no rendered height self.pdf.y = top prev_line_height = 0 last_line_height = None rendered_lines = 0 for tl_wrapper in text_lines: if isinstance(tl_wrapper, ImageParagraph): if self._render_image_paragraph(tl_wrapper): rendered_lines += 1 else: # not enough room for image break else: text_line = tl_wrapper.line text_rendered = False cur_paragraph = tl_wrapper.paragraph cur_bullet = cur_paragraph.bullet for frag in text_line.fragments: if frag.characters: text_rendered = True break 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 ): self.pdf.y += cur_paragraph.top_margin if self.pdf.y + text_line.height > bottom: # => page break last_line_height = prev_line_height break prev_line_height = last_line_height last_line_height = text_line.height col_left, col_right = self.current_x_extents(self.pdf.y, 0) if self.pdf.x < col_left or self.pdf.x >= col_right: self.pdf.x = col_left self.pdf.x += cur_paragraph.indent if cur_bullet and not cur_bullet.rendered_flag: bullet_indent_shift = ( cur_bullet.get_fragments_width() + cur_bullet.r_margin ) self.pdf.x -= bullet_indent_shift self.pdf._render_styled_text_line( cur_bullet.text_line, h=cur_bullet.text_line.height, border=0, new_x=XPos.LEFT, new_y=YPos.TOP, fill=False, ) cur_bullet.rendered_flag = True self.pdf.x += bullet_indent_shift # Don't check the return, we never render past the bottom here. self.pdf._render_styled_text_line( text_line, h=text_line.height, border=0, new_x=XPos.LEFT, new_y=YPos.NEXT, fill=False, ) self.pdf.x -= cur_paragraph.indent if tl_wrapper.last_line: margin = cur_paragraph.bottom_margin if margin and text_rendered and (self.pdf.y + margin) < bottom: self.pdf.y += cur_paragraph.bottom_margin rendered_lines += 1 if text_line.trailing_form_feed: # column break break if rendered_lines: del text_lines[:rendered_lines] return last_line_height def collect_lines(self): text_lines = [] for paragraph in self._paragraphs: if isinstance(paragraph, ImageParagraph): line = paragraph.build_line() text_lines.append(line) else: cur_lines = paragraph.build_lines(self.print_sh) if not cur_lines: continue text_lines.extend(cur_lines) return text_lines def render(self): raise NotImplementedError() def get_width(self, height): start, end = self.current_x_extents(self.pdf.y, height) if self.pdf.x > start and self.pdf.x < end: start = self.pdf.x res = end - start return res
Ancestors
Subclasses
Methods
def collect_lines(self)
def current_x_extents(self, y, height)
-
Return the horizontal extents of the current line. Columnar regions simply return the boundaries of the column. Regions with non-vertical boundaries need to check how the largest font-height in the current line actually fits in there. For that reason we include the current y and the line height.
def get_width(self, height)
def render(self)
Inherited members
class TextRegionMixin (*args, **kwargs)
-
Mix-in to be added to FPDF() in order to support text regions.
Expand source code Browse git
class TextRegionMixin: """Mix-in to be added to FPDF() in order to support text regions.""" def __init__(self, *args, **kwargs): self.clear_text_region() super().__init__(*args, **kwargs) def register_text_region(self, region): self.__current_text_region = region def is_current_text_region(self, region): return self.__current_text_region == region def clear_text_region(self): self.__current_text_region = None
Subclasses
Methods
def clear_text_region(self)
def is_current_text_region(self, region)
def register_text_region(self, region)