Source code for zebrafy.graphic_field

########################################################################################
#
#    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 base64
import operator
import zlib

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

# 3. Local imports in the relative form:
from zebrafy.crc import CRC


[docs] class GraphicField: """ Converts a PIL image to Zebra Programming Language (ZPL) graphic field data. :param PIL.Image.Image image: An instance of a PIL Image. :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 string_line_break: Number of characters in graphic field content after \ which a new line is added, defaults to `None`. .. 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, pil_image: Image, compression_type: str = None, format: str = None, string_line_break: int = None, ): self.pil_image = pil_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() self.string_line_break = string_line_break pil_image = property(operator.attrgetter("_pil_image")) @pil_image.setter def pil_image(self, i): if not i: raise ValueError("Image cannot be empty.") if not isinstance(i, Image): raise TypeError( f"Image must be a valid PIL.Image.Image object. {type(i)} was given." ) self._pil_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 type must be "ASCII","B64", or "Z64". {f} was given.' ) self._format = f 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 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" def _get_binary_byte_count(self) -> int: """ Get binary byte count. This is the total number of bytes to be transmitted for the total image or the total number of bytes that follow parameter bytes_per_row. For ASCII \ download, the parameter should match parameter graphic_field_count. \ Out-of-range values are set to the nearest limit. :returns: Binary byte count """ return len(self._get_data_string()) def _get_bytes_per_row(self) -> int: """ Get bytes per row. This is the number of bytes in the image data that comprise one row of the \ image. :returns: Bytes per row """ return int((self._pil_image.size[0] + 7) / 8) def _get_graphic_field_count(self) -> int: """ Get graphic field count. This is the total number of bytes comprising the image data (width x height). :returns: Graphic field count." """ return int(self._get_bytes_per_row() * self._pil_image.size[1]) def _get_data_string(self) -> str: """ Get graphic field data string depending on format. :returns: Graphic field data string depending on format. """ image_bytes = self._pil_image.tobytes() data_string = "" # Format ASCII: Convert bytes to ASCII hexadecimal if self._format == "ASCII": data_string = image_bytes.hex() # Format B64: Convert bytes to base64 and add header + CRC elif self._format == "B64": b64_bytes = base64.b64encode(image_bytes) data_string = ":B64:{encoded_data}:{crc}".format( encoded_data=b64_bytes.decode("ascii"), crc=CRC(b64_bytes).get_crc_hex_string(), ) # Format Z64: Convert LZ77/ Zlib compressed bytes to base64 and add # header + CRC elif self._format == "Z64": z64_bytes = base64.b64encode(zlib.compress(image_bytes)) data_string = ":Z64:{encoded_data}:{crc}".format( encoded_data=z64_bytes.decode("ascii"), crc=CRC(z64_bytes).get_crc_hex_string(), ) if self._string_line_break: data_string = "\n".join( data_string[i : i + self._string_line_break] for i in range(0, len(data_string), self._string_line_break) ) return data_string
[docs] def get_graphic_field(self) -> str: """ Get a complete graphic field string for ZPL. :returns: Complete graphic field string for ZPL. """ return ( f"^GFA,{self._get_binary_byte_count()},{self._get_graphic_field_count()}," f"{self._get_bytes_per_row()},{self._get_data_string()}^FS" )