import JsBarcode from 'jsbarcode' import { $t } from '@/locales' const MM_UNIT = 'mm' const DEFAULT_CANVAS = { width: 70, height: 40, unit: MM_UNIT, backgroundColor: '#FFFFFF', gridSize: 1 } const TEXT_STYLE_DEFAULTS = { fontSize: 3.2, fontWeight: 400, textAlign: 'left', color: '#111111', lineHeight: 1.25 } const TABLE_STYLE_DEFAULTS = { fontSize: 2.8, fontWeight: 400, textAlign: 'left', color: '#111111', borderColor: '#111111', borderWidth: 0.2, backgroundColor: '#FFFFFF' } const FIXED_FIELD_OPTIONS = [ 'id', 'code', 'name', 'groupName', 'barcode', 'spec', 'model', 'color', 'size', 'unit', 'purUnit', 'stockUnit', 'stockLevelText', 'flagLabelManageText', 'flagCheckText', 'statusText', 'describle', 'memo', 'createByText', 'createTimeText', 'updateByText', 'updateTimeText' ] function cloneDeep(value) { return JSON.parse(JSON.stringify(value ?? null)) } function normalizeText(value) { return String(value ?? '').trim() } function normalizeNumber(value, fallback = 0) { const nextValue = Number(value) return Number.isFinite(nextValue) ? nextValue : fallback } function normalizeInteger(value, fallback = 0) { return Math.round(normalizeNumber(value, fallback)) } function normalizeColor(value, fallback = '#111111') { const color = normalizeText(value) return color || fallback } function getImageNaturalSize(element = {}) { const naturalWidth = normalizeNumber(element?.naturalWidth, 0) const naturalHeight = normalizeNumber(element?.naturalHeight, 0) return { naturalWidth, naturalHeight } } export function getImageMinHeight(width, element = {}, fallback = 8) { const { naturalWidth, naturalHeight } = getImageNaturalSize(element) if (naturalWidth > 0 && naturalHeight > 0) { return Number( ((Math.max(1, normalizeNumber(width, 1)) * naturalHeight) / naturalWidth).toFixed(2) ) } return Math.max(8, normalizeNumber(fallback, 8)) } function createElementId(prefix = 'el') { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}` } export function mmToPx(mm, scale = 4) { return `${normalizeNumber(mm) * scale}px` } export function buildMatnrPrintFieldOptions(enabledFields = []) { const fixedFields = FIXED_FIELD_OPTIONS.map((field) => ({ label: $t(`pages.basicInfo.whMat.printTemplate.helpers.fixedFields.${field}`), path: field, placeholder: `{${field}}`, category: $t('pages.basicInfo.whMat.printTemplate.fieldPanel.fixedCategory') })) const extendFields = (Array.isArray(enabledFields) ? enabledFields : []) .map((field) => { const key = normalizeText(field?.fields) if (!key) { return null } return { label: normalizeText(field?.fieldsAlise || field?.fieldsAlias || key), path: `extendFields.${key}`, placeholder: `{extendFields.${key}}`, category: $t('pages.basicInfo.whMat.printTemplate.fieldPanel.extendCategory') } }) .filter(Boolean) return [...fixedFields, ...extendFields] } export function resolveTemplatePlaceholders(template, record = {}) { const source = typeof template === 'string' ? template : String(template ?? '') return source.replace(/\{([^{}]+)\}/g, (_, rawPath) => { const value = getValueByPath(record, normalizeText(rawPath)) if (value === null || value === undefined) { return '' } if (typeof value === 'object') { return '' } return String(value) }) } export function renderMatnrPrintTemplate(template = {}, record = {}) { const normalizedTemplate = getNormalizedTemplate(template) const elements = normalizedTemplate.elements .filter((element) => element.visible !== false) .map((element) => resolveElement(element, record)) .sort((left, right) => normalizeNumber(left?.zIndex) - normalizeNumber(right?.zIndex)) return { version: normalizedTemplate.version, canvas: normalizedTemplate.canvas, elements } } export function measureMatnrPrintTemplateBounds(template = {}) { return measureNormalizedTemplateBounds(getNormalizedTemplate(template)) } export function detectMatnrPrintTemplateOverflow(template = {}) { const normalizedTemplate = getNormalizedTemplate(template) const bounds = measureNormalizedTemplateBounds(normalizedTemplate) const canvasWidth = Math.max(10, normalizeNumber(normalizedTemplate?.canvas?.width, 70)) const canvasHeight = Math.max(10, normalizeNumber(normalizedTemplate?.canvas?.height, 40)) return { hasOverflow: bounds.width > canvasWidth || bounds.height > canvasHeight, overflowX: bounds.width > canvasWidth, overflowY: bounds.height > canvasHeight, bounds, canvas: { width: canvasWidth, height: canvasHeight } } } function resolveElement(element, record) { const base = { ...cloneDeep(element), x: normalizeNumber(element?.x), y: normalizeNumber(element?.y), w: Math.max(0.5, normalizeNumber(element?.w, 10)), h: Math.max(0.5, normalizeNumber(element?.h, 6)), zIndex: normalizeInteger(element?.zIndex, 1), visible: element?.visible !== false } if (base.type === 'text') { const contentTemplate = normalizeText(base.contentTemplate) base.resolvedText = base.contentMode === 'template' ? resolveTemplatePlaceholders(contentTemplate, record) : contentTemplate base.style = { ...TEXT_STYLE_DEFAULTS, ...(base.style || {}) } return base } if (base.type === 'barcode') { const value = resolveTemplatePlaceholders(base.valueTemplate, record) base.resolvedValue = value base.showText = base.showText !== false base.symbology = 'CODE128' base.svgMarkup = buildBarcodeSvgMarkup(value, { showText: base.showText, widthMm: base.w, heightMm: base.h }) return base } if (base.type === 'qrcode') { base.resolvedValue = resolveTemplatePlaceholders(base.valueTemplate, record) return base } if (base.type === 'image') { base.resolvedSrc = normalizeText(base.src) base.objectFit = ['contain', 'cover', 'fill'].includes(base.objectFit) ? base.objectFit : 'contain' return base } if (base.type === 'line') { base.direction = base.direction === 'vertical' ? 'vertical' : 'horizontal' base.borderWidth = Math.max(0.2, normalizeNumber(base.borderWidth, 0.4)) base.color = normalizeColor(base.color, '#111111') return base } if (base.type === 'rect') { base.borderWidth = Math.max(0.2, normalizeNumber(base.borderWidth, 0.4)) base.borderColor = normalizeColor(base.borderColor, '#111111') base.backgroundColor = normalizeColor(base.backgroundColor, '#FFFFFF') base.radius = Math.max(0, normalizeNumber(base.radius, 0)) return base } if (base.type === 'table') { base.style = { ...TABLE_STYLE_DEFAULTS, ...(base.style || {}) } base.columns = Array.isArray(base.columns) ? base.columns : [] base.rows = Array.isArray(base.rows) ? base.rows : [] base.cells = Array.isArray(base.cells) ? base.cells : [] base.resolvedCells = base.cells.map((cell) => ({ ...cell, row: normalizeInteger(cell?.row), col: normalizeInteger(cell?.col), rowspan: Math.max(1, normalizeInteger(cell?.rowspan, 1)), colspan: Math.max(1, normalizeInteger(cell?.colspan, 1)), resolvedText: cell?.contentMode === 'template' ? resolveTemplatePlaceholders(cell?.contentTemplate, record) : normalizeText(cell?.contentTemplate), style: { ...TABLE_STYLE_DEFAULTS, ...(base.style || {}), ...(cell?.style || {}) } })) return base } return base } function getNormalizedTemplate(template = {}) { if ( template && typeof template === 'object' && template.canvas && template.canvas.unit === MM_UNIT && Array.isArray(template.elements) ) { return template } return normalizeMatnrPrintTemplate(template) } function measureNormalizedTemplateBounds(template = {}) { const canvasWidth = Math.max(10, normalizeNumber(template?.canvas?.width, 70)) const canvasHeight = Math.max(10, normalizeNumber(template?.canvas?.height, 40)) const bounds = (Array.isArray(template?.elements) ? template.elements : []).reduce( (result, element) => { if (element?.visible === false) { return result } const right = normalizeNumber(element?.x) + normalizeNumber(element?.w) const bottom = normalizeNumber(element?.y) + normalizeNumber(element?.h) return { width: Math.max(result.width, right), height: Math.max(result.height, bottom) } }, { width: canvasWidth, height: canvasHeight } ) return { width: Number(bounds.width.toFixed(2)), height: Number(bounds.height.toFixed(2)) } } export function normalizeMatnrPrintTemplate(template = {}) { const canvasSource = template?.canvas || template?.canvasJson?.canvas ? template?.canvas || template?.canvasJson?.canvas : {} const rawCanvasJson = template?.canvasJson && typeof template.canvasJson === 'object' && !Array.isArray(template.canvasJson) ? template.canvasJson : null const source = rawCanvasJson && Array.isArray(rawCanvasJson.elements) ? rawCanvasJson : { version: template?.version, canvas: canvasSource, elements: Array.isArray(template?.elements) ? template.elements : [] } return { id: template?.id, tenantId: template?.tenantId, name: normalizeText(template?.name) || $t('pages.basicInfo.whMat.printTemplate.workspace.unnamedTemplate'), code: normalizeText(template?.code) || `MATNR_${Date.now()}`, isDefault: normalizeInteger(template?.isDefault, 0), status: normalizeInteger(template?.status, 1), memo: normalizeText(template?.memo), version: normalizeInteger(source?.version, 1), canvas: { ...DEFAULT_CANVAS, ...(source?.canvas || {}), width: Math.max(10, normalizeNumber(source?.canvas?.width, DEFAULT_CANVAS.width)), height: Math.max(10, normalizeNumber(source?.canvas?.height, DEFAULT_CANVAS.height)), unit: MM_UNIT, backgroundColor: normalizeColor( source?.canvas?.backgroundColor, DEFAULT_CANVAS.backgroundColor ), gridSize: Math.max(1, normalizeNumber(source?.canvas?.gridSize, DEFAULT_CANVAS.gridSize)) }, elements: (Array.isArray(source?.elements) ? source.elements : []).map((element, index) => normalizeElement(element, index) ) } } function normalizeElement(element = {}, index = 0) { const normalized = { id: normalizeText(element?.id) || createElementId(), type: normalizeText(element?.type) || 'text', x: normalizeNumber(element?.x, 2 + index), y: normalizeNumber(element?.y, 2 + index), w: Math.max(1, normalizeNumber(element?.w, 20)), h: Math.max(1, normalizeNumber(element?.h, 8)), zIndex: normalizeInteger(element?.zIndex, index + 1), visible: element?.visible !== false } if (normalized.type === 'text') { normalized.contentMode = element?.contentMode === 'template' ? 'template' : 'static' normalized.contentTemplate = normalizeText(element?.contentTemplate) normalized.style = { ...TEXT_STYLE_DEFAULTS, ...(element?.style || {}), fontSize: Math.max( 1.8, normalizeNumber(element?.style?.fontSize, TEXT_STYLE_DEFAULTS.fontSize) ), fontWeight: normalizeInteger(element?.style?.fontWeight, TEXT_STYLE_DEFAULTS.fontWeight), textAlign: ['left', 'center', 'right'].includes(element?.style?.textAlign) ? element.style.textAlign : TEXT_STYLE_DEFAULTS.textAlign, color: normalizeColor(element?.style?.color, TEXT_STYLE_DEFAULTS.color), lineHeight: Math.max( 1, normalizeNumber(element?.style?.lineHeight, TEXT_STYLE_DEFAULTS.lineHeight) ) } return normalized } if (normalized.type === 'barcode') { normalized.valueTemplate = normalizeText(element?.valueTemplate) normalized.showText = element?.showText !== false normalized.symbology = 'CODE128' normalized.h = Math.max(8, normalizeNumber(element?.h, 14)) return normalized } if (normalized.type === 'qrcode') { normalized.valueTemplate = normalizeText(element?.valueTemplate) normalized.w = Math.max(8, normalizeNumber(element?.w, 18)) normalized.h = Math.max(8, normalizeNumber(element?.h, 18)) return normalized } if (normalized.type === 'image') { normalized.src = normalizeText(element?.src) normalized.objectFit = ['contain', 'cover', 'fill'].includes(element?.objectFit) ? element.objectFit : 'contain' normalized.naturalWidth = Math.max(0, normalizeNumber(element?.naturalWidth, 0)) normalized.naturalHeight = Math.max(0, normalizeNumber(element?.naturalHeight, 0)) normalized.w = Math.max(8, normalizeNumber(element?.w, 20)) normalized.h = Math.max( getImageMinHeight(normalized.w, element, normalizeNumber(element?.h, 20)), normalizeNumber(element?.h, 20) ) return normalized } if (normalized.type === 'line') { normalized.direction = element?.direction === 'vertical' ? 'vertical' : 'horizontal' normalized.borderWidth = Math.max(0.2, normalizeNumber(element?.borderWidth, 0.4)) normalized.color = normalizeColor(element?.color, '#111111') normalized.w = normalized.direction === 'horizontal' ? Math.max(6, normalizeNumber(element?.w, 20)) : Math.max(0.4, normalizeNumber(element?.w, 0.4)) normalized.h = normalized.direction === 'vertical' ? Math.max(6, normalizeNumber(element?.h, 20)) : Math.max(0.4, normalizeNumber(element?.h, 0.4)) return normalized } if (normalized.type === 'rect') { normalized.borderWidth = Math.max(0.2, normalizeNumber(element?.borderWidth, 0.4)) normalized.borderColor = normalizeColor(element?.borderColor, '#111111') normalized.backgroundColor = normalizeColor(element?.backgroundColor, '#FFFFFF') normalized.radius = Math.max(0, normalizeNumber(element?.radius, 0)) return normalized } if (normalized.type === 'table') { normalized.style = { ...TABLE_STYLE_DEFAULTS, ...(element?.style || {}), fontSize: Math.max( 1.8, normalizeNumber(element?.style?.fontSize, TABLE_STYLE_DEFAULTS.fontSize) ), fontWeight: normalizeInteger(element?.style?.fontWeight, TABLE_STYLE_DEFAULTS.fontWeight), textAlign: ['left', 'center', 'right'].includes(element?.style?.textAlign) ? element.style.textAlign : TABLE_STYLE_DEFAULTS.textAlign, color: normalizeColor(element?.style?.color, TABLE_STYLE_DEFAULTS.color), borderColor: normalizeColor(element?.style?.borderColor, TABLE_STYLE_DEFAULTS.borderColor), borderWidth: Math.max( 0.2, normalizeNumber(element?.style?.borderWidth, TABLE_STYLE_DEFAULTS.borderWidth) ), backgroundColor: normalizeColor( element?.style?.backgroundColor, TABLE_STYLE_DEFAULTS.backgroundColor ) } normalized.columns = normalizeTableColumns(element?.columns, normalized.w) normalized.rows = normalizeTableRows(element?.rows) normalized.w = Math.max( normalizeNumber(normalized.w, 0), normalized.columns.reduce((total, column) => total + normalizeNumber(column?.width, 0), 0) ) normalized.h = Math.max( normalizeNumber(normalized.h, 0), normalized.rows.reduce((total, row) => total + normalizeNumber(row?.height, 0), 0) ) normalized.cells = normalizeTableCells(element?.cells) return normalized } return normalized } function normalizeTableColumns(columns = [], totalWidth = 40) { const source = Array.isArray(columns) && columns.length ? columns : [{ width: totalWidth * 0.36 }, { width: totalWidth * 0.64 }] return source.map((column) => ({ width: Math.max(6, normalizeNumber(column?.width, 18)) })) } function normalizeTableRows(rows = []) { const source = Array.isArray(rows) && rows.length ? rows : [{ height: 6 }] return source.map((row) => ({ height: Math.max(4, normalizeNumber(row?.height, 6)) })) } function normalizeTableCells(cells = []) { return (Array.isArray(cells) ? cells : []).map((cell) => ({ row: normalizeInteger(cell?.row), col: normalizeInteger(cell?.col), rowspan: Math.max(1, normalizeInteger(cell?.rowspan, 1)), colspan: Math.max(1, normalizeInteger(cell?.colspan, 1)), contentMode: cell?.contentMode === 'template' ? 'template' : 'static', contentTemplate: normalizeText(cell?.contentTemplate), style: { ...TABLE_STYLE_DEFAULTS, ...(cell?.style || {}) } })) } export function buildMatnrPrintTemplatePayload(template = {}) { const normalized = normalizeMatnrPrintTemplate(template) return { ...(normalized.id ? { id: normalized.id } : {}), name: normalized.name, code: normalized.code, isDefault: normalized.isDefault, status: normalized.status, memo: normalized.memo, canvasJson: { version: normalized.version, canvas: normalized.canvas, elements: normalized.elements } } } export function createDefaultMatnrPrintTemplate(seed = {}) { const name = normalizeText(seed?.name) || $t('pages.basicInfo.whMat.printTemplate.helpers.defaultTemplateName') const code = normalizeText(seed?.code) || `MATNR_${Date.now()}` return normalizeMatnrPrintTemplate({ name, code, status: 1, isDefault: seed?.isDefault ? 1 : 0, memo: '', canvasJson: { version: 1, canvas: { ...DEFAULT_CANVAS }, elements: [ { id: createElementId(), type: 'text', x: 4, y: 3, w: 18, h: 5, zIndex: 1, visible: true, contentMode: 'static', contentTemplate: $t( 'pages.basicInfo.whMat.printTemplate.helpers.defaultText.productCode' ), style: { ...TEXT_STYLE_DEFAULTS, fontSize: 3.1, fontWeight: 600, textAlign: 'center' } }, { id: createElementId(), type: 'barcode', x: 24, y: 3, w: 40, h: 14, zIndex: 2, visible: true, valueTemplate: '{code}', showText: true, symbology: 'CODE128' }, { id: createElementId(), type: 'text', x: 4, y: 20, w: 12, h: 5, zIndex: 3, visible: true, contentMode: 'static', contentTemplate: $t( 'pages.basicInfo.whMat.printTemplate.helpers.defaultText.productName' ), style: { ...TEXT_STYLE_DEFAULTS, fontSize: 3, fontWeight: 600 } }, { id: createElementId(), type: 'text', x: 17, y: 20, w: 47, h: 5, zIndex: 4, visible: true, contentMode: 'template', contentTemplate: '{name}', style: { ...TEXT_STYLE_DEFAULTS, fontSize: 3, fontWeight: 500 } } ] } }) } export function duplicateMatnrPrintTemplate(template = {}) { const normalized = normalizeMatnrPrintTemplate(template) return normalizeMatnrPrintTemplate({ ...cloneDeep(normalized), id: null, name: `${normalized.name}${$t('pages.basicInfo.whMat.printTemplate.helpers.copySuffix')}`, code: `${normalized.code}_COPY_${Date.now()}`, isDefault: 0, canvasJson: { version: normalized.version, canvas: normalized.canvas, elements: normalized.elements.map((element) => ({ ...cloneDeep(element), id: createElementId() })) } }) } export function createElementByType(type, offsetIndex = 0) { const offset = offsetIndex * 2 const common = { id: createElementId(), type, x: 4 + offset, y: 4 + offset, zIndex: offsetIndex + 1, visible: true } if (type === 'text') { return normalizeElement({ ...common, w: 24, h: 6, contentMode: 'static', contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.defaultText.staticText'), style: { ...TEXT_STYLE_DEFAULTS } }) } if (type === 'barcode') { return normalizeElement({ ...common, w: 34, h: 14, valueTemplate: '{code}', showText: true, symbology: 'CODE128' }) } if (type === 'qrcode') { return normalizeElement({ ...common, w: 20, h: 20, valueTemplate: '{barcode}' }) } if (type === 'image') { return normalizeElement({ ...common, w: 20, h: 20, src: '', naturalWidth: 0, naturalHeight: 0, objectFit: 'contain' }) } if (type === 'line') { return normalizeElement({ ...common, w: 24, h: 0.4, direction: 'horizontal', borderWidth: 0.4, color: '#111111' }) } if (type === 'rect') { return normalizeElement({ ...common, w: 24, h: 12, borderWidth: 0.4, borderColor: '#111111', backgroundColor: '#FFFFFF', radius: 0 }) } if (type === 'table') { return createFieldListTableElement(offsetIndex) } return normalizeElement(common) } export function createFieldListTableElement(offsetIndex = 0) { const x = 4 + offsetIndex * 2 const y = 4 + offsetIndex * 2 return normalizeElement({ id: createElementId(), type: 'table', x, y, w: 58, h: 24, zIndex: offsetIndex + 1, visible: true, style: { ...TABLE_STYLE_DEFAULTS, fontSize: 2.8 }, columns: [{ width: 18 }, { width: 40 }], rows: [{ height: 6 }, { height: 6 }, { height: 6 }, { height: 6 }], cells: [ { row: 0, col: 0, contentMode: 'static', contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.code') }, { row: 0, col: 1, contentMode: 'template', contentTemplate: '{code}' }, { row: 1, col: 0, contentMode: 'static', contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.name') }, { row: 1, col: 1, contentMode: 'template', contentTemplate: '{name}' }, { row: 2, col: 0, contentMode: 'static', contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.spec') }, { row: 2, col: 1, contentMode: 'template', contentTemplate: '{spec}' }, { row: 3, col: 0, contentMode: 'static', contentTemplate: $t('pages.basicInfo.whMat.printTemplate.helpers.tableLabels.model') }, { row: 3, col: 1, contentMode: 'template', contentTemplate: '{model}' } ] }) } export function getFieldListTableRows(element = {}) { if (element?.type !== 'table') { return [] } const rows = Array.isArray(element?.rows) ? element.rows : [] const cells = Array.isArray(element?.cells) ? element.cells : [] return rows.map((row, rowIndex) => { const labelCell = cells.find( (cell) => normalizeInteger(cell?.row) === rowIndex && normalizeInteger(cell?.col) === 0 ) const valueCell = cells.find( (cell) => normalizeInteger(cell?.row) === rowIndex && normalizeInteger(cell?.col) === 1 ) return { id: `${element.id}_row_${rowIndex}`, height: Math.max(4, normalizeNumber(row?.height, 6)), labelTemplate: normalizeText(labelCell?.contentTemplate), labelMode: labelCell?.contentMode === 'template' ? 'template' : 'static', valueTemplate: normalizeText(valueCell?.contentTemplate), valueMode: valueCell?.contentMode === 'static' ? 'static' : 'template' } }) } export function updateFieldListTableRows(element = {}, rowConfigs = []) { const sourceRows = Array.isArray(rowConfigs) && rowConfigs.length ? rowConfigs : [] return normalizeElement({ ...cloneDeep(element), rows: sourceRows.map((row) => ({ height: Math.max(4, normalizeNumber(row?.height, 6)) })), columns: normalizeTableColumns(element?.columns, element?.w), cells: sourceRows.flatMap((row, index) => [ { row: index, col: 0, contentMode: row?.labelMode === 'template' ? 'template' : 'static', contentTemplate: normalizeText(row?.labelTemplate) }, { row: index, col: 1, contentMode: row?.valueMode === 'static' ? 'static' : 'template', contentTemplate: normalizeText(row?.valueTemplate) } ]) }) } export function appendFieldPlaceholder(originValue, placeholder) { const currentValue = String(originValue ?? '') return `${currentValue}${placeholder}` } function getValueByPath(source, path) { if (!path) { return '' } return path.split('.').reduce((current, key) => { if (current === null || current === undefined) { return '' } return current[key] }, source) } export function buildBarcodeSvgMarkup(value, options = {}) { const codeValue = normalizeText(value) if (!codeValue || typeof document === 'undefined') { return '' } try { const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg') const widthMm = Math.max(8, normalizeNumber(options?.widthMm, 20)) const heightMm = Math.max(8, normalizeNumber(options?.heightMm, 12)) const fontSize = Math.max(12, normalizeNumber(options?.fontSize, 12)) const targetHeightPx = Math.max(32, heightMm * 4) const textReservePx = options?.showText !== false ? Math.max(fontSize + 8, 18) : 0 JsBarcode(svgNode, codeValue, { format: 'CODE128', displayValue: options?.showText !== false, width: Math.max(1, normalizeNumber(options?.lineWidth, 1.4)), height: Math.max(12, targetHeightPx - textReservePx), margin: 0, background: '#FFFFFF', lineColor: '#111111', font: 'Arial', fontSize, textMargin: 2 }) svgNode.setAttribute('width', `${widthMm}mm`) svgNode.setAttribute('height', `${heightMm}mm`) svgNode.setAttribute('preserveAspectRatio', 'none') return svgNode.outerHTML } catch (error) { console.warn('[matnr-print] barcode render failed', error) return '' } }