From 923e847a5e02d33d17af01971bf1b89094fe4b1d Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Fri, 29 Jan 2021 11:04:33 +0100 Subject: [PATCH] Added Inventory Sticker Extension --- .../fablabchemnitz/inventory_sticker.inx | 45 ++ .../fablabchemnitz/inventory_sticker.py | 626 ++++++++++++++++++ 2 files changed, 671 insertions(+) create mode 100644 extensions/fablabchemnitz/inventory_sticker.inx create mode 100644 extensions/fablabchemnitz/inventory_sticker.py diff --git a/extensions/fablabchemnitz/inventory_sticker.inx b/extensions/fablabchemnitz/inventory_sticker.inx new file mode 100644 index 00000000..08b72c8b --- /dev/null +++ b/extensions/fablabchemnitz/inventory_sticker.inx @@ -0,0 +1,45 @@ + + + Inventory Sticker + fablabchemnitz.de.inventory_sticker + + + + https://things.fablabchemnitz.de/flc/inventory.csv + User + Password + + qwa.es + Stadtfabrikanten e.V. + + 1 + /home/ + false + true + false + 0 + 04f9:2044 + false + + + + + + + + + + + + + + + all + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/inventory_sticker.py b/extensions/fablabchemnitz/inventory_sticker.py new file mode 100644 index 00000000..c1150349 --- /dev/null +++ b/extensions/fablabchemnitz/inventory_sticker.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 +# +# An extension to generate SVG/PNG labels (stickers) for use with our item inventory system. +# It pulls a .csv file from a server URL (protected by basic auth) and exports and prints the labels to a Brother QL-720NW label printer +# Documentation: https://wiki.fablabchemnitz.de/display/TEED/Werkstattorientierung+im+FabLab+-+Digtales+Inventar +# +# Made by FabLab Chemnitz / Stadtfabrikanten e.V. - Developer: Mario Voigt (year 2021) +# +# This extension is based on the "original" barcode extension included in default InkScape Extension Set, which is licensed by the following: +# +# Copyright (C) 2009 John Beard john.j.beard@gmail.com +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import csv +import os +import shutil +import urllib.request +from lxml import etree +import inkex +from inkex import Rectangle +from inkex.command import inkscape +import re +import subprocess +from subprocess import Popen, PIPE + +INVALID_BIT = 2 + +# CODEWORD STREAM GENERATION ========================================= +# take the text input and return the codewords, +# including the Reed-Solomon error-correcting codes. +# ===================================================================== + +def get_codewords(text, nd, nc, inter, size144): + # convert the data to the codewords + data = list(encode_to_ascii(text)) + + if not size144: # render a "normal" datamatrix + data_blocks = partition_data(data, nd * inter) # partition into data blocks of length nd*inter -> inter Reed-Solomon block + data_blocks = interleave(data_blocks, inter) # interleave consecutive inter blocks if required + data_blocks = reed_solomon(data_blocks, nd, nc) # generate and append the Reed-Solomon codewords + data_blocks = combine_interleaved(data_blocks, inter, nd, nc, False) # concatenate Reed-Solomon blocks bound for the same datamatrix + return data_blocks + + +# Takes a codeword stream and splits up into "inter" blocks. +# eg interleave( [1,2,3,4,5,6], 2 ) -> [1,3,5], [2,4,6] +def interleave(blocks, inter): + if inter == 1: # if we don"t have to interleave, just return the blocks + return blocks + else: + result = [] + for block in blocks: # for each codeword block in the stream + block_length = int(len(block) / inter) # length of each interleaved block + inter_blocks = [[0] * block_length for i in range(inter)] # the interleaved blocks + + for i in range(block_length): # for each element in the interleaved blocks + for j in range(inter): # for each interleaved block + inter_blocks[j][i] = block[i * inter + j] + + result.extend(inter_blocks) # add the interleaved blocks to the output + + return result + +# Combine interleaved blocks into the groups for the same datamatrix +# +# e.g combine_interleaved( [[d1, d3, d5, e1, e3, e5], [d2, d4, d6, e2, e4, e6]], 2, 3, 3 ) +# --> [[d1, d2, d3, d4, d5, d6, e1, e2, e3, e4, e5, e6]] +def combine_interleaved(blocks, inter, nd, nc, size144): + if inter == 1: # the blocks aren"t interleaved + return blocks + else: + result = [] + for i in range(len(blocks) // inter): # for each group of "inter" blocks -> one full datamatrix + data_codewords = [] # interleaved data blocks + + if size144: + nd_range = 1558 # 1558 = 156*8 + 155*2 + nc_range = 620 # 620 = 62*8 + 62*2 + else: + nd_range = nd * inter + nc_range = nc * inter + + for j in range(nd_range): # for each codeword in the final list + data_codewords.append(blocks[i * inter + j % inter][j // inter]) + + for j in range(nc_range): # for each block, add the ecc codewords + data_codewords.append(blocks[i * inter + j % inter][nd + j // inter]) + + result.append(data_codewords) + return result + +def encode_to_ascii(text): + """Encode this text into chunks, ascii or digits""" + i = 0 + while i < len(text): + # check for double digits, if the next char is also a digit + if text[i].isdigit() and (i < len(text) - 1) and text[i + 1].isdigit(): + yield int(text[i] + text[i + 1]) + 130 + i += 2 # move on 2 characters + else: # encode as a normal ascii, + yield ord(text[i]) + 1 # codeword is ASCII value + 1 (ISO 16022:2006 5.2.3) + i += 1 # next character + +# partition data into blocks of the appropriate size to suit the +# Reed-Solomon block being used. +# e.g. partition_data([1,2,3,4,5], 3) -> [[1,2,3],[4,5,PAD]] +def partition_data(data, rs_data): + PAD_VAL = 129 # PAD codeword (ISO 16022:2006 5.2.3) + data_blocks = [] + i = 0 + while i < len(data): + if len(data) >= i + rs_data: # we have a whole block in our data + data_blocks.append(data[i:i + rs_data]) + i = i + rs_data + else: # pad out with the pad codeword + data_block = data[i:len(data)] # add any remaining data + pad_pos = len(data) + padded = False + while len(data_block) < rs_data: # and then pad with randomised pad codewords + if not padded: + data_block.append(PAD_VAL) # add a normal pad codeword + padded = True + else: + data_block.append(randomise_pad_253(PAD_VAL, pad_pos)) + pad_pos += 1 + data_blocks.append(data_block) + break + + return data_blocks + + +# Pad character randomisation, to prevent regular patterns appearing +# in the data matrix +def randomise_pad_253(pad_value, pad_position): + pseudo_random_number = ((149 * pad_position) % 253) + 1 + randomised = pad_value + pseudo_random_number + if randomised <= 254: + return randomised + else: + return randomised - 254 + +# REED-SOLOMON ENCODING ROUTINES ===================================== + +# "prod(x,y,log,alog,gf)" returns the product "x" times "y" +def prod(x, y, log, alog, gf): + if x == 0 or y == 0: + return 0 + else: + result = alog[(log[x] + log[y]) % (gf - 1)] + return result + +# generate the log & antilog lists: +def gen_log_alog(gf, pp): + log = [0] * gf + alog = [0] * gf + + log[0] = 1 - gf + alog[0] = 1 + + for i in range(1, gf): + alog[i] = alog[i - 1] * 2 + + if alog[i] >= gf: + alog[i] = alog[i] ^ pp + + log[alog[i]] = i + + return log, alog + + +# generate the generator polynomial coefficients: +def gen_poly_coeffs(nc, log, alog, gf): + c = [0] * (nc + 1) + c[0] = 1 + + for i in range(1, nc + 1): + c[i] = c[i - 1] + + j = i - 1 + while j >= 1: + c[j] = c[j - 1] ^ prod(c[j], alog[i], log, alog, gf) + j -= 1 + + c[0] = prod(c[0], alog[i], log, alog, gf) + + return c + + +# "ReedSolomon(wd,nd,nc)" takes "nd" data codeword values in wd[] +# and adds on "nc" check codewords, all within GF(gf) where "gf" is a +# power of 2 and "pp" is the value of its prime modulus polynomial */ +def reed_solomon(data, nd, nc): + # parameters of the polynomial arithmetic + gf = 256 # operating on 8-bit codewords -> Galois field = 2^8 = 256 + pp = 301 # prime modulus polynomial for ECC-200 is 0b100101101 = 301 (ISO 16022:2006 5.7.1) + + log, alog = gen_log_alog(gf, pp) + c = gen_poly_coeffs(nc, log, alog, gf) + + for block in data: # for each block of data codewords + + block.extend([0] * (nc + 1)) # extend to make space for the error codewords + + # generate "nc" checkwords in the list block + for i in range(0, nd): + k = block[nd] ^ block[i] + + for j in range(0, nc): + block[nd + j] = block[nd + j + 1] ^ prod(k, c[nc - j - 1], log, alog, gf) + + block.pop() + + return data + + +# MODULE PLACEMENT ROUTINES=========================================== +# These routines take a steam of codewords, and place them into the +# DataMatrix in accordance with Annex F of BS ISO/IEC 16022:2006 + +def bit(byte, bit_ch): + """bit() returns the bit"th bit of the byte""" + # the MSB is bit 1, LSB is bit 8 + return (byte >> (8 - bit_ch)) % 2 + +def module(array, nrow, ncol, row, col, bit_ch): + """place a given bit with appropriate wrapping within array""" + if row < 0: + row = row + nrow + col = col + 4 - ((nrow + 4) % 8) + + if col < 0: + col = col + ncol + row = row + 4 - ((ncol + 4) % 8) + + array[row][col] = bit_ch + +def place_square(case, array, nrow, ncol, row, col, char): + """Populate corner cases (0-3) and utah case (-1)""" + for i in range(8): + x, y = [ + [(row - 1, 0), (row - 1, 1), (row - 1, 2), (0, col - 2), + (0, col - 1), (1, col - 1), (2, col - 1), (3, col - 1)], + [(row - 3, 0), (row - 2, 0), (row - 1, 0), (0, col - 4), + (0, col - 3), (0, col - 2), (0, col - 1), (1, col - 1)], + [(row - 3, 0), (row - 2, 0), (row - 1, 0), (0, col - 2), + (0, col - 1), (1, col - 1), (2, col - 1), (3, col - 1)], + [(row - 1, 0), (row - 1, col - 1), (0, col - 3), (0, col - 2), + (0, col - 1), (1, col - 3), (1, col - 2), (1, col - 1)], + + # "utah" places the 8 bits of a utah-shaped symbol character in ECC200 + [(row - 2, col -2), (row - 2, col -1), (row - 1, col - 2), (row - 1, col - 1), + (row - 1, col), (row, col - 2), (row, col - 1), (row, col)], + ][case][i] + module(array, nrow, ncol, x, y, bit(char, i + 1)) + return 1 + +def place_bits(data, nrow, ncol): + """fill an nrow x ncol array with the bits from the codewords in data.""" + # initialise and fill with -1"s (invalid value) + array = [[INVALID_BIT] * ncol for i in range(nrow)] + # Starting in the correct location for character #1, bit 8,... + char = 0 + row = 4 + col = 0 + while True: + + # first check for one of the special corner cases, then... + if (row == nrow) and (col == 0): + char += place_square(0, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow - 2) and (col == 0) and (ncol % 4): + char += place_square(1, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow - 2) and (col == 0) and (ncol % 8 == 4): + char += place_square(2, array, nrow, ncol, nrow, ncol, data[char]) + elif (row == nrow + 4) and (col == 2) and ((ncol % 8) == 0): + char += place_square(3, array, nrow, ncol, nrow, ncol, data[char]) + + # sweep upward diagonally, inserting successive characters,... + while (row >= 0) and (col < ncol): + if (row < nrow) and (col >= 0) and (array[row][col] == INVALID_BIT): + char += place_square(-1, array, nrow, ncol, row, col, data[char]) + row -= 2 + col += 2 + + row += 1 + col += 3 + + # & then sweep downward diagonally, inserting successive characters,... + while (row < nrow) and (col >= 0): + if (row >= 0) and (col < ncol) and (array[row][col] == INVALID_BIT): + char += place_square(-1, array, nrow, ncol, row, col, data[char]) + row += 2 + col -= 2 + + row += 3 + col += 1 + + # ... until the entire array is scanned + if not ((row < nrow) or (col < ncol)): + break + + # Lastly, if the lower righthand corner is untouched, fill in fixed pattern */ + if array[nrow - 1][ncol - 1] == INVALID_BIT: + array[nrow - 1][ncol - 2] = 0 + array[nrow - 1][ncol - 1] = 1 + array[nrow - 2][ncol - 1] = 0 + array[nrow - 2][ncol - 2] = 1 + + return array # return the array of 1"s and 0"s + +def add_finder_pattern(array, data_nrow, data_ncol, reg_row, reg_col): + # get the total size of the datamatrix + nrow = (data_nrow + 2) * reg_row + ncol = (data_ncol + 2) * reg_col + + datamatrix = [[0] * ncol for i in range(nrow)] # initialise and fill with 0"s + + for i in range(reg_col): # for each column of data regions + for j in range(nrow): + datamatrix[j][i * (data_ncol + 2)] = 1 # vertical black bar on left + datamatrix[j][i * (data_ncol + 2) + data_ncol + 1] = j % 2 # alternating blocks + + for i in range(reg_row): # for each row of data regions + for j in range(ncol): + datamatrix[i * (data_nrow + 2) + data_nrow + 1][j] = 1 # horizontal black bar at bottom + datamatrix[i * (data_nrow + 2)][j] = (j + 1) % 2 # alternating blocks + + for i in range(data_nrow * reg_row): + for j in range(data_ncol * reg_col): + # offset by 1, plus two for every addition block + dest_col = j + 1 + 2 * (j // data_ncol) + dest_row = i + 1 + 2 * (i // data_nrow) + + datamatrix[dest_row][dest_col] = array[i][j] # transfer from the plain bit array + + return datamatrix + +def get_valid_filename(s): + s = str(s).strip().replace(" ", "_") + return re.sub(r"(?u)[^-\w.]", "", s) + +def splitAt(string, length): + return ' '.join(string[i:i+length] for i in range(0,len(string),length)) + +class InventorySticker(inkex.Effect): + + def __init__(self): + inkex.Effect.__init__(self) + self.arg_parser.add_argument("--main_tabs") + self.arg_parser.add_argument("--server_address", default="https://the.domain.de/inventory.csv") + self.arg_parser.add_argument("--htuser", default="user") + self.arg_parser.add_argument("--htpassword", default="password") + self.arg_parser.add_argument("--sticker_ids", default="*") + self.arg_parser.add_argument("--target_url", default="qwa.es") + self.arg_parser.add_argument("--target_owner", default="Stadtfabrikanten e.V.") + self.arg_parser.add_argument("--export_dir", default="/home/") + self.arg_parser.add_argument("--flat_export", type=inkex.Boolean, default=False) + self.arg_parser.add_argument("--preview", type=inkex.Boolean, default=False) + self.arg_parser.add_argument("--export_svg", type=inkex.Boolean, default=True) + self.arg_parser.add_argument("--export_png", type=inkex.Boolean, default=False) + self.arg_parser.add_argument("--print_png", type=int, default=0) + self.arg_parser.add_argument("--print_device", default="04f9:2044") + + def effect(self): + # Adjust the document view for the desired sticker size + root = self.svg.getElement("//svg:svg") + + #our QR Code has size 16x16, each cube is sized by 16x16px -> total size is 256x256px. We use 4px padding for all directions + DataMatrix_xy = 16 + DataMatrix_height = 16 * DataMatrix_xy + DataMatrix_width = DataMatrix_height + sticker_padding = 4 + sticker_height = DataMatrix_height + 2 * sticker_padding + sticker_width = 696 + root.set("width", str(sticker_width) + "px") + root.set("height", str(sticker_height) + "px") + root.set("viewBox", "%f %f %f %f" % (0, 0, sticker_width, sticker_height)) + + #clean the document (make it blank) to avoid printing duplicated things + ct = 0 + for node in self.document.xpath('//*', namespaces=inkex.NSS): + ct = ct + 1 + if ct > 3: #we keep svg:svg, sodipodi:namedview and svg:defs which defines the default canvas without any content inside + #inkex.errormsg(str(node)) + root.remove(node) + + #set the document units + self.document.getroot().find(inkex.addNS("namedview", "sodipodi")).set("inkscape:document-units", "px") + + # Download the recent inventory CSV file and parse line by line to create an inventory sticker + password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() + password_mgr.add_password(None, self.options.server_address, self.options.htuser, self.options.htpassword) + handler = urllib.request.HTTPBasicAuthHandler(password_mgr) + opener = urllib.request.build_opener(handler) + + try: + inventoryData = opener.open(self.options.server_address).read().decode("utf-8") + urllib.request.install_opener(opener) + + inventoryCSVParent = os.path.join(self.options.export_dir, "InventorySticker") + inventoryCSV = os.path.join(inventoryCSVParent, "inventory.csv") + + # To avoid messing with old stickers we remove the directory on Client before doing something new + shutil.rmtree(inventoryCSVParent, ignore_errors=True) #remove the output directory before doing new job + + # we are going to write the imported Server CSV file temporarily. Otherwise CSV reader seems to mess with the file if passed directly + if not os.path.exists(inventoryCSVParent): + os.mkdir(inventoryCSVParent) + with open(inventoryCSV, 'w', encoding="utf-8") as f: + f.write(inventoryData) + f.close() + + #parse sticker Ids from user input + if self.options.sticker_ids != "*": + sticker_ids = self.options.sticker_ids.split(",") + else: + sticker_ids = None + + with open(inventoryCSV, 'r', encoding="utf-8") as csv_file: + csv_reader = csv.reader(csv_file, delimiter=",") + for row in csv_reader: + internal_id = row[0] + doc_title = row[1] + sticker_id = row[2] + level = row[3] + zone = row[4] + + if sticker_ids is None or sticker_id in sticker_ids: + #create new sub directories for each non-existent FabLab zone (if flat export is disabled) + if self.options.flat_export == False: + if not zone: + zoneDir = os.path.join(inventoryCSVParent, "Keinem Bereich zugeordnet") + else: + zoneDir = os.path.join(inventoryCSVParent, zone) + if not os.path.exists(zoneDir): + os.mkdir(zoneDir) + else: + zoneDir = inventoryCSVParent #use top directory + + #Generate the recent sticker content + stickerGroup = self.document.getroot().add(inkex.Group(id="InventorySticker_Id" + sticker_id)) #make a new group at root level + DataMatrixStyle = inkex.Style({"stroke": "none", "stroke-width": "1", "fill": "#000000"}) + DataMatrixAttribs = {"style": str(DataMatrixStyle), "height": str(DataMatrix_xy) + "px", "width": str(DataMatrix_xy) + "px"} + + #configure font sizes and box heights to define how large the font size may be at maximum (to omit overflow) + subline_fontsize = 40 #px; one line of bottom text (id and owner) creates a box of that height + objectNameMaxHeight = sticker_height - subline_fontsize - 4 * sticker_padding + objectNameMaxLines = 5 + objectNameFontSize = objectNameMaxHeight / objectNameMaxLines #px; generate main font size from lines and box size + + # 1 - create DataMatrix (create a 2d list corresponding to the 1"s and 0s of the DataMatrix) + encoded = self.encode(self.options.target_url + "/" + sticker_id) + DataMatrixGroup = stickerGroup.add(inkex.Group(id="DataMatrix_Id" + sticker_id)) #make a new group at root level + for x, y in self.render_data_matrix(encoded, DataMatrix_xy): + DataMatrixAttribs.update({"x": str(x + sticker_padding), "y": str(y + sticker_padding)}) + etree.SubElement(DataMatrixGroup, inkex.addNS("rect","svg"), DataMatrixAttribs) + + inline_size = sticker_width - DataMatrix_width - 3 * sticker_padding #remaining width for objects next to the DataMatrix + x_pos = DataMatrix_width + 2 * sticker_padding + + # 2 - Add Object Name Text + objectName = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(objectNameFontSize) + "px", + "x": str(x_pos) + "px", + #"xml:space": "preserve", #we cannot add this here because InkScape throws an error + #"y": "4px", #if set it does not correctly apply + "text-align" : "left", + "text-anchor": "left", + "vertical-align" : "bottom", + #style: inline-size required for text wrapping inside box; letter spacing is required to remove the additional whitespaces. The letter spacing depends to the selected font family (Miso) + "style": str(inkex.Style({"fill": "#000000", "inline-size": str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "bold", "letter-spacing": "-3.66px"})) + } + ) + objectName.set("id", "objectName_Id" + sticker_id) + objectName.set("xml:space", "preserve") #so we add it here instead .. if multiple whitespaces in text are coming after each other just render them (preserve!) + objectNameTextSpan = etree.SubElement(objectName, inkex.addNS("tspan", "svg"), {}) + objectNameTextSpan.text = splitAt(doc_title, 1) #add 1 whitespace after each chacter. So we can simulate a in-word line break (break by char instead by word) + + # 3 - Add Object Id Text - use the same position but revert text anchors/align + objectId = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(subline_fontsize) + "px", + "x": str(x_pos) + "px", + "transform": "translate(0," + str(sticker_height - subline_fontsize) + ")", + #"y": "4px", #if set it does not correctly apply + "text-align" : "right", + "text-anchor": "right", + "vertical-align" : "bottom", + "style": str(inkex.Style({"fill": "#000000", "inline-size":str(inline_size) + "px", "stroke": "none", "font-family": "Miso", "font-weight": "bold"})) #inline-size required for text wrapping + } + ) + objectId.set("id", "objectId_Id" + sticker_id) + objectIdTextSpan = etree.SubElement(objectId, inkex.addNS("tspan", "svg"), {}) + objectIdTextSpan.text = sticker_id + + # 4 - Add Owner Text + owner = etree.SubElement(stickerGroup, + inkex.addNS("text", "svg"), + { + "font-size": str(subline_fontsize) + "px", + "x": str(x_pos) + "px", + "transform": "translate(0," + str(sticker_height - subline_fontsize) + ")", + #"y": "4px", #if set it does not correctly apply + "text-align" : "left", + "text-anchor": "left", + "vertical-align" : "bottom", + "style": str(inkex.Style({"fill": "#000000", "inline-size":str(inline_size) + "px", "stroke": "none", "font-family": "Miso"})) #inline-size required for text wrapping + } + ) + owner.set("id", "owner_Id" + sticker_id) + ownerTextSpan = etree.SubElement(owner, inkex.addNS("tspan", "svg"), {}) + ownerTextSpan.text = self.options.target_owner + + # 5 - Add horizontal divider line + line_thickness = 2 #px + line_x_pos = 350 #px; start of the line (left coord) + line_length = sticker_width - line_x_pos + divider = etree.SubElement(stickerGroup, + inkex.addNS("path", "svg"), + { + "d": "m " + str(line_x_pos) + "," + str(sticker_height - subline_fontsize) + " h " + str(line_length) , + "style": str(inkex.Style({"fill": "none", "stroke": "#000000", "stroke-width": str(line_thickness) + "px", "stroke-linecap": "butt", "stroke-linejoin":"miter", "stroke-opacity": "1"})) #inline-size required for text wrapping + } + ) + divider.set("id", "divider_Id" + sticker_id) + + if self.options.preview == False: + export_file_name = sticker_id + "_" + get_valid_filename(doc_title) + export_file_path = os.path.join(zoneDir, export_file_name) + + #"Export" as SVG by just copying the recent SVG document to the target directory. We need to remove special characters to have valid file names on Windows/Linux + export_file_svg = open(export_file_path + ".svg", "w", encoding="utf-8") + export_file_svg.write(str(etree.tostring(self.document), "utf-8")) + export_file_svg.close() + + if self.options.export_png == False and self.options.export_svg == False: + inkex.errormsg("Nothing to export. Generating preview only ...") + break + + if self.options.export_png == True: #we need to generate SVG before to get PNG. But if user selected PNG only we need to remove SVG afterwards + #Make PNG from SVG (slow because each file is picked up separately. Takes about 10 minutes for 600 files + inkscape(export_file_path + ".svg", actions="export-dpi:96;export-background:white;export-filename:{file_name};export-do;FileClose".format(file_name=export_file_path + ".png")) + + #fix for "usb.core.USBError: [Errno 13] Access denied (insufficient permissions)" + #echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2044", MODE="666"' > /etc/udev/rules.d/99-garmin.rules && sudo udevadm trigger + if self.options.print_png > 0: + if self.options.export_png == False: + inkex.errormsg("No file output for printing. Please set 'Export PNG' to true first.") + else: + for x in range(self.options.print_png): + command = "brother_ql -m QL-720NW --backend pyusb --printer usb://" + self.options.print_device + " print -l 62 --600dpi -r auto " + export_file_path + ".png" + p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) #forr Windows: shell=False + stdout, stderr = p.communicate() + p.wait() + if p.returncode != 0: + inkex.errormsg("brother_ql returned: %d %s %s" % (p.returncode, stdout, stderr)) + + if self.options.export_svg != True: #If user selected PNG only we need to remove SVG again + os.remove(export_file_path + ".svg") + + self.document.getroot().remove(stickerGroup) #remove the stickerGroup again + else: #create preview by just breaking the for loop without executing remove(stickerGroup) + break + csv_file.close() + except Exception as e: + inkex.errormsg(e) + #inkex.errormsg("Wrong inventory.csv URL or invalid credentials for Basic Auth") + + # parameters for the selected datamatrix size + # drow number of rows in each data region + # dcol number of cols in each data region + # reg_row number of rows of data regions + # reg_col number of cols of data regions + # nd number of data codewords per reed-solomon block + # nc number of ECC codewords per reed-solomon block + # inter number of interleaved Reed-Solomon blocks + def encode(self, text, nrow = 16, ncol = 16, data_nrow = 14, data_ncol = 14, reg_row = 1, reg_col = 1, nd = 12, nc = 12, inter = 1): + """ + Take an input string and convert it to a sequence (or sequences) + of codewords as specified in ISO/IEC 16022:2006 (section 5.2.3) + """ + # generate the codewords including padding and ECC + codewords = get_codewords(text, nd, nc, inter, nrow == 144) + + # break up into separate arrays if more than one DataMatrix is needed + module_arrays = [] + for codeword_stream in codewords: # for each datamatrix + # place the codewords" bits across the array as modules + bit_array = place_bits(codeword_stream, data_nrow * reg_row, data_ncol * reg_col) + # add finder patterns around the modules + module_arrays.append(add_finder_pattern(bit_array, data_nrow, data_ncol, reg_row, reg_col)) + + return module_arrays + + def render_data_matrix(self, module_arrays, size): + """turn a 2D array of 1"s and 0"s into a set of black squares""" + spacing = 16 * size * 1.5 + for i, line in enumerate(module_arrays): + height = len(line) + width = len(line[0]) + + for y in range(height): # loop over all the modules in the datamatrix + for x in range(width): + if line[y][x] == 1: # A binary 1 is a filled square + yield (x * size + i * spacing, y * size) + elif line[y][x] == INVALID_BIT: # we have an invalid bit value + inkex.errormsg("Invalid bit value, {}!".format(line[y][x])) + +if __name__ == "__main__": + InventorySticker().run()