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)