# -*- coding: utf-8 -*-
import os
import gzip
import logging
import xml.dom
try:
import Image
import ImageDraw
import ImageFont
except ImportError:
try:
from PIL import Image, ImageDraw, ImageFont # lint:ok
except ImportError:
__logger = logging.getLogger(__package__)
__logger.info('PIL not found. Image output disabled.')
Image = ImageDraw = ImageFont = None # lint:ok
from abc import ABCMeta, abstractmethod
from steenzout.object import Object
from steenzout.barcode import metadata
SIZE = '{0:.3f}mm'
COMMENT = 'Autogenerated with %s %s' % (__package__, metadata.__release__)
PATH = os.path.dirname(os.path.abspath(__file__))
FONT = os.path.join(PATH, 'fonts/DejaVuSansMono.ttf')
[docs]def mm2px(value, dpi=300):
"""Converts the given value in millimeters into dots per inch.
Args:
value(float): value, in millimeters.
dpi (int): resolution, in dots per inch.
Returns:
(float): value, in dots per inch.
"""
return (value * dpi) / 25.4
[docs]def pt2mm(value):
"""Converts given value in points to millimeters.
Args:
value (int): value in points.
Returns:
(float): value, in millimeters.
"""
return value * 0.352777778
def _set_attributes(element, **attributes):
for key, value in attributes.items():
element.setAttribute(key, value)
[docs]def create_svg_object():
"""Returns a blank SVG document.
Returns:
(:py:class:`xml.dom.minidom.DocumentType`): XML document.
"""
imp = xml.dom.getDOMImplementation()
doctype = imp.createDocumentType(
'svg',
'-//W3C//DTD SVG 1.1//EN',
'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
)
document = imp.createDocument(None, 'svg', doctype)
_set_attributes(document.documentElement, version='1.1', xmlns='http://www.w3.org/2000/svg')
return document
[docs]class Interface:
"""Writer interface."""
__metaclass__ = ABCMeta
@classmethod
def __subclasshook__(cls, clazz):
if cls is Interface:
if any('save' in C.__dict__ for C in clazz.__mro__):
return True
return NotImplemented
@abstractmethod
[docs] def save(self, filename, content):
"""Saves contents to `filename`.
Args:
filename (str): filename without extension.
content (str): output of rendering process.
Returns:
(str): the filename.
"""
return None
[docs]class Base(Object, Interface):
"""Base class for all writers.
Initializes the basic writer options.
Sub-classes can add more attributes and
can set them directly or using `self.set_options(option=value)`.
Args:
initialize (function):
Callback for initializing the inheriting writer.
Is called: `callback_initialize(raw_code)`
paint_module (function):
Callback for painting one barcode module.
Is called: `callback_paint_module(xpos, ypos, width, color)`
paint_text (function):
Callback for painting the text under the barcode.
Is called: `callback_paint_text(xpos, ypos)` using `self.text`
as text.
finish (function):
Callback for doing something with the completely rendered
output.
Is called: `return callback_finish()` and must return the
rendered output.
"""
def __init__(self, initialize=None, paint_module=None, paint_text=None,
finish=None):
self._callbacks = dict(
initialize=initialize,
paint_module=paint_module,
paint_text=paint_text,
finish=finish
)
self.module_width = 10
self.module_height = 10
self.font_size = 10
self.quiet_zone = 6.5
self.background = 'white'
self.foreground = 'black'
self.text = ''
self.text_distance = 5
self.center_text = True
[docs] def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
"""Calculates the size of the barcode in pixel.
Args:
modules_per_line (int): number of modules in one line.
number_of_lines (int): number of lines of the barcode.
dpi (int): DPI to calculate.
Returns:
(tuple[int, int]): Width and height of the barcode in pixel.
"""
width = 2 * self.quiet_zone + modules_per_line * self.module_width
height = 2.0 + self.module_height * number_of_lines
if self.font_size and self.text:
height += pt2mm(self.font_size) / 2 + self.text_distance
return int(mm2px(width, dpi)), int(mm2px(height, dpi))
@abstractmethod
[docs] def save(self, filename, output):
"""See :py:func:`Interface.save`."""
[docs] def register_callback(self, action, callback):
"""Register one of the three callbacks if not given at instance
creation.
Args:
action (str):
One of 'initialize', 'paint_module', 'paint_text', 'finish'.
callback (function):
The callback function for the given action.
"""
self._callbacks[action] = callback
[docs] def set_options(self, options):
"""Sets the given options as instance attributes (only if they are known).
Args:
options (dict):
All known instance attributes and more if the child class
has defined them before this call.
"""
for key, val in options.items():
key = key.lstrip('_')
if hasattr(self, key):
setattr(self, key, val)
[docs] def render(self, code):
"""Renders the barcode to whatever the inheriting writer provides,
using the registered callbacks.
Args:
code (list[str]): list of strings matching the writer spec (only contain 0 or 1).
"""
if self._callbacks['initialize'] is not None:
self._callbacks['initialize'](code)
ypos = 1.0
for line in code:
# Left quiet zone is x startposition
xpos = self.quiet_zone
for mod in line:
if mod == '0':
color = self.background
else:
color = self.foreground
self._callbacks['paint_module'](xpos, ypos, self.module_width,
color)
xpos += self.module_width
# Add right quiet zone to every line
self._callbacks['paint_module'](xpos, ypos, self.quiet_zone,
self.background)
ypos += self.module_height
if self.text and self._callbacks['paint_text'] is not None:
ypos += self.text_distance
if self.center_text:
xpos = xpos / 2.0
else:
xpos = self.quiet_zone + 4.0
self._callbacks['paint_text'](xpos, ypos)
return self._callbacks['finish']()
[docs]class SVG(Base):
def __init__(self):
Base.__init__(self, self._init, self._create_module,
self._create_text, self._finish)
self.compress = False
self.dpi = 25.4
self._document = None
self._root = None
def _init(self, code):
width, height = self.calculate_size(len(code[0]), len(code), self.dpi)
self._document = create_svg_object()
self._root = self._document.documentElement
attributes = dict(width=SIZE.format(width), height=SIZE.format(height))
_set_attributes(self._root, **attributes)
self._root.appendChild(self._document.createComment(COMMENT))
background = self._document.createElement('rect')
attributes = dict(width='100%', height='100%',
style='fill:{0}'.format(self.background))
_set_attributes(background, **attributes)
self._root.appendChild(background)
def _create_module(self, xpos, ypos, width, color):
element = self._document.createElement('rect')
attributes = dict(x=SIZE.format(xpos), y=SIZE.format(ypos),
width=SIZE.format(width),
height=SIZE.format(self.module_height),
style='fill:{0};'.format(color))
_set_attributes(element, **attributes)
self._root.appendChild(element)
def _create_text(self, xpos, ypos):
element = self._document.createElement('text')
attributes = dict(x=SIZE.format(xpos), y=SIZE.format(ypos),
style='fill:{0};font-size:{1}pt;text-anchor:'
'middle;'.format(self.foreground,
self.font_size))
_set_attributes(element, **attributes)
text_element = self._document.createTextNode(self.text)
element.appendChild(text_element)
self._root.appendChild(element)
def _finish(self):
if self.compress:
return self._document.toxml(encoding='UTF-8')
else:
return self._document.toprettyxml(indent=4 * ' ', newl=os.linesep,
encoding='UTF-8')
[docs] def save(self, filename, content):
"""See :py:func:`Interface.save`."""
if self.compress:
_filename = '%s.svgz' % filename
with gzip.open('%s.svgz' % filename, 'wb') as output:
output.write(content)
else:
_filename = '%s.svg' % filename
with open(_filename, 'wb') as output:
output.write(content)
return _filename
if Image is None:
ImageWriter = None
else:
[docs] class ImageWriter(Base):
def __init__(self):
Base.__init__(self, self._init, self._paint_module,
self._paint_text, self._finish)
self.format = 'PNG'
self.dpi = 300
self._image = None
self._draw = None
def _init(self, code):
size = self.calculate_size(len(code[0]), len(code), self.dpi)
self._image = Image.new('RGB', size, self.background)
self._draw = ImageDraw.Draw(self._image)
def _paint_module(self, xpos, ypos, width, color):
size = [(mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)),
(mm2px(xpos + width, self.dpi),
mm2px(ypos + self.module_height, self.dpi))]
self._draw.rectangle(size, outline=color, fill=color)
def _paint_text(self, xpos, ypos):
font = ImageFont.truetype(FONT, self.font_size * 2)
width, height = font.getsize(self.text)
pos = (mm2px(xpos, self.dpi) - width // 2,
mm2px(ypos, self.dpi) - height // 4)
self._draw.text(pos, self.text, font=font, fill=self.foreground)
def _finish(self):
return self._image
[docs] def save(self, filename, output):
"""See :py:func:`Interface.save`."""
filename = '{0}.{1}'.format(filename, self.format.lower())
output.save(filename, self.format.upper())
return filename
DEFAULT_WRITER = SVG
DEFAULT_WRITER_OPTIONS = {
'module_width': 0.2,
'module_height': 15.0,
'quiet_zone': 6.5,
'font_size': 10,
'text_distance': 5.0,
'background': 'white',
'foreground': 'black',
'write_text': True,
'text': '',
}