Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop font properties from leaking #384

Merged
merged 62 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
58e7454
small fixes and cleanup
gmischler Sep 25, 2021
c9c4c72
removing mistaken checkin
gmischler Sep 25, 2021
a6048b8
error message fix, expand test coverage
gmischler Oct 14, 2021
d1b9d63
Graphics state stack implemented, rotation fixed
gmischler Oct 18, 2021
d39dae8
include line_width in graphics context stack
gmischler Nov 10, 2021
d255ff9
migrating test_regular_polygon.py to standard fixture
gmischler Nov 12, 2021
cfaf298
changelog
gmischler Mar 12, 2022
4c9a87a
Fix parsing of csv template files
gmischler Sep 18, 2021
51bc274
fixes suggested by static code check
gmischler Sep 18, 2021
ddfbe7c
Update template.py
gmischler Sep 19, 2021
cfa7844
now it's dark.
gmischler Sep 20, 2021
5bb88cc
do some hardcoded template tests without multiline
gmischler Sep 21, 2021
02337e6
first round Splitting Template() into FlexTemplate()
gmischler Sep 25, 2021
a6380ff
offset and rotate for render(), first test
gmischler Sep 25, 2021
8d45a33
small fixes and cleanup
gmischler Sep 25, 2021
e094e1e
test for multipage Template(); Template.code39 with standard template…
gmischler Sep 26, 2021
85aacfb
refer defaults to type handlers, x2 optional for barcodes
gmischler Sep 26, 2021
7ad6bb7
more template and flextemplate tests
gmischler Sep 26, 2021
aff7534
static check fixes
gmischler Sep 26, 2021
ed6f46b
more pylint
gmischler Sep 26, 2021
b171614
blackity-black
gmischler Sep 26, 2021
eecec6f
even blacker
gmischler Sep 26, 2021
b8c3b8f
Expand docstrings, update help, hide private methods.
gmischler Sep 29, 2021
fc985c2
Issues from PR review
gmischler Sep 29, 2021
10e2d35
Issue #226 solved: Rotate anything anywhere
gmischler Sep 30, 2021
422ee8e
Issue #238 solved - split_multicell doesn't modify target document
gmischler Sep 30, 2021
053ee69
Documentation details and corrections
gmischler Sep 30, 2021
fd99366
breaking up long line
gmischler Sep 30, 2021
23236d1
Update CHANGELOG.md
gmischler Sep 30, 2021
9b5e0e7
FlexTemplate.render() with scaling
gmischler Oct 1, 2021
c4dce8a
empty text field - consistency between T and W
gmischler Oct 2, 2021
2d7a13b
Enforce user input types as early as possible
gmischler Oct 2, 2021
4dd951b
Fix to make sure deprecated code39 arguments still work
gmischler Oct 2, 2021
39bcb1c
sync to upstream
gmischler Oct 2, 2021
d604a99
some more test coverage
gmischler Oct 2, 2021
ae8d153
pylint asking for style points...
gmischler Oct 2, 2021
3c4336f
picky black...
gmischler Oct 2, 2021
b30cdb0
Change background default to transparent
gmischler Oct 8, 2021
9dc6af6
Add ellipse element to templates
gmischler Oct 8, 2021
f05362a
Bugfix skipping check for x2 with barcods
gmischler Oct 8, 2021
141a25d
More template tests
gmischler Oct 8, 2021
3a7502a
code cleanup
gmischler Oct 8, 2021
33f1fb3
list template changes to log
gmischler Oct 9, 2021
a938b50
new set_dash_pattern(); dashed_line() retired
gmischler Oct 14, 2021
26bba99
error message fix, expand test coverage
gmischler Oct 14, 2021
8a3976c
Graphics state stack implemented, rotation fixed
gmischler Oct 18, 2021
fa38487
Simplify Templates again, making use of flexible rotation
gmischler Oct 31, 2021
dacb410
code cleanup
gmischler Oct 31, 2021
fd2612f
include line_width in graphics context stack
gmischler Nov 10, 2021
9ef15f7
migrating test_regular_polygon.py to standard fixture
gmischler Nov 12, 2021
cb08c89
fix leaking properties after markdown (#359 et al)
gmischler Apr 9, 2022
93e9158
regression test for #359
gmischler Apr 9, 2022
0993235
fix leaking properties after markdown (#359 et al)
gmischler Apr 9, 2022
a608ed3
regression test for #359
gmischler Apr 9, 2022
5a28a95
Update CHANGELOG.md
gmischler Apr 9, 2022
f2ffcd7
fix rebase
gmischler Apr 10, 2022
4d36ca1
Merge branch 'leak_stop' of https://github.com/gmischler/fpdf2 into l…
gmischler Apr 10, 2022
8647095
Delete badrot3.py, cleanup rebase
gmischler Apr 10, 2022
3ca42da
test for fragment.underline
gmischler Apr 11, 2022
12719b3
more review fixes
gmischler Apr 11, 2022
5f60bdc
Merge remote-tracking branch 'upstream/master' into leak_stop
gmischler Apr 12, 2022
a1f581d
resolving text_mode conflicts
gmischler Apr 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
- the page structure of the documentation has been revised, with a new page about [adding text](https://pyfpdf.github.io/fpdf2/Text.html), thanks to @gmischler
- a warning is now raised if a context manager is used inside an `unbreakable()` section, which is not supported
### Fixed
- No font properties should be leaked anymore after using markdown or in any other situations ([#359](https://github.com/PyFPDF/fpdf2/issues/349), thanks to @gmischler
- If `multi_cell(align="J")` is given text with multiple paragraphs (text followed by an empty line) at once, it now renders the last line of each paragraph left-aligned, instead of just the very last line [#364](https://github.com/PyFPDF/fpdf2/issues/364), thanks to @gmischler
- a regression: now again `multi_cell()` always renders a cell, even if `txt` is an empty string - _cf._ [#349](https://github.com/PyFPDF/fpdf2/issues/349)
- a bug with string width calculation when Markdown is enabled - _cf._ [#351](https://github.com/PyFPDF/fpdf2/issues/351)
Expand Down
14 changes: 7 additions & 7 deletions docs/Templates.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Templates #
# Introduction #

Templates are predefined documents (like invoices, tax forms, etc.), or parts of such documents, where each element (text, lines, barcodes, etc.) has a fixed position (x1, y1, x2, y2), style (font, size, etc.) and a default text.

Expand All @@ -9,9 +9,9 @@ Besides being defined in code, the elements can also be defined in a CSV file or
A template is used like a dict, setting its items' values.


** How to use Templates? **
# How to use Templates #

There are two approaches to using templates, detailed in the sections below:
There are two approaches to using templates.


## Using Template() ##
Expand Down Expand Up @@ -151,7 +151,7 @@ FlexTemplate["company_name"] = "Sample Company"
```


### Details - Template definition ###
# Details - Template definition #

A template definition consists of a number of elements, which have the following properties (columns in a CSV, items in a dict, fields in a database).
Dimensions (except font size, which always uses points) are given in user defined units (default: mm). Those are the units that can be specified when creating a `Template()` or a `FPDF()` instance.
Expand Down Expand Up @@ -214,7 +214,7 @@ Dimensions (except font size, which always uses points) are given in user define
Fields that are not relevant to a specific element type will be ignored there, but if not left empty, they must still adhere to the specified data type (in dicts, string fields may be None).


### How to create a template ###
# How to create a template #

A template can be created in 3 ways:

Expand All @@ -223,7 +223,7 @@ A template can be created in 3 ways:
* By defining the template in a database (this applies to [Web2Py] (Web2Py.md) integration)


### Example - Hardcoded ###
# Example - Hardcoded #

```python

Expand Down Expand Up @@ -257,7 +257,7 @@ f.render("./template.pdf")
See template.py or [Web2Py] (Web2Py.md) for a complete example.


### Example - Elements defined in CSV file ###
# Example - Elements defined in CSV file #
You define your elements in a CSV file "mycsvfile.csv"
that will look like:
```
Expand Down
161 changes: 76 additions & 85 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,6 @@ def __init__(
# Do nothing by default. Allowed values: 'WARN', 'DOWNSCALE':
self.oversized_images = None
self.oversized_images_ratio = 2 # number of pixels per UserSpace point
self._markdown_leak_end_style = False
# Only set if XMP metadata is added to the document:
self._xmp_metadata_obj_id = None
self.struct_builder = StructureTreeBuilder()
Expand Down Expand Up @@ -2260,7 +2259,7 @@ def cell(
# Font styles preloading must be performed before any call to FPDF.get_string_width:
txt = self.normalize_text(txt)
styled_txt_frags = self._preload_font_styles(txt, markdown)
return self._render_styled_cell_text(
return self._render_styled_text_line(
TextLine(
styled_txt_frags,
text_width=0.0,
Expand All @@ -2278,7 +2277,7 @@ def cell(
center=center,
)

def _render_styled_cell_text(
def _render_styled_text_line(
self,
text_line: TextLine,
w: float = None,
Expand Down Expand Up @@ -2357,47 +2356,46 @@ def _render_styled_cell_text(
h = self.font_size
if center:
self.x = self.l_margin + (self.epw - w) / 2
align = "C"
page_break_triggered = self._perform_page_break_if_need_be(h)
s = ""
sl = []
k = self.k
# pylint: disable=invalid-unary-operand-type
# "h" can't actually be None
if fill:
op = "B" if border == 1 else "f"
s = (
sl.append(
f"{self.x * k:.2f} {(self.h - self.y) * k:.2f} "
f"{w * k:.2f} {-h * k:.2f} re {op} "
f"{w * k:.2f} {-h * k:.2f} re {op}"
)
elif border == 1:
s = (
sl.append(
f"{self.x * k:.2f} {(self.h - self.y) * k:.2f} "
f"{w * k:.2f} {-h * k:.2f} re S "
f"{w * k:.2f} {-h * k:.2f} re S"
)
# pylint: enable=invalid-unary-operand-type

if isinstance(border, str):
x = self.x
y = self.y
if "L" in border:
s += (
sl.append(
f"{x * k:.2f} {(self.h - y) * k:.2f} m "
f"{x * k:.2f} {(self.h - (y + h)) * k:.2f} l S "
f"{x * k:.2f} {(self.h - (y + h)) * k:.2f} l S"
)
if "T" in border:
s += (
sl.append(
f"{x * k:.2f} {(self.h - y) * k:.2f} m "
f"{(x + w) * k:.2f} {(self.h - y) * k:.2f} l S "
f"{(x + w) * k:.2f} {(self.h - y) * k:.2f} l S"
)
if "R" in border:
s += (
sl.append(
f"{(x + w) * k:.2f} {(self.h - y) * k:.2f} m "
f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S "
f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S"
)
if "B" in border:
s += (
sl.append(
f"{x * k:.2f} {(self.h - (y + h)) * k:.2f} m "
f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S "
f"{(x + w) * k:.2f} {(self.h - (y + h)) * k:.2f} l S"
)

if self.record_text_quad_points:
Expand All @@ -2418,6 +2416,9 @@ def _render_styled_cell_text(

s_start = self.x
s_width, underlines = 0, []
# We try to avoid modifying global settings for temporary changes.
current_font_style = self.font_style
current_font = self.current_font
if text_line.fragments:
if align == "R":
dx = w - self.c_margin - styled_txt_width
Expand All @@ -2428,17 +2429,15 @@ def _render_styled_cell_text(
s_start += dx

if self.fill_color != self.text_color:
s += f"q {self.text_color} "
style_changed = False
sl.append(self.text_color)

prev_font_style, prev_underline = self.font_style, self.underline
s += (
sl.append(
f"BT {(self.x + dx) * k:.2f} "
f"{(self.h - self.y - 0.5 * h - 0.3 * self.font_size) * k:.2f} Td"
)

if self.text_mode != TextMode.FILL:
s += f" {self.text_mode} Tr {self.line_width:.2f} w"
sl.append(f"{self.text_mode} Tr {self.line_width:.2f} w")

# precursor to self.ws, or manual spacing of unicode fonts/
word_spacing = 0
Expand All @@ -2451,98 +2450,83 @@ def _render_styled_cell_text(
# adjustment before each space
space = escape_parens(" ".encode("utf-16-be").decode("latin-1"))
if self.ws > 0:
s += " 0 Tw"
sl.append("0 Tw")
self.ws = 0
for frag in text_line.fragments:
if self.font_style != frag.style:
self.font_style = frag.style
self.current_font = self.fonts[
self.font_family + self.font_style
]
s += f" /F{self.current_font['i']} {self.font_size_pt:.2f} Tf"
style_changed = True
if current_font_style != frag.style:
current_font_style = frag.style
current_font = self.fonts[self.font_family + current_font_style]
sl.append(f"/F{current_font['i']} {self.font_size_pt:.2f} Tf")
txt_frag_mapped = ""
for char in frag.string:
uni = ord(char)
txt_frag_mapped += chr(self.current_font["subset"].pick(uni))
txt_frag_mapped += chr(current_font["subset"].pick(uni))

# Determine the position of space (" ") in the current subset and
# split words whenever this mapping code is found
words = txt_frag_mapped.split(
chr(self.current_font["subset"].pick(ord(" ")))
chr(current_font["subset"].pick(ord(" ")))
)

s += " ["
words_strl = []
for i, word in enumerate(words):
word = escape_parens(word.encode("utf-16-be").decode("latin-1"))
s += f"({word}) "
is_last_word = (i + 1) == len(words)
if not is_last_word:
if i == 0:
words_strl.append(f"({word})")
else:
adj = -(word_spacing * self.k) * 1000 / self.font_size_pt
s += f"{adj:.3f}({space}) "
words_strl.append(f"{adj:.3f}({space}{word})")
sl.append(f"[{' '.join(words_strl)}] TJ")
if frag.underline:
underlines.append((self.x + dx + s_width, frag.string))
self.underline = frag.underline
s_width += self.get_string_width(
frag.string, True
) + word_spacing * frag.string.count(" ")
s += "] TJ"
frag_width = self.get_normalized_string_width_with_style(
frag.string, current_font_style
)
# /1000 for font space conversion, /100 for percentage -> *0.00001
frag_width *= self.font_stretching * self.font_size * 0.00001
s_width += frag_width + self.ws * frag.string.count(" ")
else:
if word_spacing and word_spacing != self.ws:
self._out(f"{word_spacing * self.k:.3f} Tw")
sl.append(f"{word_spacing * self.k:.3f} Tw")
elif self.ws > 0:
self._out("0 Tw")
sl.append("0 Tw")
self.ws = word_spacing

for frag in text_line.fragments:
if self.font_style != frag.style:
self.font_style = frag.style
self.current_font = self.fonts[
self.font_family + self.font_style
]
s += f" /F{self.current_font['i']} {self.font_size_pt:.2f} Tf"
style_changed = True
if current_font_style != frag.style:
current_font_style = frag.style
current_font = self.fonts[self.font_family + current_font_style]
sl.append(f"/F{current_font['i']} {self.font_size_pt:.2f} Tf")
if self.unifontsubset:
txt_frag_mapped = ""
for char in frag.string:
uni = ord(char)
txt_frag_mapped += chr(
self.current_font["subset"].pick(uni)
)

txt_frag_mapped += chr(current_font["subset"].pick(uni))
txt_frag_escaped = escape_parens(
txt_frag_mapped.encode("utf-16-be").decode("latin-1")
)
else:
txt_frag_escaped = escape_parens(frag.string)
s += f" ({txt_frag_escaped}) Tj"
sl.append(f"({txt_frag_escaped}) Tj")
if frag.underline:
underlines.append((self.x + dx + s_width, frag.string))
self.underline = frag.underline
s_width += self.get_string_width(
frag.string, True
) + self.ws * frag.string.count(" ")
s += " ET"
# Restoring font style & underline mode after handling changes
# by Markdown annotations:
if not self._markdown_leak_end_style:
if self.font_style != prev_font_style:
self.font_style = prev_font_style
self.current_font = self.fonts[self.font_family + self.font_style]
s += f" /F{self.current_font['i']} {self.font_size_pt:.2f} Tf"
self.underline = prev_underline
frag_width = self.get_normalized_string_width_with_style(
frag.string, current_font_style
)
# /1000 for font space conversion, /100 for percentage -> *0.00001
frag_width *= self.font_stretching * self.font_size * 0.00001
s_width += frag_width + self.ws * frag.string.count(" ")
sl.append("ET")

for start_x, txt_frag in underlines:
s += " " + self._do_underline(
start_x, self.y + (0.5 * h) + (0.3 * self.font_size), txt_frag
sl.append(
self._do_underline(
start_x,
self.y + (0.5 * h) + (0.3 * self.font_size),
txt_frag,
current_font,
)
)

if self.fill_color != self.text_color:
s += " Q"
# cf. issue 348 & test_multi_cell_markdown_with_fill_color:
if style_changed:
s += f" /F{self.current_font['i']} {self.font_size_pt:.2f} Tf"

if link:
self.link(
self.x + dx,
Expand All @@ -2551,7 +2535,15 @@ def _render_styled_cell_text(
self.font_size,
link,
)
if s:
if sl:
# If any PDF settings have been left modified, wrap the line in a local context.
if (
current_font_style != self.font_style
or self.fill_color != self.text_color
):
s = f"q {' '.join(sl)} Q"
else:
s = " ".join(sl)
self._out(s)
self.lasth = h

Expand Down Expand Up @@ -2823,8 +2815,6 @@ def multi_cell(
styled_text_fragments = self._preload_font_styles(normalized_string, markdown)

prev_font_style, prev_underline = self.font_style, self.underline
if markdown and not split_only:
self._markdown_leak_end_style = True
prev_x, prev_y = self.x, self.y

if not border:
Expand Down Expand Up @@ -2863,7 +2853,7 @@ def multi_cell(
else:
current_cell_height = h

new_page = self._render_styled_cell_text(
new_page = self._render_styled_text_line(
text_line,
w,
h=current_cell_height,
Expand Down Expand Up @@ -2910,7 +2900,6 @@ def multi_cell(
self.font_style = prev_font_style
self.current_font = self.fonts[self.font_family + self.font_style]
self.underline = prev_underline
self._markdown_leak_end_style = False

return page_break_triggered

Expand Down Expand Up @@ -2978,7 +2967,7 @@ def write(
else:
line_width = full_width
self.ln()
new_page = self._render_styled_cell_text(
new_page = self._render_styled_text_line(
text_line,
line_width,
h=h,
Expand Down Expand Up @@ -4119,8 +4108,10 @@ def _newobj(self):
self._out(f"{self.n} 0 obj")
return self.n

def _do_underline(self, x, y, txt):
def _do_underline(self, x, y, txt, current_font=None):
"Draw an horizontal line starting from (x, y) with a length equal to 'txt' width"
if current_font is None:
current_font = self.current_font
up = self.current_font["up"]
ut = self.current_font["ut"]
w = self.get_string_width(txt, True) + self.ws * txt.count(" ")
Expand Down
Binary file modified test/graphics_context.pdf
Binary file not shown.
Binary file modified test/html/html_features.pdf
Binary file not shown.
Binary file modified test/html/html_justify_paragraph.pdf
Binary file not shown.
Binary file modified test/html/test_img_inside_html_table_centered_with_caption.pdf
Binary file not shown.
Binary file modified test/link_with_zoom_and_shift.pdf
Binary file not shown.
Binary file modified test/links.pdf
Binary file not shown.
Binary file modified test/outline/2_pages_outline.pdf
Binary file not shown.
Binary file modified test/outline/simple_outline.pdf
Binary file not shown.
Binary file modified test/template/flextemplate_elements.pdf
Binary file not shown.
Binary file modified test/template/flextemplate_multipage.pdf
Binary file not shown.
Binary file modified test/template/flextemplate_rotation.pdf
Binary file not shown.
Binary file modified test/template/template_justify.pdf
Binary file not shown.
Binary file modified test/template/template_multipage.pdf
Binary file not shown.
Binary file modified test/template/template_nominal_csv.pdf
Binary file not shown.
Binary file modified test/template/template_nominal_hardcoded.pdf
Binary file not shown.
Binary file modified test/text/cell_markdown_bleeding.pdf
Binary file not shown.
Binary file modified test/text/cell_markdown_right_aligned.pdf
Binary file not shown.
Binary file modified test/text/ln_positioning_and_page_breaking_for_multicell.pdf
Binary file not shown.
Binary file added test/text/multi_cell_font_leakage.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_j_paragraphs.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_ln_newpos.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_markdown.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_markdown_justified.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_markdown_with_fill_color.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_markdown_with_ttf_fonts.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_newpos.pdf
Binary file not shown.
Binary file modified test/text/render_styled_newpos.pdf
Binary file not shown.
Loading