From b648cfd67f930337d878690375f30cdb1aa6e49e Mon Sep 17 00:00:00 2001
From: Romain Porte <microjoe@microjoe.org>
Date: Thu, 31 Aug 2017 09:25:35 +0200
Subject: [PATCH] First attempt at centering images and QRs (#250)

This was tested on ZJ-5890 with success. By default centering is
deactivated for backward compatibility. Trying to center a QR code in
native mode will raise an exception as we do not know ATM if the native
rendering is centered by default or not.

* Added basic tests for center feature

* Check image size before centering
---
 examples/qr_code.py                 |  2 +-
 src/escpos/escpos.py                | 18 +++++++++++++++---
 src/escpos/image.py                 | 16 ++++++++++++++++
 test/test_function_image.py         | 25 ++++++++++++++++++++-----
 test/test_function_qr_native.py     | 13 ++++++++++++-
 test/test_function_qr_non-native.py | 10 ++++++++++
 6 files changed, 74 insertions(+), 10 deletions(-)

diff --git a/examples/qr_code.py b/examples/qr_code.py
index 6db8b68..325f845 100644
--- a/examples/qr_code.py
+++ b/examples/qr_code.py
@@ -16,4 +16,4 @@ if __name__ == '__main__':
 
     # Adapt to your needs
     p = Usb(0x0416, 0x5011, profile="POS-5890")
-    p.qr(content)
+    p.qr(content, center=True)
diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py
index 04c1d80..c94125c 100644
--- a/src/escpos/escpos.py
+++ b/src/escpos/escpos.py
@@ -87,7 +87,7 @@ class Escpos(object):
         raise NotImplementedError()
 
     def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster",
-              fragment_height=960):
+              fragment_height=960, center=False):
         """ Print an image
 
         You can select whether the printer should print in high density or not. The default value is high density.
@@ -108,14 +108,19 @@ class Escpos(object):
         :param high_density_horizontal: print in high density in horizontal direction *default:* True
         :param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn`
         :param fragment_height: Images larger than this will be split into multiple fragments *default:* 960
+        :param center: Center image horizontally *default:* False
 
         """
         im = EscposImage(img_source)
 
         try:
             max_width = int(self.profile.profile_data['media']['width']['pixels'])
+
             if im.width > max_width:
                 raise ImageWidthError('{} > {}'.format(im.width, max_width))
+
+            if center:
+                im.center(max_width)
         except KeyError:
             # If the printer's pixel width is not known, print anyways...
             pass
@@ -173,7 +178,8 @@ class Escpos(object):
         header = self._int_low_high(len(data) + 2, 2)
         self._raw(GS + b'(L' + header + m + fn + data)
 
-    def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False):
+    def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2,
+           native=False, center=False):
         """ Print QR Code for the provided string
 
         :param content: The content of the code. Numeric data will be more efficiently compacted.
@@ -185,6 +191,7 @@ class Escpos(object):
             by all printers).
         :param native: True to render the code on the printer, False to render the code as an image and send it to the
             printer (Default)
+        :param center: Centers the code *default:* False
         """
         # Basic validation
         if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]:
@@ -211,12 +218,17 @@ class Escpos(object):
             qr_code.make(fit=True)
             qr_img = qr_code.make_image()
             im = qr_img._img.convert("RGB")
+
             # Convert the RGB image in printable image
             self.text('\n')
-            self.image(im)
+            self.image(im, center=center)
             self.text('\n')
             self.text('\n')
             return
+
+        if center:
+            raise NotImplementedError("Centering not implemented for native QR rendering")
+
         # Native 2D code printing
         cn = b'1'  # Code type for QR code
         # Select model: 1, 2 or micro.
diff --git a/src/escpos/image.py b/src/escpos/image.py
index a5b15ab..76b75fd 100644
--- a/src/escpos/image.py
+++ b/src/escpos/image.py
@@ -115,3 +115,19 @@ class EscposImage(object):
             box = (left, upper, right, lower)
             fragments.append(self.img_original.crop(box))
         return fragments
+
+    def center(self, max_width):
+        """In-place image centering
+
+        :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
diff --git a/test/test_function_image.py b/test/test_function_image.py
index 5aac41b..5f66612 100644
--- a/test/test_function_image.py
+++ b/test/test_function_image.py
@@ -145,10 +145,8 @@ def test_large_graphics():
     assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\xc0\x1dv0\x00\x01\x00\x01\x00\x00')
 
 
-def test_width_too_large():
-    """
-    Test printing an image that is too large in width.
-    """
+@pytest.fixture
+def dummy_with_width():
     instance = printer.Dummy()
     instance.profile.profile_data = {
         'media': {
@@ -157,8 +155,25 @@ def test_width_too_large():
             }
         }
     }
+    return instance
+
+
+def test_width_too_large(dummy_with_width):
+    """
+    Test printing an image that is too large in width.
+    """
+    instance = dummy_with_width
 
     with pytest.raises(ImageWidthError):
         instance.image(Image.new("RGB", (385, 200)))
 
-    instance.image(Image.new("RGB", (384, 200)))
\ No newline at end of file
+    instance.image(Image.new("RGB", (384, 200)))
+
+
+def test_center_image(dummy_with_width):
+    instance = dummy_with_width
+
+    with pytest.raises(ImageWidthError):
+        instance.image(Image.new("RGB", (385, 200)), center=True)
+
+    instance.image(Image.new("RGB", (384, 200)), center=True)
diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py
index 4aa9c1d..e15e45d 100644
--- a/test/test_function_qr_native.py
+++ b/test/test_function_qr_native.py
@@ -13,6 +13,8 @@ from __future__ import print_function
 from __future__ import unicode_literals
 
 from nose.tools import raises
+import pytest
+
 import escpos.printer as printer
 from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
 
@@ -25,7 +27,6 @@ def test_defaults():
         b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
     assert(instance.output == expected)
 
-
 def test_empty():
     """Test QR printing blank code"""
     instance = printer.Dummy()
@@ -99,3 +100,13 @@ def test_image_invalid_model():
     """Test unsupported QR model as image"""
     instance = printer.Dummy()
     instance.qr("1234", native=False, model=QR_MODEL_1)
+
+
+@pytest.fixture
+def instance():
+    return printer.Dummy()
+
+
+def test_center_not_implementer(instance):
+    with pytest.raises(NotImplementedError):
+        instance.qr("test", center=True, native=True)
\ No newline at end of file
diff --git a/test/test_function_qr_non-native.py b/test/test_function_qr_non-native.py
index bba47fd..a420b2b 100644
--- a/test/test_function_qr_non-native.py
+++ b/test/test_function_qr_non-native.py
@@ -13,6 +13,7 @@ from __future__ import division
 from __future__ import print_function
 from __future__ import unicode_literals
 
+import pytest
 import mock
 
 from escpos.printer import Dummy
@@ -30,3 +31,12 @@ def test_type_of_object_passed_to_image_function(img_function):
     d.qr("LoremIpsum")
     args, kwargs = img_function.call_args
     assert isinstance(args[0], Image.Image)
+
+
+@pytest.fixture
+def instance():
+    return Dummy()
+
+
+def test_center(instance):
+    instance.qr("LoremIpsum", center=True)