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)