diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b67c80c..bae70b02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ This can also be enabled programmatically with `warnings.simplefilter('default', - [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): allowing images path starting with `data` to be passed as input ### Deprecated - the `center` optional parameter of [`FPDF.cell()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.cell) is **no more** deprecated, as it allows for horizontal positioning, which is different from text alignment control with `align="C"` +### Changed +- useless trailing zeros in the PDF content streams have been removed, which makes the size of the generated PDF files slightly smaller ## [2.7.4] - 2023-04-28 ### Added diff --git a/docs/Links.md b/docs/Links.md index ec1af546c..fc035706c 100644 --- a/docs/Links.md +++ b/docs/Links.md @@ -104,7 +104,7 @@ Other methods can also insert internal links: * [FPDF.link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.link) * [FPDF.write_html](HTML.md) using anchor tags: `link text` -The unit tests `test_internal_links()` in [test_links.py](https://github.com/PyFPDF/fpdf2/blob/master/test/test_links.py) provides examples for all of those methods. +The unit test `test_internal_links()` in [test_links.py](https://github.com/PyFPDF/fpdf2/blob/master/test/test_links.py) provides examples for all of those methods. ## Links to other documents on the filesystem ## diff --git a/fpdf/annotations.py b/fpdf/annotations.py index 687cae8b2..f731e2fb2 100644 --- a/fpdf/annotations.py +++ b/fpdf/annotations.py @@ -16,6 +16,7 @@ from .syntax import create_dictionary_string as pdf_dict from .syntax import create_list_string as pdf_list from .syntax import iobj_ref as pdf_ref +from .util import nbr2str # cf. https://docs.verapdf.org/validation/pdfa-part1/#rule-653-2 @@ -47,7 +48,9 @@ def __init__( ): self.type = Name("Annot") self.subtype = Name(subtype) - self.rect = f"[{x:.2f} {y:.2f} {x + width:.2f} {y - height:.2f}]" + self.rect = ( + f"[{nbr2str(x)} {nbr2str(y)} {nbr2str(x + width)} {nbr2str(y - height)}]" + ) self.border = f"[0 0 {border_width}]" self.f_t = Name(field_type) if field_type else None self.v = value @@ -59,14 +62,14 @@ def __init__( self.t = PDFString(title) if title else None self.m = PDFDate(modification_time) if modification_time else None self.quad_points = ( - pdf_list(f"{quad_point:.2f}" for quad_point in quad_points) + pdf_list(f"{nbr2str(quad_point)}" for quad_point in quad_points) if quad_points else None ) self.p = None # must always be set before calling .serialize() self.name = name self.ink_list = ( - ("[" + pdf_list(f"{coord:.2f}" for coord in ink_list) + "]") + ("[" + pdf_list(f"{nbr2str(coord)}" for coord in ink_list) + "]") if ink_list else None ) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 393e896f5..88eaae213 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -13,7 +13,7 @@ PDFStyleKeys, ) from .syntax import Name, Raw -from .util import escape_parens +from .util import escape_parens, nbr2str __pdoc__ = {"force_nodocument": False} @@ -87,21 +87,6 @@ def _check_range(value, minimum=0.0, maximum=1.0): return value -def number_to_str(number): - """ - Convert a decimal number to a minimal string representation (no trailing 0 or .). - - Args: - number (Number): the number to be converted to a string. - - Returns: - The number's string representation. - """ - # this approach tries to produce minimal representations of floating point numbers - # but can also produce "-0". - return f"{number:.4f}".rstrip("0").rstrip(".") - - # this maybe should live in fpdf.syntax def render_pdf_primitive(primitive): """ @@ -143,7 +128,7 @@ def render_pdf_primitive(primitive): elif isinstance(primitive, bool): # has to come before number check output = ["false", "true"][primitive] elif isinstance(primitive, NumberClass): - output = number_to_str(primitive) + output = nbr2str(primitive) elif isinstance(primitive, (list, tuple)): output = "[" + " ".join(render_pdf_primitive(val) for val in primitive) + "]" elif isinstance(primitive, dict): @@ -209,7 +194,7 @@ def colors(self): return self[:-1] def serialize(self) -> str: - return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}" + return " ".join(nbr2str(val) for val in self.colors) + f" {self.OPERATOR}" __pdoc__["DeviceRGB.OPERATOR"] = False @@ -252,7 +237,7 @@ def colors(self): return self[:-1] def serialize(self) -> str: - return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}" + return " ".join(nbr2str(val) for val in self.colors) + f" {self.OPERATOR}" __pdoc__["DeviceGray.OPERATOR"] = False @@ -308,7 +293,7 @@ def colors(self): return self[:-1] def serialize(self) -> str: - return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}" + return " ".join(nbr2str(val) for val in self.colors) + f" {self.OPERATOR}" __pdoc__["DeviceCMYK.OPERATOR"] = False @@ -481,7 +466,7 @@ class Point(NamedTuple): def render(self): """Render the point to the string `"x y"` for emitting to a PDF.""" - return f"{number_to_str(self.x)} {number_to_str(self.y)}" + return f"{nbr2str(self.x)} {nbr2str(self.y)}" def dot(self, other): """ @@ -679,7 +664,7 @@ def __matmul__(self, other): return NotImplemented def __str__(self): - return f"(x={number_to_str(self.x)}, y={number_to_str(self.y)})" + return f"(x={nbr2str(self.x)}, y={nbr2str(self.y)})" class Transform(NamedTuple): @@ -1019,18 +1004,18 @@ def render(self, last_item): A tuple of `(str, last_item)`. `last_item` is returned unchanged. """ return ( - f"{number_to_str(self.a)} {number_to_str(self.b)} " - f"{number_to_str(self.c)} {number_to_str(self.d)} " - f"{number_to_str(self.e)} {number_to_str(self.f)} cm", + f"{nbr2str(self.a)} {nbr2str(self.b)} " + f"{nbr2str(self.c)} {nbr2str(self.d)} " + f"{nbr2str(self.e)} {nbr2str(self.f)} cm", last_item, ) def __str__(self): return ( f"transform: [" - f"{number_to_str(self.a)} {number_to_str(self.b)} 0; " - f"{number_to_str(self.c)} {number_to_str(self.d)} 0; " - f"{number_to_str(self.e)} {number_to_str(self.f)} 1]" + f"{nbr2str(self.a)} {nbr2str(self.b)} 0; " + f"{nbr2str(self.c)} {nbr2str(self.d)} 0; " + f"{nbr2str(self.e)} {nbr2str(self.f)} 1]" ) @@ -3155,7 +3140,7 @@ def render(self, gsd_registry, first_point, scale, height, starting_style): render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) - + f" {number_to_str(style.stroke_dash_phase)} d", + + f" {nbr2str(style.stroke_dash_phase)} d", ) render_list.append("Q") @@ -3220,7 +3205,7 @@ def render_debug( render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) - + f" {number_to_str(style.stroke_dash_phase)} d", + + f" {nbr2str(style.stroke_dash_phase)} d", ) render_list.append("Q") @@ -4076,8 +4061,7 @@ def build_render_list( if emit_dash is not None: render_list.append( - render_pdf_primitive(emit_dash[0]) - + f" {number_to_str(emit_dash[1])} d" + render_pdf_primitive(emit_dash[0]) + f" {nbr2str(emit_dash[1])} d" ) if debug_stream: diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 5a15d0820..c74a11fa6 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -89,6 +89,7 @@ class Image: from .util import ( escape_parens, get_scale_factor, + nbr2str, ) # Public global variables: @@ -801,7 +802,7 @@ def add_page( self._out("2 J") # Set line cap style to square self.line_width = lw # Set line width - self._out(f"{lw * self.k:.2f} w") + self._out(f"{nbr2str(lw * self.k)} w") # Set font if family: @@ -821,7 +822,7 @@ def add_page( if self.line_width != lw: # Restore line width self.line_width = lw - self._out(f"{lw * self.k:.2f} w") + self._out(f"{nbr2str(lw * self.k)} w") if family: self.set_font(family, style, size) # Restore font @@ -979,7 +980,7 @@ def set_line_width(self, width): """ self.line_width = width if self.page > 0: - self._out(f"{width * self.k:.2f} w") + self._out(f"{nbr2str(width * self.k)} w") def set_page_background(self, background): """ @@ -1160,8 +1161,8 @@ def line(self, x1, y1, x2, y2): y2 (float): Ordinate of second point """ self._out( - f"{x1 * self.k:.2f} {(self.h - y1) * self.k:.2f} m {x2 * self.k:.2f} " - f"{(self.h - y2) * self.k:.2f} l S" + f"{nbr2str(x1 * self.k)} {nbr2str((self.h - y1) * self.k)} m " + f"{nbr2str(x2 * self.k)} {nbr2str((self.h - y2) * self.k)} l S" ) @check_page @@ -1197,7 +1198,7 @@ def polyline(self, point_list, fill=False, polygon=False, style=None): operator = "m" for point in point_list: self._out( - f"{point[0] * self.k:.2f} {(self.h - point[1]) * self.k:.2f} {operator}" + f"{nbr2str(point[0] * self.k)} {nbr2str((self.h - point[1]) * self.k)} {operator}" ) operator = "l" if polygon: @@ -1283,8 +1284,8 @@ def rect(self, x, y, w, h, style=None, round_corners=False, corner_radius=0): self._draw_rounded_rect(x, y, w, h, style, round_corners, corner_radius) else: self._out( - f"{x * self.k:.2f} {(self.h - y) * self.k:.2f} {w * self.k:.2f} " - f"{-h * self.k:.2f} re {style.operator}" + f"{nbr2str(x * self.k)} {nbr2str((self.h - y) * self.k)} {nbr2str(w * self.k)} " + f"{nbr2str(-h * self.k)} re {style.operator}" ) def _draw_rounded_rect(self, x, y, w, h, style, round_corners, r): @@ -1387,31 +1388,31 @@ def _draw_ellipse(self, x, y, w, h, operator): self._out( ( - f"{(cx + rx) * self.k:.2f} {(self.h - cy) * self.k:.2f} m " - f"{(cx + rx) * self.k:.2f} {(self.h - cy + ly) * self.k:.2f} " - f"{(cx + lx) * self.k:.2f} {(self.h - cy + ry) * self.k:.2f} " - f"{cx * self.k:.2f} {(self.h - cy + ry) * self.k:.2f} c" + f"{nbr2str((cx + rx) * self.k)} {nbr2str((self.h - cy) * self.k)} m " + f"{nbr2str((cx + rx) * self.k)} {nbr2str((self.h - cy + ly) * self.k)} " + f"{nbr2str((cx + lx) * self.k)} {nbr2str((self.h - cy + ry) * self.k)} " + f"{nbr2str(cx * self.k)} {nbr2str((self.h - cy + ry) * self.k)} c" ) ) self._out( ( - f"{(cx - lx) * self.k:.2f} {(self.h - cy + ry) * self.k:.2f} " - f"{(cx - rx) * self.k:.2f} {(self.h - cy + ly) * self.k:.2f} " - f"{(cx - rx) * self.k:.2f} {(self.h - cy) * self.k:.2f} c" + f"{nbr2str((cx - lx) * self.k)} {nbr2str((self.h - cy + ry) * self.k)} " + f"{nbr2str((cx - rx) * self.k)} {nbr2str((self.h - cy + ly) * self.k)} " + f"{nbr2str((cx - rx) * self.k)} {nbr2str((self.h - cy) * self.k)} c" ) ) self._out( ( - f"{(cx - rx) * self.k:.2f} {(self.h - cy - ly) * self.k:.2f} " - f"{(cx - lx) * self.k:.2f} {(self.h - cy - ry) * self.k:.2f} " - f"{cx * self.k:.2f} {(self.h - cy - ry) * self.k:.2f} c" + f"{nbr2str((cx - rx) * self.k)} {nbr2str((self.h - cy - ly) * self.k)} " + f"{nbr2str((cx - lx) * self.k)} {nbr2str((self.h - cy - ry) * self.k)} " + f"{nbr2str(cx * self.k)} {nbr2str((self.h - cy - ry) * self.k)} c" ) ) self._out( ( - f"{(cx + lx) * self.k:.2f} {(self.h - cy - ry) * self.k:.2f} " - f"{(cx + rx) * self.k:.2f} {(self.h - cy - ly) * self.k:.2f} " - f"{(cx + rx) * self.k:.2f} {(self.h - cy) * self.k:.2f} c {operator}" + f"{nbr2str((cx + lx) * self.k)} {nbr2str((self.h - cy - ry) * self.k)} " + f"{nbr2str((cx + rx) * self.k)} {nbr2str((self.h - cy - ly) * self.k)} " + f"{nbr2str((cx + rx) * self.k)} {nbr2str((self.h - cy) * self.k)} c {operator}" ) ) @@ -1594,13 +1595,13 @@ def derivative_evaluate(eta): # Move to the start point if start_from_center: - self._out(f"{cx * self.k:.2f} {(self.h - cy) * self.k:.2f} m") + self._out(f"{nbr2str(cx * self.k)} {nbr2str((self.h - cy) * self.k)} m") self._out( - f"{start_point[0] * self.k:.2f} {(self.h - start_point[1]) * self.k:.2f} l" + f"{nbr2str(start_point[0] * self.k)} {nbr2str((self.h - start_point[1]) * self.k)} l" ) else: self._out( - f"{start_point[0] * self.k:.2f} {(self.h - start_point[1]) * self.k:.2f} m" + f"{nbr2str(start_point[0] * self.k)} {nbr2str((self.h - start_point[1]) * self.k)} m" ) # Number of curves to use, maximal segment angle is 2*PI/max_curves @@ -1633,9 +1634,10 @@ def derivative_evaluate(eta): self._out( ( - f"{control_point_1[0] * self.k:.2f} {(self.h - control_point_1[1]) * self.k:.2f} " - f"{control_point_2[0] * self.k:.2f} {(self.h - control_point_2[1]) * self.k:.2f} " - f"{p2[0] * self.k:.2f} {(self.h - p2[1]) * self.k:.2f} c" + end + f"{nbr2str(control_point_1[0] * self.k)} {nbr2str((self.h - control_point_1[1]) * self.k)} " + f"{nbr2str(control_point_2[0] * self.k)} {nbr2str((self.h - control_point_2[1]) * self.k)} " + f"{nbr2str(p2[0] * self.k)} {nbr2str((self.h - p2[1]) * self.k)} c" + + end ) ) @@ -1644,7 +1646,7 @@ def derivative_evaluate(eta): self._out(f"h {style.operator}") else: self._out( - f"{cx * self.k:.2f} {(self.h - cy) * self.k:.2f} l {style.operator}" + f"{nbr2str(cx * self.k)} {nbr2str((self.h - cy) * self.k)} l {style.operator}" ) def solid_arc( @@ -1834,7 +1836,7 @@ def set_font(self, family=None, style="", size=0): self.font_size_pt = size self.current_font = self.fonts[fontkey] if self.page > 0: - self._out(f"BT /F{self.current_font.i} {self.font_size_pt:.2f} Tf ET") + self._out(f"BT /F{self.current_font.i} {nbr2str(self.font_size_pt)} Tf ET") def set_font_size(self, size): """ @@ -1851,7 +1853,7 @@ def set_font_size(self, size): raise FPDFException( "Cannot set font size: a font must be selected first" ) - self._out(f"BT /F{self.current_font.i} {self.font_size_pt:.2f} Tf ET") + self._out(f"BT /F{self.current_font.i} {nbr2str(self.font_size_pt)} Tf ET") def set_char_spacing(self, spacing): """ @@ -1867,7 +1869,7 @@ def set_char_spacing(self, spacing): return self.char_spacing = spacing if self.page > 0: - self._out(f"BT {spacing:.2f} Tc ET") + self._out(f"BT {nbr2str(spacing)} Tc ET") def set_stretching(self, stretching): """ @@ -1881,7 +1883,7 @@ def set_stretching(self, stretching): return self.font_stretching = stretching if self.page > 0: - self._out(f"BT {stretching:.2f} Tz ET") + self._out(f"BT {nbr2str(stretching)} Tz ET") def set_fallback_fonts(self, fallback_fonts, exact_match=True): """ @@ -2309,9 +2311,9 @@ def text(self, x, y, txt=""): txt2 = escape_parens(txt_mapped.encode("utf-16-be").decode("latin-1")) else: txt2 = escape_parens(txt) - sl = [f"BT {x * self.k:.2f} {(self.h - y) * self.k:.2f} Td"] + sl = [f"BT {nbr2str(x * self.k)} {nbr2str((self.h - y) * self.k)} Td"] if self.text_mode != TextMode.FILL: - sl.append(f" {self.text_mode} Tr {self.line_width:.2f} w") + sl.append(f" {self.text_mode} Tr {nbr2str(self.line_width)} w") sl.append(f"({txt2}) Tj ET") if (self.underline and txt != "") or self._record_text_quad_points: w = self.get_string_width(txt, normalized=True, markdown=False) @@ -2357,8 +2359,8 @@ def rotate(self, angle, x=None, y=None): cx = x * self.k cy = (self.h - y) * self.k s = ( - f"q {c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " - f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm" + f"q {c:.5F} {s:.5F} {-s:.5F} {c:.5F} {nbr2str(cx)} {nbr2str(cy)} cm " + f"1 0 0 1 {nbr2str(-cx)} {nbr2str(-cy)} cm" ) self._out(s) @@ -2398,8 +2400,8 @@ def rotation(self, angle, x=None, y=None): cx, cy = x * self.k, (self.h - y) * self.k with self.local_context(): self._out( - f"{c:.5F} {s:.5F} {-s:.5F} {c:.5F} {cx:.2F} {cy:.2F} cm " - f"1 0 0 1 {-cx:.2F} {-cy:.2F} cm" + f"{c:.5F} {s:.5F} {-s:.5F} {c:.5F} {nbr2str(cx)} {nbr2str(cy)} cm " + f"1 0 0 1 {nbr2str(-cx)} {nbr2str(-cy)} cm" ) yield @@ -2432,8 +2434,8 @@ def skew(self, ax=0, ay=0, x=None, y=None): cx, cy = x * self.k, (self.h - y) * self.k with self.local_context(): self._out( - f"1 {ay:.5f} {ax:.5f} 1 {cx:.2f} {cy:.2f} cm " - f"1 0 0 1 -{cx:.2f} -{cy:.2f} cm" + f"1 {ay:.5f} {ax:.5f} 1 {nbr2str(cx)} {nbr2str(cy)} cm " + f"1 0 0 1 -{nbr2str(cx)} -{nbr2str(cy)} cm" ) yield @@ -2466,8 +2468,8 @@ def mirror(self, origin, angle): with self.local_context(): self._out( - f"{a:.5f} {b:.5f} {b:.5f} {a*-1:.5f} {cx:.2f} {cy:.2f} cm " - f"1 0 0 1 -{cx:.2f} -{cy:.2f} cm" + f"{a:.5f} {b:.5f} {b:.5f} {a*-1:.5f} {nbr2str(cx)} {nbr2str(cy)} cm " + f"1 0 0 1 -{nbr2str(cx)} -{nbr2str(cy)} cm" ) yield @@ -2792,13 +2794,13 @@ def _render_styled_text_line( if fill: op = "B" if border == 1 else "f" sl.append( - f"{self.x * k:.2f} {(self.h - self.y) * k:.2f} " - f"{w * k:.2f} {-h * k:.2f} re {op}" + f"{nbr2str(self.x * k)} {nbr2str((self.h - self.y) * k)} " + f"{nbr2str(w * k)} {nbr2str(-h * k)} re {op}" ) elif border == 1: sl.append( - f"{self.x * k:.2f} {(self.h - self.y) * k:.2f} " - f"{w * k:.2f} {-h * k:.2f} re S" + f"{nbr2str(self.x * k)} {nbr2str((self.h - self.y) * k)} " + f"{nbr2str(w * k)} {nbr2str(-h * k)} re S" ) # pylint: enable=invalid-unary-operand-type @@ -2807,23 +2809,23 @@ def _render_styled_text_line( y = self.y if "L" in border: 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"{nbr2str(x * k)} {nbr2str((self.h - y) * k)} m " + f"{nbr2str(x * k)} {nbr2str((self.h - (y + h)) * k)} l S" ) if "T" in border: 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"{nbr2str(x * k)} {nbr2str((self.h - y) * k)} m " + f"{nbr2str((x + w) * k)} {nbr2str((self.h - y) * k)} l S" ) if "R" in border: 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"{nbr2str((x + w) * k)} {nbr2str((self.h - y) * k)} m " + f"{nbr2str((x + w) * k)} {nbr2str((self.h - (y + h)) * k)} l S" ) if "B" in border: 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"{nbr2str(x * k)} {nbr2str((self.h - (y + h)) * k)} m " + f"{nbr2str((x + w) * k)} {nbr2str((self.h - (y + h)) * k)} l S" ) if self._record_text_quad_points: @@ -2861,8 +2863,8 @@ def _render_styled_text_line( w - self.c_margin - self.c_margin - styled_txt_width ) / text_line.number_of_spaces sl.append( - f"BT {(self.x + dx) * k:.2f} " - f"{(self.h - self.y - 0.5 * h - 0.3 * max_font_size) * k:.2f} Td" + f"BT {nbr2str((self.x + dx) * k)} " + f"{nbr2str((self.h - self.y - 0.5 * h - 0.3 * max_font_size) * k)} Td" ) for i, frag in enumerate(text_line.fragments): if word_spacing and frag.font_stretching != 100: @@ -2872,25 +2874,25 @@ def _render_styled_text_line( frag_ws = word_spacing if current_font_stretching != frag.font_stretching: current_font_stretching = frag.font_stretching - sl.append(f"{frag.font_stretching:.2f} Tz") + sl.append(f"{nbr2str(frag.font_stretching)} Tz") if current_char_spacing != frag.char_spacing: current_char_spacing = frag.char_spacing - sl.append(f"{frag.char_spacing:.2f} Tc") + sl.append(f"{nbr2str(frag.char_spacing)} Tc") if current_font != frag.font or current_char_vpos != frag.char_vpos: if current_char_vpos != frag.char_vpos: current_char_vpos = frag.char_vpos current_font = frag.font - sl.append(f"/F{frag.font.i} {frag.font_size_pt:.2f} Tf") + sl.append(f"/F{frag.font.i} {nbr2str(frag.font_size_pt)} Tf") lift = frag.lift if lift != 0.0: # Use text rise operator: - sl.append(f"{lift:.2f} Ts") + sl.append(f"{nbr2str(lift)} Ts") if ( frag.text_mode != TextMode.FILL or frag.text_mode != current_text_mode ): current_text_mode = frag.text_mode - sl.append(f"{frag.text_mode} Tr {frag.line_width:.2f} w") + sl.append(f"{frag.text_mode} Tr {nbr2str(frag.line_width)} w") if frag.is_ttf_font: mapped_text = "" @@ -3755,8 +3757,8 @@ def image( raise ValueError(f"Unsupported 'x' value passed to .image(): {x}") stream_content = ( - f"q {w * self.k:.2f} 0 0 {h * self.k:.2f} {x * self.k:.2f} " - f"{(self.h - y - h) * self.k:.2f} cm /I{info['i']} Do Q" + f"q {nbr2str(w * self.k)} 0 0 {nbr2str(h * self.k)} {nbr2str(x * self.k)} " + f"{nbr2str((self.h - y - h) * self.k)} cm /I{info['i']} Do Q" ) if title or alt_text: with self._marked_sequence(title=title, alt_text=alt_text): @@ -3803,7 +3805,7 @@ def preload_image(self, name, dims=None): info["usages"] += 1 else: info = ImageInfo(get_img_info(name, img, self.image_filter, dims)) - info["i"] = len(self.images) + 1 + info["i"] = self._new_img_id() info["usages"] = 1 info["iccp_i"] = None iccp = info.get("iccp") @@ -3951,7 +3953,7 @@ def _downscale_image(self, name, img, info, w, h): name, img or load_image(name), self.image_filter, dims ) ) - info["i"] = len(self.images) + 1 + info["i"] = self._new_img_id() info["usages"] = 1 self.images[lowres_name] = info LOGGER.debug( @@ -3971,6 +3973,9 @@ def _downscale_image(self, name, img, info, w, h): info = lowres_info return info + def _new_img_id(self): + return (max(img["i"] for img in self.images.values()) if self.images else 0) + 1 + @contextmanager def _marked_sequence(self, **kwargs): """ @@ -4248,9 +4253,9 @@ def _do_underline(self, x, y, w, current_font=None): up = current_font.up ut = current_font.ut return ( - f"{x * self.k:.2f} " - f"{(self.h - y + up / 1000 * self.font_size) * self.k:.2f} " - f"{w * self.k:.2f} {-ut / 1000 * self.font_size_pt:.2f} re f" + f"{nbr2str(x * self.k)} " + f"{nbr2str((self.h - y + up / 1000 * self.font_size) * self.k)} " + f"{nbr2str(w * self.k)} {nbr2str(-ut / 1000 * self.font_size_pt)} re f" ) def _out(self, s): @@ -4404,8 +4409,8 @@ def rect_clip(self, x, y, w, h): """ self._out( ( - f"q {x * self.k:.2f} {(self.h - y - h) * self.k:.2f} {w * self.k:.2f} " - f"{h * self.k:.2f} re W n" + f"q {nbr2str(x * self.k)} {nbr2str((self.h - y - h) * self.k)} {nbr2str(w * self.k)} " + f"{nbr2str(h * self.k)} re W n" ) ) yield diff --git a/fpdf/output.py b/fpdf/output.py index 1e570a362..bb45f10ea 100644 --- a/fpdf/output.py +++ b/fpdf/output.py @@ -21,6 +21,7 @@ from .syntax import create_dictionary_string as pdf_dict from .syntax import create_list_string as pdf_list from .syntax import iobj_ref as pdf_ref +from .util import nbr2str from fontTools import ttLib from fontTools import subset as ftsubset @@ -1030,7 +1031,7 @@ def _tt_font_widths(font, maxUni): def _dimensions_to_mediabox(dimensions): width_pt, height_pt = dimensions - return f"[0 0 {width_pt:.2f} {height_pt:.2f}]" + return f"[0 0 {nbr2str(width_pt)} {nbr2str(height_pt)}]" def _sizeof_fmt(num, suffix="B"): diff --git a/fpdf/util.py b/fpdf/util.py index d31b7e3c2..22a8496c3 100644 --- a/fpdf/util.py +++ b/fpdf/util.py @@ -7,6 +7,12 @@ PIL_MEM_BLOCK_SIZE_IN_MIB = 16 +def nbr2str(nbr): + # Reduce the overall PDF document size by limiting the number of trailing zeros + # Recipe from https://stackoverflow.com/a/2440786/636849 + return f"{nbr:.2f}".rstrip("0").rstrip(".") + + def buffer_subst(buffer, placeholder, value): buffer_size = len(buffer) assert len(placeholder) == len(value), f"placeholder={placeholder} value={value}" diff --git a/test/drawing/test_drawing.py b/test/drawing/test_drawing.py index 8be2a4fdf..89917155b 100644 --- a/test/drawing/test_drawing.py +++ b/test/drawing/test_drawing.py @@ -74,8 +74,8 @@ def test_range_check(self): fpdf.drawing._check_range(1, minimum=0.0, maximum=1.0) @pytest.mark.parametrize("number, converted", parameters.numbers) - def test_number_to_str(self, number, converted): - assert fpdf.drawing.number_to_str(number) == converted + def test_nbr2str(self, number, converted): + assert fpdf.drawing.nbr2str(number) == converted @pytest.mark.parametrize("primitive, result", parameters.pdf_primitives) def test_render_primitive(self, primitive, result):