1
0
mirror of https://github.com/python-escpos/python-escpos synced 2025-09-13 09:09:58 +00:00
Files
python-escpos/src/escpos/image.py
Alexandre Detiste 66a2e78e16 start removal of six and improve type annotation (#607)
* fix unfinished Python2 -> 3 translation
* remove some six usage
* annotate
* fix regression in Six removal
* mypy: self.enf is always defined
* fix return type of cups.py
* Usb idVendor/idProduct are integers
* self.default_args is always defined
* tweak usb_args, PEP589 is better
* lp.py: reassure mypy
* correctly cast call to CashDrawerError()
* update CUPS test
* add missing close() method in metaclass
* document a bug in typeshed
* query_status() returns bytes as seen in constants.py
* remove more SIX usage
* test examples too
* remove type comment where type is annotated
* adapt behavior of cups printer to match other implementations

---------

Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2023-12-16 22:02:24 +01:00

121 lines
3.9 KiB
Python

"""Image format handling class.
This module contains the image format handler :py:class:`EscposImage`.
:author: `Michael Billington <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 Michael Billington <michael.billington@gmail.com>
:license: MIT
"""
import math
from typing import Iterator, Union
from PIL import Image, ImageOps
class EscposImage:
"""
Load images in, and output ESC/POS formats.
The class is designed to efficiently delegate image processing to
PIL, rather than spend CPU cycles looping over pixels.
"""
def __init__(self, img_source: Union[Image.Image, str]) -> None:
"""Load in an image.
:param img_source: PIL.Image, or filename to load one from.
"""
if isinstance(img_source, Image.Image):
img_original = img_source
else:
img_original = Image.open(img_source)
# store image for eventual further processing (splitting)
self.img_original = img_original
# Convert to white RGB background, paste over white background
# to strip alpha.
img_original = img_original.convert("RGBA")
im = Image.new("RGB", img_original.size, (255, 255, 255))
im.paste(img_original, mask=img_original.split()[3])
# Convert down to greyscale
im = im.convert("L")
# Invert: Only works on 'L' images
im = ImageOps.invert(im)
# Pure black and white
self._im = im.convert("1")
@property
def width(self) -> int:
"""Return width of image in pixels."""
width_pixels, _ = self._im.size
return width_pixels
@property
def width_bytes(self) -> int:
"""Return width of image if you use 8 pixels per byte and 0-pad at the end."""
return (self.width + 7) >> 3
@property
def height(self) -> int:
"""Height of image in pixels."""
_, height_pixels = self._im.size
return height_pixels
def to_column_format(self, high_density_vertical: bool = True) -> Iterator[bytes]:
"""Extract slices of an image as equal-sized blobs of column-format data.
:param high_density_vertical: Printed line height in dots
"""
im = self._im.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT)
line_height = 24 if high_density_vertical else 8
width_pixels, height_pixels = im.size
top = 0
left = 0
while left < width_pixels:
box = (left, top, left + line_height, top + height_pixels)
im_slice = im.transform((line_height, height_pixels), Image.EXTENT, box)
im_bytes = im_slice.tobytes()
yield (im_bytes)
left += line_height
def to_raster_format(self) -> bytes:
"""Convert image to raster-format binary."""
return self._im.tobytes()
def split(self, fragment_height: int):
"""Split an image into multiple fragments after fragment_height pixels.
:param fragment_height: height of fragment
:return: list of PIL objects
"""
passes = int(math.ceil(self.height / fragment_height))
fragments = []
for n in range(0, passes):
left = 0
right = self.width
upper = n * fragment_height
lower = min((n + 1) * fragment_height, self.height)
box = (left, upper, right, lower)
fragments.append(self.img_original.crop(box))
return fragments
def center(self, max_width: int) -> None:
"""Center image in place.
:param: Maximum width in order to deduce x offset for centering
:return: None
"""
old_width, height = self._im.size
new_size = (max_width, height)
new_im = Image.new("1", new_size)
paste_x = int((max_width - old_width) / 2)
new_im.paste(self._im, (paste_x, 0))
self._im = new_im