diff --git a/index.template.html b/index.template.html index 5f341b9..0e21cde 100644 --- a/index.template.html +++ b/index.template.html @@ -508,6 +508,7 @@ + diff --git a/js/tools/CodesTool.js b/js/tools/CodesTool.js index 0084fe0..000f9d7 100644 --- a/js/tools/CodesTool.js +++ b/js/tools/CodesTool.js @@ -52,7 +52,10 @@ class CodesTool extends Tool { } if (this.codesFormat === 'ean13') { var digits = text.replace(/\D/g, ''); - if (digits.length !== 12 && digits.length !== 13) { + if (!digits.length) { + return 'EAN-13 requires digits only (12 or 13 digits). Letters and symbols are not valid.'; + } + if (digits.length < 12 || digits.length > 13) { return 'EAN-13 requires 12 or 13 digits (check digit is calculated automatically for 12).'; } } @@ -64,23 +67,34 @@ class CodesTool extends Tool { codesClearOutput: function() { this.codesOutputUrl = ''; this.codesOutputSvg = ''; + }, + codesResetGenerate: function() { + this.codesClearOutput(); this.codesError = ''; }, codesGenerate: function() { var validationError = this.codesValidateGenerateInput(); if (validationError) { - this.codesError = validationError; this.codesClearOutput(); + this.codesError = validationError; return; } this.codesError = ''; - this.codesOutputUrl = ''; - this.codesOutputSvg = ''; + this.codesClearOutput(); var text = String(this.codesInput || '').trim(); if (this.codesFormat === 'ean13') { - text = text.replace(/\D/g, ''); + if (typeof Ean13Utils === 'undefined' || typeof Ean13Utils.normalize !== 'function') { + this.codesError = 'EAN-13 helpers not loaded. Rebuild the app (npm run build).'; + return; + } + var ean13 = Ean13Utils.normalize(text); + if (!ean13.ok) { + this.codesError = ean13.error; + return; + } + text = ean13.value; } if (this.codesFormat === 'code39') { text = text.toUpperCase(); @@ -137,6 +151,60 @@ class CodesTool extends Tool { this.codesError = (err && err.message) || 'Failed to generate barcode.'; } }, + codesRasterizeToPng: function(imageUrl) { + return new Promise(function(resolve, reject) { + var img = new Image(); + img.onload = function() { + var canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth || img.width; + canvas.height = img.naturalHeight || img.height; + var ctx = canvas.getContext('2d'); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); + resolve(canvas.toDataURL('image/png')); + }; + img.onerror = function() { + reject(new Error('Failed to render barcode image.')); + }; + img.src = imageUrl; + }); + }, + codesCopyImage: function() { + var self = this; + if (!this.codesOutputUrl) { + return; + } + if (!navigator.clipboard || typeof navigator.clipboard.write !== 'function') { + this.codesError = 'Image copy is not supported in this browser.'; + return; + } + + var blobPromise; + if (this.codesOutputUrl.indexOf('data:image/png') === 0) { + blobPromise = fetch(this.codesOutputUrl).then(function(response) { + return response.blob(); + }); + } else { + blobPromise = this.codesRasterizeToPng(this.codesOutputUrl).then(function(pngUrl) { + return fetch(pngUrl).then(function(response) { + return response.blob(); + }); + }); + } + + blobPromise.then(function(blob) { + return navigator.clipboard.write([ + new ClipboardItem({ 'image/png': blob }) + ]); + }).then(function() { + if (typeof self.showNotification === 'function') { + self.showNotification('Image copied to clipboard', 'success', 'fas fa-copy'); + } + }).catch(function(err) { + self.codesError = (err && err.message) || 'Failed to copy image to clipboard.'; + }); + }, codesDownload: function() { if (!this.codesOutputUrl) { return; @@ -248,7 +316,7 @@ class CodesTool extends Tool { } }, codesFormat: function() { - this.codesClearOutput(); + this.codesResetGenerate(); } }; } diff --git a/js/utils/ean13.js b/js/utils/ean13.js new file mode 100644 index 0000000..13d9230 --- /dev/null +++ b/js/utils/ean13.js @@ -0,0 +1,72 @@ +/** + * EAN-13 normalization helpers for the Codes tool. + */ +(function(global) { + 'use strict'; + + function checksum(base12) { + var sum = 0; + for (var i = 0; i < 12; i++) { + var digit = parseInt(base12.charAt(i), 10); + sum += (i % 2 === 0) ? digit : digit * 3; + } + return String((10 - (sum % 10)) % 10); + } + + function upcChecksum(upc12) { + var sum = 0; + for (var i = 0; i < 11; i++) { + var digit = parseInt(upc12.charAt(i), 10); + sum += (i % 2 === 0) ? digit * 3 : digit; + } + return String((10 - (sum % 10)) % 10); + } + + function isValidUpcA(digits) { + return digits.length === 12 + && digits.charAt(0) === '0' + && upcChecksum(digits) === digits.charAt(11); + } + + function fromUpcA(upc12) { + var ean12 = '0' + upc12.slice(0, 11); + return ean12 + checksum(ean12); + } + + function normalize(input) { + var digits = String(input || '').replace(/\D/g, ''); + if (digits.length < 12) { + return { + ok: false, + error: 'EAN-13 requires 12 or 13 digits.' + }; + } + if (digits.length > 13) { + return { + ok: false, + error: 'EAN-13 accepts at most 13 digits.' + }; + } + + if (digits.length === 12 && isValidUpcA(digits)) { + return { ok: true, value: fromUpcA(digits) }; + } + + var base12 = digits.length === 12 ? digits : digits.slice(0, 12); + return { ok: true, value: base12 + checksum(base12) }; + } + + var api = { + checksum: checksum, + upcChecksum: upcChecksum, + isValidUpcA: isValidUpcA, + fromUpcA: fromUpcA, + normalize: normalize + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = api; + } else { + global.Ean13Utils = api; + } +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/templates/codes.html b/templates/codes.html index 0b13fad..426ad9b 100644 --- a/templates/codes.html +++ b/templates/codes.html @@ -80,29 +80,32 @@ + +
-
{{ codesError }}
-
+ -
diff --git a/tests/test_codes_ean13.js b/tests/test_codes_ean13.js new file mode 100644 index 0000000..a848bb0 --- /dev/null +++ b/tests/test_codes_ean13.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +const assert = require('assert'); +const path = require('path'); + +const Ean13Utils = require(path.join(__dirname, '..', 'js', 'utils', 'ean13.js')); + +assert.strictEqual(Ean13Utils.checksum('590123412345'), '7'); +assert.strictEqual(Ean13Utils.checksum('400638133393'), '1'); + +const from12 = Ean13Utils.normalize('590123412345'); +assert.strictEqual(from12.ok, true); +assert.strictEqual(from12.value, '5901234123457'); + +const from13 = Ean13Utils.normalize('5901234123457'); +assert.strictEqual(from13.ok, true); +assert.strictEqual(from13.value, '5901234123457'); + +const wrongCheck = Ean13Utils.normalize('5901234123450'); +assert.strictEqual(wrongCheck.ok, true); +assert.strictEqual(wrongCheck.value, '5901234123457'); + +const withSeparators = Ean13Utils.normalize('590-1234-12345'); +assert.strictEqual(withSeparators.ok, true); +assert.strictEqual(withSeparators.value, '5901234123457'); + +const upcA = Ean13Utils.normalize('036000291452'); +assert.strictEqual(upcA.ok, true); +assert.strictEqual(upcA.value, '0036000291452'); + +const tooShort = Ean13Utils.normalize('12345'); +assert.strictEqual(tooShort.ok, false); + +const tooLong = Ean13Utils.normalize('12345678901234'); +assert.strictEqual(tooLong.ok, false); + +const EAN13 = require(path.join(__dirname, '..', 'node_modules', 'jsbarcode', 'bin', 'barcodes', 'EAN_UPC', 'EAN13.js')).default; +const defaults = require(path.join(__dirname, '..', 'node_modules', 'jsbarcode', 'bin', 'options', 'defaults.js')).default; + +const normalizedWrong = Ean13Utils.normalize('5901234123450'); +const encoder = new EAN13(normalizedWrong.value, Object.assign({}, defaults, { displayValue: true })); +assert.strictEqual(encoder.valid(), true, 'Normalized value should pass JsBarcode validation'); + +console.log('EAN-13 codes tests passed');