Search code examples
pythonpython-3.xpdffpdffpdf2

how can i solve the problem in the language's direction in FPDF2?


I am working on a project to print words in arabic on a PDF file using FPDF2 in python.

but i have faced a lot of problems in showing the language in its original direction.

FPDF2 shows everything language in english direction.

this is how you read english :

enter image description here

and this is how you should read arabic:

enter image description here

but when i put the word above in arabic i face a problem >>>>

The problem is FPDF Shows arabic like this :

1- Tax Before Discount

2- The Total Without

But I need it to be like this:

1- The Total Without

2- Tax Before Discount

i wrote it in english to the problem be understandable.

And This is the problem in arabic language :

enter image description here

But Why This Problem Comes ?


because FPDF2 shows arabic like it shows english, it shows it in the same direction.

This is my code:

from fpdf import FPDF
import arabic_reshaper



def arabic(text):
    if text.isascii():
        return text
    else:
        reshaped_text = arabic_reshaper.reshape(text)
        return reshaped_text[::-1]


pdf = FPDF()
pdf.add_page()
pdf.add_font(family='DejaVu', style='', fname="DejaVuSans.ttf")
pdf.add_font(family='DejaVu', style='B', fname="DejaVuSans.ttf")
pdf.set_font(family='DejaVu', style='', size=55)
pdf.set_margin(5.0)



with pdf.table(cell_fill_color=(200,200,200), cell_fill_mode="ALL", text_align="CENTER") as table:
    names_row = table.row()
    names_row.cell(text=arabic("الإجمالي بدون الضريبة قبل الخصم"))

    pdf.set_fill_color(r=255,g=255,b=255)
    row = table.row()
    row.cell(text=str(5000))




pdf.output('final.pdf')

The result of the code :

enter image description here

The problem will be shown when you have to separate the text to multi lines not in one line.

i have tried a lot of solutions but no one of them made me close.

Thanks.


Solution

  • The Reason of the why this happens that FPDF when it wants to break the text ot lines it goes from left to right to break it like this :

    enter image description here

    but in arabic you should break the text to lines from right to left , but FPDF does not do that it deal with it like it is an english text and it breaks it from left to right like this :

    enter image description here


    this is a problem in the FPDF library.


    how i solved the problem ?

    You should edit the library it self , not how you enter the text into the library.

    then how to edit the library ?

    when you are puting a text in a table inside a cell you are using multi_cell to make that text breakable automaticly by the library.

    then you have to go to the function that it deals with the multi_cell, and edit it to when this function takes arabic it has to reverse the the lines and add them into the cell from bottom to up.

    the lines before adding them into the cell will be like this :

    enter image description here

    and this is the lines in arabic :

    enter image description here

    now we will fuces on the list of lins to solve our problem.

    the list of lins

    like we have said that you should reverse the list of lines before adding them into the final cell , and when you reverse them they will be like this :

    enter image description here

    and this is the solusion in theory.

    but how can i edit the function multi_cell in FPDF to when the enterd text is english the list will be like what it is, But when the enterd text is arabic the list will be reversed, how can we do that ?

    to explain everything that will be dificult but i will show you the code that i added in the function multi_cll to do what i want.

    this is the code that i have added in the function multi_cell :

    if not txt.isascii():
        text_lines.reverse()
    

    and this is where i puted it inside the function to be working well :

    This is the edited function:

    def multi_cell(
            self,
            w,
            h=None,
            txt="",
            border=0,
            align=Align.J,
            fill=False,
            split_only=False,  # DEPRECATED
            link="",
            ln="DEPRECATED",
            max_line_height=None,
            markdown=False,
            print_sh=False,
            new_x=XPos.RIGHT,
            new_y=YPos.NEXT,
            wrapmode: WrapMode = WrapMode.WORD,
            dry_run=False,
            output=MethodReturnValue.PAGE_BREAK,
            center=False,
        ):
            """
            This method allows printing text with line breaks. They can be automatic
            (breaking at the most recent space or soft-hyphen character) as soon as the text
            reaches the right border of the cell, or explicit (via the `\\n` character).
            As many cells as necessary are stacked, one below the other.
            Text can be aligned, centered or justified. The cell block can be framed and
            the background painted.
    
            Args:
                w (float): cell width. If 0, they extend up to the right margin of the page.
                h (float): cell height. Default value: None, meaning to use the current font size.
                txt (str): string to print.
                border: Indicates if borders must be drawn around the cell.
                    The value can be either a number (`0`: no border ; `1`: frame)
                    or a string containing some or all of the following characters
                    (in any order):
                    `L`: left ; `T`: top ; `R`: right ; `B`: bottom. Default value: 0.
                align (fpdf.enums.Align, str): Set text alignment inside the cell.
                    Possible values are:
                    `J`: justify (default value); `L` or empty string: left align;
                    `C`: center; `X`: center around current x position; `R`: right align
                fill (bool): Indicates if the cell background must be painted (`True`)
                    or transparent (`False`). Default value: False.
                split_only (bool): **DEPRECATED since 2.7.4**:
                    Use `dry_run=True` and `output=("LINES",)` instead.
                link (str): optional link to add on the cell, internal
                    (identifier returned by `add_link`) or external URL.
                new_x (fpdf.enums.XPos, str): New current position in x after the call. Default: RIGHT
                new_y (fpdf.enums.YPos, str): New current position in y after the call. Default: NEXT
                ln (int): **DEPRECATED since 2.5.1**: Use `new_x` and `new_y` instead.
                max_line_height (float): optional maximum height of each sub-cell generated
                markdown (bool): enable minimal markdown-like markup to render part
                    of text as bold / italics / underlined. Default to False.
                print_sh (bool): Treat a soft-hyphen (\\u00ad) as a normal printable
                    character, instead of a line breaking opportunity. Default value: False
                wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default),
                    "CHAR" for character based line wrapping.
                dry_run (bool): if `True`, does not output anything in the document.
                    Can be useful when combined with `output`.
                output (fpdf.enums.MethodReturnValue): defines what this method returns.
                    If several enum values are joined, the result will be a tuple.
                center (bool): center the cell horizontally on the page.
    
            Using `new_x=XPos.RIGHT, new_y=XPos.TOP, maximum height=pdf.font_size` is
            useful to build tables with multiline text in cells.
    
            Returns: a single value or a tuple, depending on the `output` parameter value
            """
            if split_only:
                warnings.warn(
                    # pylint: disable=implicit-str-concat
                    'The parameter "split_only" is deprecated.'
                    ' Use instead dry_run=True and output="LINES".',
                    DeprecationWarning,
                    stacklevel=get_stack_level(),
                )
            if dry_run or split_only:
                with self._disable_writing():
                    return self.multi_cell(
                        w=w,
                        h=h,
                        txt=txt,
                        border=border,
                        align=align,
                        fill=fill,
                        link=link,
                        ln=ln,
                        max_line_height=max_line_height,
                        markdown=markdown,
                        print_sh=print_sh,
                        new_x=new_x,
                        new_y=new_y,
                        wrapmode=wrapmode,
                        dry_run=False,
                        split_only=False,
                        output=MethodReturnValue.LINES if split_only else output,
                        center=center,
                    )
            if not self.font_family:
                raise FPDFException("No font set, you need to call set_font() beforehand")
            wrapmode = WrapMode.coerce(wrapmode)
            if isinstance(w, str) or isinstance(h, str):
                raise ValueError(
                    # pylint: disable=implicit-str-concat
                    "Parameter 'w' and 'h' must be numbers, not strings."
                    " You can omit them by passing string content with txt="
                )
            new_x = XPos.coerce(new_x)
            new_y = YPos.coerce(new_y)
            if ln != "DEPRECATED":
                # For backwards compatibility, if "ln" is used we overwrite "new_[xy]".
                if ln == 0:
                    new_x = XPos.RIGHT
                    new_y = YPos.NEXT
                elif ln == 1:
                    new_x = XPos.LMARGIN
                    new_y = YPos.NEXT
                elif ln == 2:
                    new_x = XPos.LEFT
                    new_y = YPos.NEXT
                elif ln == 3:
                    new_x = XPos.RIGHT
                    new_y = YPos.TOP
                else:
                    raise ValueError(
                        f'Invalid value for parameter "ln" ({ln}),'
                        " must be an int between 0 and 3."
                    )
                warnings.warn(
                    (
                        'The parameter "ln" is deprecated.'
                        f" Instead of ln={ln} use new_x=XPos.{new_x.name}, new_y=YPos.{new_y.name}."
                    ),
                    DeprecationWarning,
                    stacklevel=get_stack_level(),
                )
            align = Align.coerce(align)
    
            page_break_triggered = False
    
            if h is None:
                h = self.font_size
            # If width is 0, set width to available width between margins
            if w == 0:
                w = self.w - self.r_margin - self.x
            if center:
                self.x = (
                    self.w / 2 if align == Align.X else self.l_margin + (self.epw - w) / 2
                )
            maximum_allowed_width = w - 2 * self.c_margin
    
            # Calculate text length
            txt = self.normalize_text(txt)
            normalized_string = txt.replace("\r", "")
            styled_text_fragments = self._preload_font_styles(normalized_string, markdown)
    
            prev_font_style, prev_underline = self.font_style, self.underline
            prev_x, prev_y = self.x, self.y
            total_height = 0
    
            if not border:
                border = ""
            elif border == 1:
                border = "LTRB"
    
            text_lines = []
            multi_line_break = MultiLineBreak(
                styled_text_fragments,
                justify=(align == Align.J),
                print_sh=print_sh,
                wrapmode=wrapmode,
            )
            txt_line = multi_line_break.get_line_of_given_width(maximum_allowed_width)
            while (txt_line) is not None:
                text_lines.append(txt_line)
                txt_line = multi_line_break.get_line_of_given_width(maximum_allowed_width)
    
            if not text_lines:  # ensure we display at least one cell - cf. issue #349
                text_lines = [
                    TextLine(
                        "",
                        text_width=0,
                        number_of_spaces=0,
                        justify=False,
                        trailing_nl=False,
                    )
                ]
            should_render_bottom_blank_cell = False
    
    
            if not txt.isascii():
                text_lines.reverse()
    
    
            for text_line_index, text_line in enumerate(text_lines):
                is_last_line = text_line_index == len(text_lines) - 1
                should_render_bottom_blank_cell = False
                if max_line_height is not None and h > max_line_height:
                    current_cell_height = max_line_height
                    h -= current_cell_height
                    if is_last_line:
                        if h > 0 and len(text_lines) > 1:
                            should_render_bottom_blank_cell = True
                        else:
                            h += current_cell_height
                            current_cell_height = h
                else:
                    current_cell_height = h
                has_line_after = not is_last_line or should_render_bottom_blank_cell
                new_page = self._render_styled_text_line(
                    text_line,
                    w,
                    h=current_cell_height,
                    border="".join(
                        (
                            "T" if "T" in border and text_line_index == 0 else "",
                            "L" if "L" in border else "",
                            "R" if "R" in border else "",
                            "B" if "B" in border and not has_line_after else "",
                        )
                    ),
                    new_x=new_x if not has_line_after else XPos.LEFT,
                    new_y=new_y if not has_line_after else YPos.NEXT,
                    align=Align.L if (align == Align.J and is_last_line) else align,
                    fill=fill,
                    link=link,
                )
                page_break_triggered = page_break_triggered or new_page
                total_height += current_cell_height
                if not is_last_line and align == Align.X:
                    # prevent cumulative shift to the left
                    self.x = prev_x
            if should_render_bottom_blank_cell:
                new_page = self._render_styled_text_line(
                    TextLine(
                        "",
                        text_width=0,
                        number_of_spaces=0,
                        justify=False,
                        trailing_nl=False,
                    ),
                    w,
                    h=h,
                    border="".join(
                        (
                            "L" if "L" in border else "",
                            "R" if "R" in border else "",
                            "B" if "B" in border else "",
                        )
                    ),
                    new_x=new_x,
                    new_y=new_y,
                    fill=fill,
                    link=link,
                )
                page_break_triggered = page_break_triggered or new_page
            if new_page and new_y == YPos.TOP:
                # When a page jump is performed and the requested y is TOP,
                # pretend we started at the top of the text block on the new page.
                # cf. test_multi_cell_table_with_automatic_page_break
                prev_y = self.y
            # pylint: disable=undefined-loop-variable
            if text_line and text_line.trailing_nl and new_y in (YPos.LAST, YPos.NEXT):
                # The line renderer can't handle trailing newlines in the text.
                self.ln()
    
            if new_y == YPos.TOP:  # We may have jumped a few lines -> reset
                self.y = prev_y
    
            if markdown:
                if self.font_style != prev_font_style:
                    self.font_style = prev_font_style
                    self.current_font = self.fonts[self.font_family + self.font_style]
                self.underline = prev_underline
    
            output = MethodReturnValue.coerce(output)
            return_value = ()
            if output & MethodReturnValue.PAGE_BREAK:
                return_value += (page_break_triggered,)
            if output & MethodReturnValue.LINES:
                output_lines = []
                for text_line in text_lines:
                    characters = []
                    for frag in text_line.fragments:
                        characters.extend(frag.characters)
                    output_lines.append("".join(characters))
                return_value += (output_lines,)
            if output & MethodReturnValue.HEIGHT:
                return_value += (total_height,)
            if len(return_value) == 1:
                return return_value[0]
            return return_value
    

    This is everything. Thanks