From 9d0ea70028ff82b4709705de4e2b59d7102ee7ad Mon Sep 17 00:00:00 2001 From: TheeCoderAhmed Date: Wed, 4 Nov 2026 05:58:28 +0300 Subject: [PATCH] Left align text when center_text is false The writers docs say that center_text=False left aligns the text. The position was moved to the start of the barcode, but both writers kept anchoring the text by its center, so half of it ended up over the quiet zone and long text was cut off at the left edge. Anchor the text at its start in that case. Text blocks between EAN guards are positioned by their center, so they stay centered. --- barcode/writer.py | 9 +++++++-- docs/changelog.rst | 2 ++ tests/test_writers.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/barcode/writer.py b/barcode/writer.py index ca8a02d..f7dd282 100644 --- a/barcode/writer.py +++ b/barcode/writer.py @@ -140,6 +140,7 @@ def __init__( self.text_distance = 5 self.text_line_distance = 1 self.center_text = True + self._left_align_text = False self.guard_height_factor = 1.1 self.margin_top = 1 self.margin_bottom = 1 @@ -275,6 +276,8 @@ def render(self, code: list[str]) -> bytes | T_Image: ypos += self.module_height if self.text and self._callbacks["paint_text"] is not None: + # Text blocks between guards are always positioned by their center. + self._left_align_text = not (self.center_text or text["start"]) if not text["start"]: # If we don't have any start value, print the entire ean ypos += self.text_distance @@ -378,6 +381,7 @@ def _create_text(self, xpos: float, ypos: float) -> None: # check option to override self.text with self.human (barcode as # human readable data, can be used to print own formats) barcodetext = self.human if self.human != "" else self.text + anchor = "start" if self._left_align_text else "middle" for subtext in barcodetext.split("\n"): element = self._document.createElement("text") attributes = { @@ -385,7 +389,7 @@ def _create_text(self, xpos: float, ypos: float) -> None: "y": SIZE.format(ypos), "style": ( f"fill:{self.foreground};" - f"font-size:{self.font_size}pt;text-anchor:middle;" + f"font-size:{self.font_size}pt;text-anchor:{anchor};" ), } _set_attributes(element, **attributes) @@ -494,13 +498,14 @@ def _paint_text(self, xpos: float, ypos: float) -> None: if font_size <= 0: return font = ImageFont.truetype(self.font_path, font_size) + anchor = "ld" if self._left_align_text else "md" for subtext in barcodetext.split("\n"): pos = ( mm2px(xpos, self.dpi), mm2px(ypos, self.dpi), ) self._draw.text( - pos, subtext, font=font, fill=self.foreground, anchor="md" + pos, subtext, font=font, fill=self.foreground, anchor=anchor ) ypos += pt2mm(self.font_size) / 2 + self.text_line_distance diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a15b52..223efd7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,8 @@ v0.17.0 barcodes. (#251) * Fix ISBN-10 and ISSN barcodes being truncated to 10 and 8 digits. * Fix Code128 emitting different (but valid) output depending on invocation. +* Fix ``center_text=False`` centering the text on the left edge of the barcode + instead of left aligning it, which cut off the text. (#145) v0.16.1 ~~~~~~~ diff --git a/tests/test_writers.py b/tests/test_writers.py index 27454bb..4dd73bf 100644 --- a/tests/test_writers.py +++ b/tests/test_writers.py @@ -2,15 +2,19 @@ import os from io import BytesIO +from xml.dom.minidom import Element +from xml.dom.minidom import parseString from barcode import EAN13 from barcode.writer import ImageWriter from barcode.writer import SVGWriter +from barcode.writer import mm2px PATH = os.path.dirname(os.path.abspath(__file__)) TESTPATH = os.path.join(PATH, "test_outputs") if ImageWriter is not None: + from PIL import ImageOps def test_saving_image_to_byteio() -> None: assert ImageWriter is not None # workaround for mypy @@ -34,6 +38,20 @@ def test_saving_rgba_image() -> None: f, options={"background": "rgba(255,0,0,0)"} ) + def test_image_center_text_false_left_aligns_text() -> None: + assert ImageWriter is not None # workaround for mypy + + writer = ImageWriter() + image = EAN13("100000902922", writer=writer).render( + {"center_text": False, "quiet_zone": 6.5, "module_height": 15.0} + ) + assert not isinstance(image, bytes) # workaround for mypy + + below_bars = (0, int(mm2px(17, writer.dpi)), image.width, image.height) + text_box = ImageOps.invert(image.convert("L")).crop(below_bars).getbbox() + assert text_box is not None + assert text_box[0] >= int(mm2px(6.5, writer.dpi)) + def test_saving_svg_to_byteio() -> None: rv = BytesIO() @@ -49,3 +67,31 @@ def test_saving_svg_to_byteio_with_guardbar() -> None: with open(f"{TESTPATH}/somefile_guardbar.svg", "wb") as f: EAN13("100000011111", writer=SVGWriter(), guardbar=True).write(f) + + +def _svg_texts(barcode: EAN13, **options: object) -> list[Element]: + rv = BytesIO() + barcode.write(rv, options=options) + return parseString(rv.getvalue()).getElementsByTagName("text") + + +def test_svg_text_is_centered_by_default() -> None: + [text] = _svg_texts(EAN13("100000902922", writer=SVGWriter())) + + assert "text-anchor:middle;" in text.getAttribute("style") + + +def test_svg_center_text_false_left_aligns_text() -> None: + [text] = _svg_texts(EAN13("100000902922", writer=SVGWriter()), center_text=False) + + assert "text-anchor:start;" in text.getAttribute("style") + assert text.getAttribute("x") == "6.500mm" + + +def test_svg_center_text_false_keeps_guardbar_blocks_centered() -> None: + texts = _svg_texts( + EAN13("100000902922", writer=SVGWriter(), guardbar=True), center_text=False + ) + + assert len(texts) > 1 + assert all("text-anchor:middle;" in text.getAttribute("style") for text in texts)