Source code for zebrafy.zebrafy_image

########################################################################################
#
#    Author: Miika Nissi
#    Copyright 2023-2023 Miika Nissi (https://miikanissi.com)
#
#    This file is part of zebrafy
#    (see https://github.com/miikanissi/zebrafy).
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with this program. If not, see <http://www.gnu.org/licenses/>.
#
########################################################################################

# 1. Standard library imports:
import operator
from io import BytesIO
from typing import Union

# 2. Known third party imports:
from PIL import Image

# 3. Local imports in the relative form:
from zebrafy.graphic_field import GraphicField


[docs] class ZebrafyImage: """ Convert a PIL Image or image bytes into Zebra Programming Language (ZPL). :param image: Image as a PIL Image or bytes object. :param compression_type (deprecated): ZPL compression type parameter that accepts \ the following values, defaults to ``"A"``: - ``"A"``: ASCII hexadecimal - most compatible (default) - ``"B"``: Base64 binary - ``"C"``: LZ77 / Zlib compressed base64 binary - best compression :param format: ZPL format parameter that accepts the following values, \ defaults to ``"ASCII"``: - ``"ASCII"``: ASCII hexadecimal - most compatible (default) - ``"B64"``: Base64 binary - ``"Z64"``: LZ77 / Zlib compressed base64 binary - best compression :param invert: Invert the black and white in resulting image, defaults to ``False`` :param dither: Dither the pixels instead of hard limit on black and white, \ defaults to ``True`` :param threshold: Black pixel threshold for undithered image (``0-255``), defaults \ to ``128`` :param width: Width of the image in the resulting ZPL. If ``0``, use default image \ width, defaults to ``0`` :param height: Height of the image in the resulting ZPL. If ``0``, use default \ image height, defaults to ``0`` :param pos_x: X position of the image on the resulting ZPL, defaults to ``0`` :param pos_y: Y position of the image on the resulting ZPL, defaults to ``0`` :param rotation: Additional rotation in degrees ``0``, ``90``, ``180``, or \ ``270``, defaults to ``0`` :param string_line_break: Number of characters in graphic field content after \ which a new line is added, defaults to `None`. :param complete_zpl: Return a complete ZPL with header and footer included. \ Otherwise return only the graphic field, defaults to ``True`` .. deprecated:: 1.1.0 The `compression_type` parameter is deprecated in favor of `format` and will \ be removed in version 2.0.0. """ def __init__( self, image: Union[bytes, Image.Image], compression_type: str = None, format: str = None, invert: bool = None, dither: bool = None, threshold: int = None, width: int = None, height: int = None, pos_x: int = None, pos_y: int = None, rotation: int = None, string_line_break: int = None, complete_zpl: bool = None, ): self.image = image if format is None: if compression_type is None: format = "ASCII" else: format = self._compression_type_to_format(compression_type) self.format = format.upper() if invert is None: invert = False self.invert = invert if dither is None: dither = True self.dither = dither if threshold is None: threshold = 128 self.threshold = threshold if width is None: width = 0 self.width = width if height is None: height = 0 self.height = height if pos_x is None: pos_x = 0 self.pos_x = pos_x if pos_y is None: pos_y = 0 self.pos_y = pos_y if rotation is None: rotation = 0 self.rotation = rotation self.string_line_break = string_line_break if complete_zpl is None: complete_zpl = True self.complete_zpl = complete_zpl image = property(operator.attrgetter("_image")) @image.setter def image(self, i): if not i: raise ValueError("Image cannot be empty.") if not isinstance(i, bytes) and not isinstance(i, Image.Image): raise TypeError( "Image must be a valid bytes object or PIL.Image.Image object." f" {type(i)} was given." ) self._image = i format = property(operator.attrgetter("_format")) @format.setter def format(self, f): if f is None: raise ValueError("Format cannot be empty.") if not isinstance(f, str): raise TypeError(f"Format must be a valid string. {type(f)} was given.") if f not in ["ASCII", "B64", "Z64"]: raise ValueError(f'Format must be "ASCII","B64", or "Z64". {f} was given.') self._format = f invert = property(operator.attrgetter("_invert")) @invert.setter def invert(self, i): if i is None: raise ValueError("Invert cannot be empty.") if not isinstance(i, bool): raise TypeError(f"Invert must be a boolean. {type(i)} was given.") self._invert = i dither = property(operator.attrgetter("_dither")) @dither.setter def dither(self, d): if d is None: raise ValueError("Dither cannot be empty.") if not isinstance(d, bool): raise TypeError(f"Dither must be a boolean. {type(d)} was given.") self._dither = d threshold = property(operator.attrgetter("_threshold")) @threshold.setter def threshold(self, t): if t is None: raise ValueError("Threshold cannot be empty.") if not isinstance(t, int): raise TypeError(f"Threshold must be an integer. {type(t)} was given.") if t < 0 or t > 255: raise ValueError(f"Threshold must be within 0 to 255. {t} was given.") self._threshold = t width = property(operator.attrgetter("_width")) @width.setter def width(self, w): if w is None: raise ValueError("Width cannot be empty.") if not isinstance(w, int): raise TypeError(f"Width must be an integer. {type(w)} was given.") self._width = w height = property(operator.attrgetter("_height")) @height.setter def height(self, h): if h is None: raise ValueError("Height cannot be empty.") if not isinstance(h, int): raise TypeError(f"Height must be an integer. {type(h)} was given.") self._height = h pos_x = property(operator.attrgetter("_pos_x")) @pos_x.setter def pos_x(self, x): if x is None: raise ValueError("X position cannot be empty.") if not isinstance(x, int): raise TypeError(f"X position must be an integer. {type(x)} was given.") self._pos_x = x pos_y = property(operator.attrgetter("_pos_y")) @pos_y.setter def pos_y(self, y): if y is None: raise ValueError("Y position cannot be empty.") if not isinstance(y, int): raise TypeError(f"Y position must be an integer. {type(y)} was given.") self._pos_y = y rotation = property(operator.attrgetter("_rotation")) @rotation.setter def rotation(self, r): if r is None: raise ValueError("Rotation cannot be empty.") if not isinstance(r, int): raise TypeError(f"Rotation must be an integer. {type(r)} was given.") if r not in [0, 90, 180, 270]: raise ValueError( f'Rotation must be "0", "90", "180" or "270". {r} was given.' ) self._rotation = r string_line_break = property(operator.attrgetter("_string_line_break")) @string_line_break.setter def string_line_break(self, s): if s and not isinstance(s, int): raise TypeError( f"String line break must be a valid integer. {type(s)} was given." ) if s and s < 1: raise ValueError("String line break must be greater than 0.") self._string_line_break = s complete_zpl = property(operator.attrgetter("_complete_zpl")) @complete_zpl.setter def complete_zpl(self, c): if c is None: raise ValueError("Complete ZPL cannot be empty.") if not isinstance(c, bool): raise TypeError(f"Complete ZPL must be a boolean. {type(c)} was given.") self._complete_zpl = c def _compression_type_to_format(self, compression_type: str) -> str: """Convert deprecated compression type to format.""" if compression_type.upper() == "A": return "ASCII" elif compression_type.upper() == "B": return "B64" elif compression_type.upper() == "C": return "Z64"
[docs] def to_zpl(self) -> str: """ Convert PIL Image or image bytes into Zebra Programming Language (ZPL). :returns: A complete ZPL file string which can be sent to a ZPL compatible \ printer or a ZPL graphic field if complete_zpl is not set. """ if isinstance(self._image, bytes): pil_image = Image.open(BytesIO(self._image)) else: pil_image = self._image # Rotate image based on given parameters if self._rotation: pil_image = pil_image.rotate(self._rotation, expand=False) # Resize image if width or height defined in parameters if self._width or self._height: width, height = pil_image.size if self._width: width = self._width if self._height: height = self._height pil_image = pil_image.resize((width, height)) # Convert image to black and white based on given parameters if self._dither: pil_image = pil_image.convert("1") if self._invert: pil_image = pil_image.point(lambda x: 255 - x) else: pil_image = pil_image.convert("L") pil_image = pil_image.point( lambda x: ( (0 if self._invert else 255) if x > self._threshold else (255 if self._invert else 0) ), mode="1", ) graphic_field = GraphicField( pil_image, format=self._format, string_line_break=self._string_line_break ) # Set graphic field position based on given parameters pos = "^FO0,0" if self._pos_x or self._pos_y: pos = f"^FO{self._pos_x},{self._pos_y}" # Return complete ZPL with header and footer or only the graphic field based on # given parameters if self._complete_zpl: return "^XA\n" + pos + graphic_field.get_graphic_field() + "\n^XZ\n" return pos + graphic_field.get_graphic_field()