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 :
and this is how you should read arabic:
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 :
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 :
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.
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 :
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 :
You should edit the library it self , not how you enter the text into 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 :
and this is the lines in arabic :
now we will fuces on the list of lins to solve our problem.
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 :
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