import { FastifyInstance } from "fastify" import { PNG } from "pngjs" import { Utils } from "@mmote/niimbluelib" import bwipjs from "bwip-js" import Sharp from "sharp" import { tenants } from "../../db/schema" import { eq } from "drizzle-orm" export const useNextNumberRangeNumber = async ( server: FastifyInstance, tenantId: number, numberRange: string ) => { const numberRangeFallbacks: Record = { costEstimates: "quotes", packingSlips: "deliveryNotes", advanceInvoices: "invoices", cancellationInvoices: "invoices", } const [tenant] = await server.db .select() .from(tenants) .where(eq(tenants.id, tenantId)) if (!tenant) { throw new Error(`Tenant ${tenantId} not found`) } const numberRanges = tenant.numberRanges || {} const resolvedNumberRange = numberRanges[numberRange] ? numberRange : numberRangeFallbacks[numberRange] if (!resolvedNumberRange || !numberRanges[resolvedNumberRange]) { throw new Error(`Number range '${numberRange}' not found`) } const current = numberRanges[resolvedNumberRange] const usedNumber = (current.prefix || "") + current.nextNumber + (current.suffix || "") const updatedRanges = { // @ts-ignore ...numberRanges, [resolvedNumberRange]: { ...current, nextNumber: current.nextNumber + 1, }, } await server.db .update(tenants) .set({ numberRanges: updatedRanges }) .where(eq(tenants.id, tenantId)) return { usedNumber } } export async function encodeBase64ToNiimbot(base64Png: string, printDirection: "top" | "left" = "top") { const buffer = Buffer.from(base64Png, "base64") const png = PNG.sync.read(buffer) const { width, height, data } = png const cols = printDirection === "left" ? height : width const rows = printDirection === "left" ? width : height const rowsData: any[] = [] if (cols % 8 !== 0) throw new Error("Column count must be multiple of 8") for (let row = 0; row < rows; row++) { let isVoid = true let blackPixelsCount = 0 const rowData = new Uint8Array(cols / 8) for (let colOct = 0; colOct < cols / 8; colOct++) { let pixelsOctet = 0 for (let colBit = 0; colBit < 8; colBit++) { const x = printDirection === "left" ? row : colOct * 8 + colBit const y = printDirection === "left" ? height - 1 - (colOct * 8 + colBit) : row const idx = (y * width + x) * 4 const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2] const isBlack = lum < 128 if (isBlack) { pixelsOctet |= 1 << (7 - colBit) isVoid = false blackPixelsCount++ } } rowData[colOct] = pixelsOctet } const newPart = { dataType: isVoid ? "void" : "pixels", rowNumber: row, repeat: 1, rowData: isVoid ? undefined : rowData, blackPixelsCount, } if (rowsData.length === 0) { rowsData.push(newPart) } else { const last = rowsData[rowsData.length - 1] let same = newPart.dataType === last.dataType if (same && newPart.dataType === "pixels") { same = Utils.u8ArraysEqual(newPart.rowData, last.rowData) } if (same) last.repeat++ else rowsData.push(newPart) if (row % 200 === 199) { rowsData.push({ dataType: "check", rowNumber: row, repeat: 0, rowData: undefined, blackPixelsCount: 0, }) } } } return { cols, rows, rowsData } } function escapeXml(value: string) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'") } export async function generateLabel(context: any = {}, width = 584, height = 354) { const normalizedWidth = Math.ceil(Number(width) / 8) * 8 const normalizedHeight = Math.max(1, Number(height) || 203) const idFont = Math.max(24, Math.round(normalizedHeight * 0.125)) const nameFont = Math.max(17, Math.round(normalizedHeight * 0.078)) const customerFont = Math.max(14, Math.round(normalizedHeight * 0.06)) const serialFont = Math.max(12, Math.round(normalizedHeight * 0.052)) const labelId = context.customerInventoryId || context.datamatrix || context.id || "N/A" const labelName = context.name || context.text || "Kundeninventarartikel" const customerName = context.customerName || "" const serial = context.serialNumber ? `SN: ${context.serialNumber}` : "" const nameLine1 = String(labelName).slice(0, 30) const nameLine2 = String(labelName).slice(30, 60) const dataMatrixPng = await bwipjs.toBuffer({ bcid: "datamatrix", text: String(labelId), scale: normalizedWidth >= 560 ? 7 : 5, includetext: false, }) const dataMatrixMeta = await Sharp(dataMatrixPng).metadata() const dataMatrixWidth = dataMatrixMeta.width || 0 const dataMatrixHeight = dataMatrixMeta.height || 0 const dmLeft = Math.max(8, normalizedWidth - dataMatrixWidth - 28) const dmTop = Math.max(8, Math.floor((normalizedHeight - dataMatrixHeight) / 2)) const textMaxWidth = Math.max(120, dmLeft - 20) const textSvg = ` ${escapeXml(String(labelId).slice(0, 26))} ${escapeXml(nameLine1)} ${escapeXml(nameLine2)} ${escapeXml(String(customerName).slice(0, 40))} ${escapeXml(String(serial).slice(0, 42))} `.trim() const final = await Sharp({ create: { width: normalizedWidth, height: normalizedHeight, channels: 3, background: { r: 255, g: 255, b: 255 }, }, }) .composite([ { input: Buffer.from(textSvg), top: 0, left: 0 }, { input: dataMatrixPng, top: dmTop, left: dmLeft }, ]) .png() .toBuffer() return final.toString("base64") }