zaz-barcode/source/pythonpath/barcode/writer.py

398 lines
13 KiB
Python
Executable File

import gzip
import os
import xml.dom
from typing import BinaryIO
from barcode.version import version
try:
import Image
import ImageDraw
import ImageFont
except ImportError:
try:
from PIL import Image, ImageDraw, ImageFont # lint:ok
except ImportError:
import logging
log = logging.getLogger("pyBarcode")
log.info("Pillow not found. Image output disabled")
Image = ImageDraw = ImageFont = None # lint:ok
def mm2px(mm, dpi=300):
return (mm * dpi) / 25.4
def pt2mm(pt):
return pt * 0.352777778
def _set_attributes(element, **attributes):
for key, value in attributes.items():
element.setAttribute(key, value)
def create_svg_object(with_doctype=False):
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 if with_doctype else None)
_set_attributes(
document.documentElement, version="1.1", xmlns="http://www.w3.org/2000/svg"
)
return document
SIZE = "{0:.3f}mm"
COMMENT = "Autogenerated with python-barcode {}".format(version)
PATH = os.path.dirname(os.path.abspath(__file__))
class BaseWriter:
"""Baseclass for all writers.
Initializes the basic writer options. Childclasses can add more
attributes and can set them directly or using
`self.set_options(option=value)`.
:parameters:
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 = {
"initialize": initialize,
"paint_module": paint_module,
"paint_text": paint_text,
"finish": finish,
}
self.module_width = 10
self.module_height = 10
self.font_path = os.path.join(PATH, "fonts", "DejaVuSansMono.ttf")
self.font_size = 10
self.quiet_zone = 6.5
self.background = "white"
self.foreground = "black"
self.text = ""
self.human = "" # human readable text
self.text_distance = 5
self.text_line_distance = 1
self.center_text = True
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
"""Calculates the size of the barcode in pixel.
:parameters:
modules_per_line : Integer
Number of modules in one line.
number_of_lines : Integer
Number of lines of the barcode.
dpi : Integer
DPI to calculate.
:returns: Width and height of the barcode in pixel.
:rtype: Tuple
"""
width = 2 * self.quiet_zone + modules_per_line * self.module_width
height = 2.0 + self.module_height * number_of_lines
number_of_text_lines = len(self.text.splitlines())
if self.font_size and self.text:
height += (
pt2mm(self.font_size) / 2 * number_of_text_lines + self.text_distance
)
height += self.text_line_distance * (number_of_text_lines - 1)
return int(mm2px(width, dpi)), int(mm2px(height, dpi))
def save(self, filename, output):
"""Saves the rendered output to `filename`.
:parameters:
filename : String
Filename without extension.
output : String
The rendered output.
:returns: The full filename with extension.
:rtype: String
"""
raise NotImplementedError
def register_callback(self, action, callback):
"""Register one of the three callbacks if not given at instance
creation.
:parameters:
action : String
One of 'initialize', 'paint_module', 'paint_text', 'finish'.
callback : Function
The callback function for the given action.
"""
self._callbacks[action] = callback
def set_options(self, options):
"""Sets the given options as instance attributes (only
if they are known).
:parameters:
options : Dict
All known instance attributes and more if the childclass
has defined them before this call.
:rtype: None
"""
for key, val in options.items():
key = key.lstrip("_")
if hasattr(self, key):
setattr(self, key, val)
def render(self, code):
"""Renders the barcode to whatever the inheriting writer provides,
using the registered callbacks.
:parameters:
code : List
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 cc, line in enumerate(code):
"""
Pack line to list give better gfx result, otherwise in can
result in aliasing gaps
'11010111' -> [2, -1, 1, -1, 3]
"""
line += " "
c = 1
mlist = []
for i in range(0, len(line) - 1):
if line[i] == line[i + 1]:
c += 1
else:
if line[i] == "1":
mlist.append(c)
else:
mlist.append(-c)
c = 1
# Left quiet zone is x startposition
xpos = self.quiet_zone
bxs = xpos # x start of barcode
for mod in mlist:
if mod < 1:
color = self.background
else:
color = self.foreground
# remove painting for background colored tiles?
self._callbacks["paint_module"](
xpos, ypos, self.module_width * abs(mod), color
)
xpos += self.module_width * abs(mod)
bxe = xpos
# Add right quiet zone to every line, except last line,
# quiet zone already provided with background,
# should it be removed completely?
if (cc + 1) != len(code):
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:
# better center position for text
xpos = bxs + ((bxe - bxs) / 2.0)
else:
xpos = bxs
self._callbacks["paint_text"](xpos, ypos)
return self._callbacks["finish"]()
class SVGWriter(BaseWriter):
def __init__(self):
BaseWriter.__init__(
self, self._init, self._create_module, self._create_text, self._finish
)
self.compress = False
self.dpi = 25.4
self.with_doctype = True
self._document = None
self._root = None
self._group = None
def _init(self, code):
width, height = self.calculate_size(len(code[0]), len(code), self.dpi)
self._document = create_svg_object(self.with_doctype)
self._root = self._document.documentElement
attributes = {
"width": SIZE.format(width),
"height": SIZE.format(height),
}
_set_attributes(self._root, **attributes)
self._root.appendChild(self._document.createComment(COMMENT))
# create group for easier handling in 3rd party software
# like corel draw, inkscape, ...
group = self._document.createElement("g")
attributes = {"id": "barcode_group"}
_set_attributes(group, **attributes)
self._group = self._root.appendChild(group)
background = self._document.createElement("rect")
attributes = {
"width": "100%",
"height": "100%",
"style": "fill:{}".format(self.background),
}
_set_attributes(background, **attributes)
self._group.appendChild(background)
def _create_module(self, xpos, ypos, width, color):
element = self._document.createElement("rect")
attributes = {
"x": SIZE.format(xpos),
"y": SIZE.format(ypos),
"width": SIZE.format(width),
"height": SIZE.format(self.module_height),
"style": "fill:{};".format(color),
}
_set_attributes(element, **attributes)
self._group.appendChild(element)
def _create_text(self, xpos, ypos):
# check option to override self.text with self.human (barcode as
# human readable data, can be used to print own formats)
if self.human != "":
barcodetext = self.human
else:
barcodetext = self.text
for subtext in barcodetext.split("\n"):
element = self._document.createElement("text")
attributes = {
"x": SIZE.format(xpos),
"y": SIZE.format(ypos),
"style": "fill:{};font-size:{}pt;text-anchor:middle;".format(
self.foreground,
self.font_size,
),
}
_set_attributes(element, **attributes)
text_element = self._document.createTextNode(subtext)
element.appendChild(text_element)
self._group.appendChild(element)
ypos += pt2mm(self.font_size) + self.text_line_distance
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"
)
def save(self, filename, output):
if self.compress:
_filename = "{}.svgz".format(filename)
f = gzip.open(_filename, "wb")
f.write(output)
f.close()
else:
_filename = "{}.svg".format(filename)
with open(_filename, "wb") as f:
f.write(output)
return _filename
def write(self, content, fp: BinaryIO):
"""Write `content` into a file-like object.
Content should be a barcode rendered by this writer.
"""
fp.write(content)
if Image is None:
ImageWriter = None
else:
class ImageWriter(BaseWriter): # type: ignore
format: str
mode: str
dpi: int
def __init__(self, format="PNG", mode="RGB"):
"""Initialise a new write instance.
:params format: The file format for the generated image. This parameter can
take any value that Pillow accepts.
:params mode: The colour-mode for the generated image. Set this to RGBA if
you wish to use colours with transparency.
"""
BaseWriter.__init__(
self, self._init, self._paint_module, self._paint_text, self._finish
)
self.format = format
self.mode = mode
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(self.mode, 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(self.font_path, self.font_size * 2)
for subtext in self.text.split("\n"):
width, height = font.getsize(subtext)
# determine the maximum width of each line
pos = (
mm2px(xpos, self.dpi) - width // 2,
mm2px(ypos, self.dpi) - height // 4,
)
self._draw.text(pos, subtext, font=font, fill=self.foreground)
ypos += pt2mm(self.font_size) / 2 + self.text_line_distance
def _finish(self):
return self._image
def save(self, filename, output):
filename = "{}.{}".format(filename, self.format.lower())
output.save(filename, self.format.upper())
return filename
def write(self, content, fp: BinaryIO):
"""Write `content` into a file-like object.
Content should be a barcode rendered by this writer.
"""
content.save(fp, format=self.format)