added some comments, fix test server, better formatting

This commit is contained in:
André Fiedler 2024-12-03 21:32:18 +01:00
parent 0009b4fc55
commit 77ae56924e
15 changed files with 2429 additions and 154 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
.vscode/ .vscode/
.DS_Store .DS_Store
node_modules/

9
.prettierrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"trailingComma": "none",
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"singleAttributePerLine": false,
"html.format.wrapLineLength": 0,
"printWidth": 140
}

View File

@ -2,6 +2,7 @@ import SVGRenderer from '/src/SVGRenderer.mjs'
import PNGRenderer from '/src/PNGRenderer.mjs' import PNGRenderer from '/src/PNGRenderer.mjs'
import PDFRenderer from '/src/PDFRenderer.mjs' import PDFRenderer from '/src/PDFRenderer.mjs'
// Get references to the custom elements
const box = document.querySelector('fabaccess-preview-box') const box = document.querySelector('fabaccess-preview-box')
const form = document.querySelector('fabaccess-settings-form') const form = document.querySelector('fabaccess-settings-form')
@ -12,10 +13,12 @@ form.addEventListener('change', (e) => {
box.update() box.update()
}) })
// Add click event listeners to the buttons
document.querySelector('#download-qr-code-svg').addEventListener('click', () => SVGRenderer.downloadSVG(form.machineID, form.size)) document.querySelector('#download-qr-code-svg').addEventListener('click', () => SVGRenderer.downloadSVG(form.machineID, form.size))
document.querySelector('#download-qr-code-png').addEventListener('click', () => PNGRenderer.downloadPNG(form.machineID, form.size)) document.querySelector('#download-qr-code-png').addEventListener('click', () => PNGRenderer.downloadPNG(form.machineID, form.size))
document.querySelector('#add-qr-code-to-page').addEventListener('click', () => PDFRenderer.addToPDF(form.machineID, form.size)) document.querySelector('#add-qr-code-to-page').addEventListener('click', () => PDFRenderer.addToPDF(form.machineID, form.size))
document.querySelector('#download-pdf').addEventListener('click', () => PDFRenderer.downloadPDF()) document.querySelector('#download-pdf').addEventListener('click', () => PDFRenderer.downloadPDF())
// Initialize the preview box
box.setAttribute('value', form.machineID) box.setAttribute('value', form.machineID)
box.setAttribute('size', form.size) box.setAttribute('size', form.size)

1997
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,21 @@
"homepage": "https://fabaccess-sticker-generator.sternenlabor.de/", "homepage": "https://fabaccess-sticker-generator.sternenlabor.de/",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
"start": "light-server --serve ./" "start": "npm run lite",
"lite": "lite-server"
}, },
"author": "André Fiedler", "author": "André Fiedler",
"license": "CC0", "license": "CC0",
"bugs": { "bugs": {
"url": "https://github.com/Sternenlabor/fabaccess-sticker-generator/issues" "url": "https://github.com/Sternenlabor/fabaccess-sticker-generator/issues"
}, },
"dependencies": {} "devDependencies": {
"axios": "^1.7.8",
"lite-server": "^2.6.1"
},
"overrides": {
"localtunnel": {
"axios": "1.6.2"
}
}
} }

View File

@ -1,3 +1,4 @@
// Create a template element to define the structure of the custom checkbox component
const template = document.createElement('template') const template = document.createElement('template')
template.innerHTML = /* html */ ` template.innerHTML = /* html */ `
<style> @import url("/src/Checkbox.css"); </style> <style> @import url("/src/Checkbox.css"); </style>
@ -7,34 +8,53 @@ template.innerHTML = /* html */ `
<slot></slot> <slot></slot>
</label>` </label>`
// Define a custom Checkbox class that extends HTMLElement
class Checkbox extends HTMLElement { class Checkbox extends HTMLElement {
#root = null #root = null // Private property to store the root node
/**
* Creates an instance of Checkbox and initializes the shadow DOM.
*/
constructor() { constructor() {
super() super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' }) this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true)) this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode() this.#root = this.shadowRoot.getRootNode()
// Get the checkbox input element from the shadow DOM
const checkbox = this.#root.getElementById('checkbox') const checkbox = this.#root.getElementById('checkbox')
// Add an event listener for the 'change' event on the checkbox
checkbox.addEventListener('change', this.#handleChange.bind(this), { checkbox.addEventListener('change', this.#handleChange.bind(this), {
passive: false passive: false
}) })
} }
/**
* Handles the 'change' event of the checkbox input.
* @param {Event} e - The event object.
* @private
*/
#handleChange(e) { #handleChange(e) {
const val = e.target.checked const val = e.target.checked // Get the checked state of the checkbox
// Dispatch a custom 'change' event from the custom element
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('change', { new CustomEvent('change', {
detail: { detail: {
checked: val checked: val // Include the checked state in the event detail
}, },
composed: true composed: true // Allow the event to bubble up through the shadow DOM boundary
}) })
) )
} }
} }
// Define the custom element 'fabaccess-checkbox' associated with the Checkbox class
customElements.define('fabaccess-checkbox', Checkbox) customElements.define('fabaccess-checkbox', Checkbox)

View File

@ -1,54 +1,70 @@
import Color from '/lib/color.js' import Color from '/lib/color.js' // Import the Color library
// Thanks to Firefox who's not supporting all color stuff in SVG like Chrome does ... we need to do this... // Due to Firefox not supporting all color functionalities in SVG like Chrome does, we need to do this workaround
export default class ColorUtils { export default class ColorUtils {
/**
* Converts a CSS color function to a hex color code.
* @param {string} cssColorFunction - The CSS color function string to convert.
* @returns {string|null} - The converted hex color code or null if conversion is not possible.
*/
static convertColor(cssColorFunction) { static convertColor(cssColorFunction) {
// Regular expression to match hex color codes // Regular expression to match hex color codes (e.g., #FFF or #FFFFFF)
const hexColorRegex = /^#(?:[0-9a-fA-F]{3}){1,2}$/ const hexColorRegex = /^#(?:[0-9a-fA-F]{3}){1,2}$/
// Check if the input is a valid hex color code // Check if the input is a valid hex color code
if (hexColorRegex.test(cssColorFunction)) { if (hexColorRegex.test(cssColorFunction)) {
return cssColorFunction // Return the hex color code if it matches the pattern return cssColorFunction // Return the hex color code if it matches the pattern
} else { } else {
// Extracting the adjustments and hex value // Extract the adjustments and hex value from the CSS color function
const result = this.extractAdjustmentsAndHex(cssColorFunction) const result = this.extractAdjustmentsAndHex(cssColorFunction)
if (result) { if (result) {
// Create a new Color object from the extracted hex value and convert it to OKLCH color space
const oklch = new Color(result.hex).to('oklch') const oklch = new Color(result.hex).to('oklch')
console.log(oklch.l, oklch.c, oklch.h) console.log(oklch.l, oklch.c, oklch.h) // Log the original OKLCH values
// Apply the adjustments to the OKLCH components
oklch.l += result.lAdjust oklch.l += result.lAdjust
oklch.c += result.cAdjust oklch.c += result.cAdjust
oklch.h += result.hAdjust oklch.h += result.hAdjust
console.log(oklch.l, oklch.c, oklch.h) console.log(oklch.l, oklch.c, oklch.h) // Log the adjusted OKLCH values
// Convert the adjusted OKLCH color back to a hex color code in A98 RGB color space
const hex = oklch.to('a98rgb').toString({ format: 'hex' }) const hex = oklch.to('a98rgb').toString({ format: 'hex' })
console.log(hex) console.log(hex) // Log the final hex color code
return hex return hex // Return the final hex color code
} else { } else {
// If no adjustments and hex value could be extracted, log a message
console.log('No values found.', cssColorFunction) console.log('No values found.', cssColorFunction)
} }
} }
} }
/**
* Extracts adjustments and hex value from a CSS OKLCH color function string.
* @param {string} css - The CSS color function string to extract from.
* @returns {Object|null} - An object containing hex, lAdjust, cAdjust, hAdjust or null if no matches found.
*/
static extractAdjustmentsAndHex(css) { static extractAdjustmentsAndHex(css) {
// Regular expression to match the specific OKLCH color function format
const regex = /oklch\(from (#\w{6}) calc\(l \+ ([\d\.\-]+)\) calc\(c \+ ([\d\.\-]+)\) calc\(h - ([\d\.\-]+)\)\)/ const regex = /oklch\(from (#\w{6}) calc\(l \+ ([\d\.\-]+)\) calc\(c \+ ([\d\.\-]+)\) calc\(h - ([\d\.\-]+)\)\)/
const matches = css.match(regex) const matches = css.match(regex)
if (matches) { if (matches) {
// Destructure the matches to extract hex value and adjustments
const [_, hex, lAdjust, cAdjust, hAdjust] = matches const [_, hex, lAdjust, cAdjust, hAdjust] = matches
return { return {
hex: hex, hex: hex, // Original hex color code
lAdjust: parseFloat(lAdjust), lAdjust: parseFloat(lAdjust), // Adjustment for lightness
cAdjust: parseFloat(cAdjust), cAdjust: parseFloat(cAdjust), // Adjustment for chroma
hAdjust: parseFloat(hAdjust) hAdjust: parseFloat(hAdjust) // Adjustment for hue
} }
} else { } else {
// Return null or some default values if no matches found // Return null if no matches found
return null return null
} }
} }

View File

@ -1,27 +1,49 @@
// Implementation of a custom EventTarget class to manage event listeners and dispatch events
export default class EventTarget { export default class EventTarget {
#listeners = {} #listeners = {} // Private property to store event listeners
/**
* Adds an event listener for a specific event type.
* @param {string} event - The event type to listen for.
* @param {Function} callback - The callback function to be called when the event is dispatched.
*/
addEventListener(event, callback) { addEventListener(event, callback) {
// Initialize the listeners array for the event if it doesn't exist
if (!this.#listeners[event]) { if (!this.#listeners[event]) {
this.#listeners[event] = [] this.#listeners[event] = []
} }
// Add the callback to the listeners array for the event
this.#listeners[event].push(callback) this.#listeners[event].push(callback)
} }
/**
* Removes an event listener for a specific event type.
* @param {string} event - The event type for which the listener should be removed.
* @param {Function} callback - The callback function to remove from the listeners.
*/
removeEventListener(event, callback) { removeEventListener(event, callback) {
// If there are no listeners for the event, exit the method
if (!this.#listeners[event]) { if (!this.#listeners[event]) {
return return
} }
// Find the index of the callback in the listeners array
const callbackIndex = this.#listeners[event].indexOf(callback) const callbackIndex = this.#listeners[event].indexOf(callback)
// If the callback exists, remove it from the array
if (callbackIndex > -1) { if (callbackIndex > -1) {
this.#listeners[event].splice(callbackIndex, 1) this.#listeners[event].splice(callbackIndex, 1)
} }
} }
/**
* Dispatches an event to all registered listeners of the event's type.
* @param {Object} event - The event object to dispatch. Should have a 'type' property.
*/
dispatchEvent(event) { dispatchEvent(event) {
// If there are no listeners for the event type, exit the method
if (!this.#listeners[event.type]) { if (!this.#listeners[event.type]) {
return return
} }
// Call each listener's callback function with the event object
this.#listeners[event.type].forEach((callback) => { this.#listeners[event.type].forEach((callback) => {
callback(event) callback(event)
}) })

View File

@ -6,6 +6,7 @@ import { changeDpiBlob } from '/lib/changeDPI/index.js'
import Utils from '/src/Utils.mjs' import Utils from '/src/Utils.mjs'
import SVGRenderer from '/src/SVGRenderer.mjs' import SVGRenderer from '/src/SVGRenderer.mjs'
// Constants for rendering
const DPI = 300.0 const DPI = 300.0
const SVG_PIXEL_HEIGHT = 108.0 const SVG_PIXEL_HEIGHT = 108.0
const SVG_PIXEL_WIDTH = 197.0 const SVG_PIXEL_WIDTH = 197.0
@ -16,10 +17,15 @@ const PAGE_MARGIN_Y = 10.0 // mm
const SPACE_BETWEEN_CODE_AND_LABEL = 4.0 // mm const SPACE_BETWEEN_CODE_AND_LABEL = 4.0 // mm
export default class PDFRenderer { export default class PDFRenderer {
static #qrCodes = [] static #qrCodes = [] // Array to store QR code data
static #pdfDoc = null static #pdfDoc = null // PDF document instance
static #listeners = {} static #listeners = {} // Event listeners
/**
* Adds an event listener for a specific event type.
* @param {string} event - The event type to listen for.
* @param {Function} callback - The callback function to be called when the event is dispatched.
*/
static addEventListener(event, callback) { static addEventListener(event, callback) {
if (!this.#listeners[event]) { if (!this.#listeners[event]) {
this.#listeners[event] = [] this.#listeners[event] = []
@ -27,6 +33,11 @@ export default class PDFRenderer {
this.#listeners[event].push(callback) this.#listeners[event].push(callback)
} }
/**
* Removes an event listener for a specific event type.
* @param {string} event - The event type for which the listener should be removed.
* @param {Function} callback - The callback function to remove from the listeners.
*/
static removeEventListener(event, callback) { static removeEventListener(event, callback) {
if (!this.#listeners[event]) { if (!this.#listeners[event]) {
return return
@ -37,6 +48,10 @@ export default class PDFRenderer {
} }
} }
/**
* Dispatches an event to all registered listeners of the event's type.
* @param {Object} event - The event object to dispatch. Should have a 'type' property.
*/
static dispatchEvent(event) { static dispatchEvent(event) {
if (!this.#listeners[event.type]) { if (!this.#listeners[event.type]) {
return return
@ -46,30 +61,39 @@ export default class PDFRenderer {
}) })
} }
/**
* Adds a QR code to the PDF document.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} size - The size of the QR code in millimeters.
*/
static async addToPDF(machineID, size) { static async addToPDF(machineID, size) {
const mmHeight = parseFloat(size) const mmHeight = parseFloat(size) // Convert size to float
const mmWidth = (mmHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH const mmWidth = (mmHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate width proportionally
const inches = mmHeight /* mm */ / 25.4 // There are 25.4 millimeters in an inch const inches = mmHeight /* mm */ / 25.4 // There are 25.4 millimeters in an inch
const pixelHeight = inches * DPI const pixelHeight = inches * DPI // Calculate pixel height
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate pixel width
const height = Math.round(pixelHeight) const height = Math.round(pixelHeight) // Round to nearest integer
const width = Math.round(pixelWidth) const width = Math.round(pixelWidth) // Round to nearest integer
// Generate SVG code for the QR code
const svgCode = SVGRenderer.getCode(machineID, height, width) const svgCode = SVGRenderer.getCode(machineID, height, width)
// Create an offscreen canvas to render the SVG
const c = new OffscreenCanvas(width, height) const c = new OffscreenCanvas(width, height)
const ctx = c.getContext('2d') const ctx = c.getContext('2d')
// Use canvg to render SVG to canvas
const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen()) const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen())
v.resize(width, height, 'xMidYMid meet') v.resize(width, height, 'xMidYMid meet') // Resize the SVG to fit the canvas
await v.render() await v.render() // Render the SVG onto the canvas
let b = await c.convertToBlob() let b = await c.convertToBlob() // Convert canvas to Blob
b = await changeDpiBlob(b, DPI) b = await changeDpiBlob(b, DPI) // Change the DPI of the blob
const imgData = URL.createObjectURL(b) const imgData = URL.createObjectURL(b) // Create a URL for the blob
// Store QR code data
this.#qrCodes.push({ this.#qrCodes.push({
machineID, machineID,
mmHeight, mmHeight,
@ -79,12 +103,17 @@ export default class PDFRenderer {
imgData imgData
}) })
// Render the PDF with the new QR code
this.renderPDF() this.renderPDF()
} }
/**
* Renders the PDF document with all added QR codes.
*/
static async renderPDF() { static async renderPDF() {
const compress = 'fast' const compress = 'fast' // Compression option for images
// Create a new jsPDF document
this.#pdfDoc = new jspdf.jsPDF({ this.#pdfDoc = new jspdf.jsPDF({
orientation: 'portrait', orientation: 'portrait',
unit: 'mm', unit: 'mm',
@ -94,47 +123,55 @@ export default class PDFRenderer {
compressPdf: false compressPdf: false
}) })
this.#pdfDoc.setFontSize(9) this.#pdfDoc.setFontSize(9) // Set font size for labels
this.#pdfDoc.setTextColor('#3c474d') this.#pdfDoc.setTextColor('#3c474d') // Set text color
// Get page dimensions
const pageSize = this.#pdfDoc.internal.pageSize const pageSize = this.#pdfDoc.internal.pageSize
const pageHeight = pageSize.height ? pageSize.height : pageSize.getHeight() const pageHeight = pageSize.height ? pageSize.height : pageSize.getHeight()
const pageWidth = this.#pdfDoc.internal.pageSize.width || this.#pdfDoc.internal.pageSize.getWidth() const pageWidth = pageSize.width ? pageSize.width : pageSize.getWidth()
// Sort QR codes by height
this.#qrCodes.sort((a, b) => a.mmHeight - b.mmHeight) this.#qrCodes.sort((a, b) => a.mmHeight - b.mmHeight)
let curX = PAGE_MARGIN_X let curX = PAGE_MARGIN_X // Current X position
let curY = PAGE_MARGIN_Y let curY = PAGE_MARGIN_Y // Current Y position
for (let i = 0; i < this.#qrCodes.length; i++) { for (let i = 0; i < this.#qrCodes.length; i++) {
const { machineID, imgData, mmWidth, mmHeight } = this.#qrCodes[i] const { machineID, imgData, mmWidth, mmHeight } = this.#qrCodes[i]
// Add QR code image to PDF
this.#pdfDoc.addImage(imgData, 'PNG', curX, curY, Math.round(mmWidth), Math.round(mmHeight), undefined, compress) this.#pdfDoc.addImage(imgData, 'PNG', curX, curY, Math.round(mmWidth), Math.round(mmHeight), undefined, compress)
// Get dimensions of the machine ID text
const txtDim = this.#pdfDoc.getTextDimensions(machineID) const txtDim = this.#pdfDoc.getTextDimensions(machineID)
// Add machine ID text below the QR code, centered
this.#pdfDoc.text(machineID, curX + (mmWidth - txtDim.w) / 2, curY + mmHeight + SPACE_BETWEEN_CODE_AND_LABEL) this.#pdfDoc.text(machineID, curX + (mmWidth - txtDim.w) / 2, curY + mmHeight + SPACE_BETWEEN_CODE_AND_LABEL)
curX += mmWidth + PADDING_BETWEEN_CODES_X curX += mmWidth + PADDING_BETWEEN_CODES_X // Update X position
if (i < this.#qrCodes.length - 1) { if (i < this.#qrCodes.length - 1) {
// we are not at the end // We are not at the end
if (curX + PADDING_BETWEEN_CODES_X + this.#qrCodes[i + 1].mmWidth > pageWidth) { if (curX + PADDING_BETWEEN_CODES_X + this.#qrCodes[i + 1].mmWidth > pageWidth) {
// next code will not fit on the current line // Next code will not fit on the current line
curX = PAGE_MARGIN_X curX = PAGE_MARGIN_X // Reset X position
curY += mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h + PADDING_BETWEEN_CODES_Y curY += mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h + PADDING_BETWEEN_CODES_Y // Move to next line
} }
if (curY + PADDING_BETWEEN_CODES_Y + this.#qrCodes[i + 1].mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h > pageHeight) { if (curY + PADDING_BETWEEN_CODES_Y + this.#qrCodes[i + 1].mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h > pageHeight) {
this.#pdfDoc.addPage() // Next code will not fit on the current page
curX = PAGE_MARGIN_X this.#pdfDoc.addPage() // Add a new page
curY = PAGE_MARGIN_Y curX = PAGE_MARGIN_X // Reset X position
curY = PAGE_MARGIN_Y // Reset Y position
} }
} }
} }
// Generate blob from PDF and create URL
var blobPDF = new Blob([this.#pdfDoc.output('blob')], { type: 'application/pdf' }) var blobPDF = new Blob([this.#pdfDoc.output('blob')], { type: 'application/pdf' })
var blobUrl = URL.createObjectURL(blobPDF) var blobUrl = URL.createObjectURL(blobPDF)
// Dispatch event with the PDF URL
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('change', { new CustomEvent('change', {
detail: { detail: {
@ -145,7 +182,10 @@ export default class PDFRenderer {
) )
} }
/**
* Initiates the download of the generated PDF document.
*/
static downloadPDF() { static downloadPDF() {
this.#pdfDoc?.save(Utils.getFilename('pdf')) this.#pdfDoc?.save(Utils.getFilename('pdf')) // Save the PDF with a generated filename
} }
} }

View File

@ -5,6 +5,7 @@ import PDFRenderer from '/src/PDFRenderer.mjs'
pdfjsLib.GlobalWorkerOptions.workerSrc = '/lib/jspdf/build/pdf.worker.mjs' pdfjsLib.GlobalWorkerOptions.workerSrc = '/lib/jspdf/build/pdf.worker.mjs'
// Create a template element to define the structure of the PDF viewer component
const template = document.createElement('template') const template = document.createElement('template')
template.innerHTML = /* html */ ` template.innerHTML = /* html */ `
<style> @import url("/src/PDFViewer.css"); </style> <style> @import url("/src/PDFViewer.css"); </style>
@ -32,71 +33,96 @@ template.innerHTML = /* html */ `
<canvas id="cnv"></canvas> <canvas id="cnv"></canvas>
</div>` </div>`
// Define a custom PDFViewer class that extends HTMLElement
class PDFViewer extends HTMLElement { class PDFViewer extends HTMLElement {
#root = null #root = null // Private property to store the root node
/**
* Creates an instance of PDFViewer and initializes the component.
*/
constructor() { constructor() {
super() super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' }) this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true)) this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode() this.#root = this.shadowRoot.getRootNode()
// Add event listener for the 'download' button to download the PDF
// this.#root.querySelector('#print').addEventListener('click', () => {}) // this.#root.querySelector('#print').addEventListener('click', () => {})
this.#root.querySelector('#download').addEventListener('click', () => PDFRenderer.downloadPDF()) this.#root.querySelector('#download').addEventListener('click', () => PDFRenderer.downloadPDF())
// Listen for 'change' events from PDFRenderer to display the PDF
PDFRenderer.addEventListener('change', (e) => { PDFRenderer.addEventListener('change', (e) => {
this.showPDF(e.detail.pdf) this.showPDF(e.detail.pdf)
}) })
} }
/**
* Displays the PDF document in the viewer.
* @param {string} blobUrl - The URL of the PDF blob to display.
*/
async showPDF(blobUrl) { async showPDF(blobUrl) {
// Load the PDF document
const loadingTask = pdfjsLib.getDocument(blobUrl) const loadingTask = pdfjsLib.getDocument(blobUrl)
const canvas = this.#root.querySelector('#cnv') const canvas = this.#root.querySelector('#cnv') // Get the canvas element
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d') // Get the 2D rendering context
const scale = 1.5 const scale = 1.5 // Scale for rendering the PDF pages
let numPage = 1 let numPage = 1 // Current page number
const doc = await loadingTask.promise const doc = await loadingTask.promise // Wait for the PDF to be loaded
/**
* Renders a specific page of the PDF document.
* @param {number} numPage - The page number to display.
*/
const showPage = async (numPage) => { const showPage = async (numPage) => {
const page = await doc.getPage(numPage) const page = await doc.getPage(numPage) // Get the page
let viewport = page.getViewport({ scale: scale }) let viewport = page.getViewport({ scale: scale }) // Get the viewport at the desired scale
canvas.height = viewport.height canvas.height = viewport.height // Set canvas height
canvas.width = viewport.width canvas.width = viewport.width // Set canvas width
let renderContext = { let renderContext = {
canvasContext: ctx, canvasContext: ctx,
viewport: viewport viewport: viewport
} }
page.render(renderContext) await page.render(renderContext) // Render the page into the canvas
// Update the page number display
this.#root.querySelector('#npages').innerHTML = `Page ${numPage} / ${doc.numPages}` this.#root.querySelector('#npages').innerHTML = `Page ${numPage} / ${doc.numPages}`
} }
// Show the first page
this.#root.querySelector('#npages').innerHTML = `Page 1 / ${doc.numPages}` this.#root.querySelector('#npages').innerHTML = `Page 1 / ${doc.numPages}`
showPage(numPage) showPage(numPage)
// Function to go to the previous page
const prevPage = () => { const prevPage = () => {
if (numPage === 1) { if (numPage === 1) {
return return // Do nothing if already at the first page
} }
numPage-- numPage--
showPage(numPage) showPage(numPage)
} }
// Function to go to the next page
const nextPage = () => { const nextPage = () => {
if (numPage >= doc.numPages) { if (numPage >= doc.numPages) {
return return // Do nothing if already at the last page
} }
numPage++ numPage++
showPage(numPage) showPage(numPage)
} }
// Add event listeners for the 'prev' and 'next' buttons
this.#root.querySelector('#prev').addEventListener('click', prevPage) this.#root.querySelector('#prev').addEventListener('click', prevPage)
this.#root.querySelector('#next').addEventListener('click', nextPage) this.#root.querySelector('#next').addEventListener('click', nextPage)
} }
} }
// Define the custom element 'fabaccess-pdf-viewer' associated with the PDFViewer class
customElements.define('fabaccess-pdf-viewer', PDFViewer) customElements.define('fabaccess-pdf-viewer', PDFViewer)

View File

@ -11,32 +11,43 @@ const SVG_PIXEL_HEIGHT = 108.0
const SVG_PIXEL_WIDTH = 197.0 const SVG_PIXEL_WIDTH = 197.0
export default class PNGRenderer { export default class PNGRenderer {
/**
* Downloads a PNG image of a QR code for the given machine ID and size.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the image in millimeters.
* @param {number|null} [width=null] - The width of the image in millimeters (optional).
*/
static async downloadPNG(machineID, height, width = null) { static async downloadPNG(machineID, height, width = null) {
const n = Utils.getFilename('png') const n = Utils.getFilename('png')
// Convert height from millimeters to inches
const inches = parseFloat(height) /* mm */ / 25.4 // There are 25.4 millimeters in an inch const inches = parseFloat(height) /* mm */ / 25.4 // There are 25.4 millimeters in an inch
const pixelHeight = inches * DPI const pixelHeight = inches * DPI // Calculate pixel height based on DPI
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate pixel width proportionally
const heightPx = Math.round(pixelHeight) const heightPx = Math.round(pixelHeight) // Round pixel height to nearest integer
const widthPx = Math.round(pixelWidth) const widthPx = Math.round(pixelWidth) // Round pixel width to nearest integer
// Generate SVG code for the QR code
const svgCode = SVGRenderer.getCode(machineID, heightPx, widthPx) const svgCode = SVGRenderer.getCode(machineID, heightPx, widthPx)
// Create an offscreen canvas to render the SVG
const c = new OffscreenCanvas(widthPx, heightPx) const c = new OffscreenCanvas(widthPx, heightPx)
const ctx = c.getContext('2d') const ctx = c.getContext('2d')
// Use canvg to render SVG to canvas
const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen()) const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen())
v.resize(widthPx, heightPx, 'xMidYMid meet') v.resize(widthPx, heightPx, 'xMidYMid meet') // Resize the SVG to fit the canvas
await v.render() await v.render() // Render the SVG onto the canvas
let b = await c.convertToBlob() let b = await c.convertToBlob() // Convert canvas to Blob
b = await changeDpiBlob(b, DPI) b = await changeDpiBlob(b, DPI) // Change the DPI of the blob
// Handle file download for different browsers
if (window.navigator.msSaveOrOpenBlob) { if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(b, n) window.navigator.msSaveOrOpenBlob(b, n) // For IE and Edge
} else { } else {
const a = document.createElement('a') const a = document.createElement('a')
const u = URL.createObjectURL(b) const u = URL.createObjectURL(b)

View File

@ -1,65 +1,100 @@
import SVGRenderer from '/src/SVGRenderer.mjs' import SVGRenderer from '/src/SVGRenderer.mjs'
// Create a template element to define the structure of the preview box component
const template = document.createElement('template') const template = document.createElement('template')
template.innerHTML = /* html */ ` template.innerHTML = /* html */ `
<style> @import url("/src/PreviewBox.css"); </style> <style> @import url("/src/PreviewBox.css"); </style>
<div id="box"></div>` <div id="box"></div>`
// Define a custom PreviewBox class that extends HTMLElement
class PreviewBox extends HTMLElement { class PreviewBox extends HTMLElement {
value = '' value = '' // The value to be encoded in the QR code
size = 0 size = 0 // The size of the QR code
#root = null #root = null // Private property to store the root node
#box = null #box = null // Private property to store the box element
#animationId = 0 #animationId = 0 // Private property for requestAnimationFrame
/**
* Creates an instance of PreviewBox and initializes the component.
*/
constructor() { constructor() {
super() super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' }) this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true)) this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode() this.#root = this.shadowRoot.getRootNode()
// Get the box element from the shadow DOM
this.#box = this.#root.getElementById('box') this.#box = this.#root.getElementById('box')
// Initialize value and size from attributes
this.value = this.getAttribute('value') || '' this.value = this.getAttribute('value') || ''
this.size = parseFloat(this.getAttribute('size')) this.size = parseFloat(this.getAttribute('size'))
} }
/**
* Observed attributes for the custom element.
* @returns {Array<string>} - The list of attributes to observe.
*/
static get observedAttributes() { static get observedAttributes() {
return ['value', 'size'] return ['value', 'size']
} }
/**
* Called when one of the observed attributes changes.
* @param {string} name - The name of the attribute that changed.
* @param {string} oldValue - The old value of the attribute.
* @param {string} newValue - The new value of the attribute.
*/
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
if (newValue != oldValue) { if (newValue != oldValue) {
switch (name) { switch (name) {
case 'value': case 'value':
this[name] = newValue this[name] = newValue // Update the value property
break break
case 'size': case 'size':
this[name] = parseFloat(newValue) this[name] = parseFloat(newValue) // Update the size property
break break
} }
this.#render() this.#render() // Re-render the QR code
} }
} }
/**
* Public method to update the QR code rendering.
*/
update() { update() {
this.#render() this.#render()
} }
/**
* Clears the content of the box element.
* @private
*/
#clear() { #clear() {
while (this.#box.childNodes[0]) { while (this.#box.firstChild) {
this.#box.removeChild(this.#box.childNodes[0]) this.#box.removeChild(this.#box.firstChild)
} }
} }
/**
* Renders the QR code inside the box element.
* @private
*/
#render() { #render() {
// Cancel any pending animation frame
window.cancelAnimationFrame(this.#animationId) window.cancelAnimationFrame(this.#animationId)
// Schedule the rendering in the next animation frame
this.#animationId = window.requestAnimationFrame(() => { this.#animationId = window.requestAnimationFrame(() => {
this.#clear() this.#clear() // Clear existing content
const time = new Date() const time = new Date()
// Generate the QR code SVG and insert it into the box
this.#box.innerHTML = SVGRenderer.getCode(this.value, `${this.size}mm`) this.#box.innerHTML = SVGRenderer.getCode(this.value, `${this.size}mm`)
console.log('QRCode generation time: ' + (new Date() - time) + ' ms') console.log('QRCode generation time: ' + (new Date() - time) + ' ms')
@ -67,4 +102,5 @@ class PreviewBox extends HTMLElement {
} }
} }
// Define the custom element 'fabaccess-preview-box' associated with the PreviewBox class
customElements.define('fabaccess-preview-box', PreviewBox) customElements.define('fabaccess-preview-box', PreviewBox)

View File

@ -7,6 +7,13 @@ const SVG_PIXEL_WIDTH = 197.0
export default class SVGRenderer { export default class SVGRenderer {
static optimizeForPrint = false static optimizeForPrint = false
/**
* Generates the SVG code for a QR code with the given machine ID and dimensions.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the SVG in pixels or millimeters.
* @param {number|null} [width=null] - The width of the SVG in pixels or millimeters (optional).
* @returns {string} - The generated SVG code.
*/
static getCode(machineID, height, width = null) { static getCode(machineID, height, width = null) {
const data = { const data = {
msg: `urn:fabaccess:resource:${machineID}`, msg: `urn:fabaccess:resource:${machineID}`,
@ -27,6 +34,7 @@ export default class SVGRenderer {
wh += `height="${height}" ` wh += `height="${height}" `
} }
// Generate the SVG code with placeholders for colors
let svgCode = ` let svgCode = `
<svg version="1.1" viewBox="0 0 ${SVG_PIXEL_WIDTH} ${SVG_PIXEL_HEIGHT}" ${wh} xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges"> <svg version="1.1" viewBox="0 0 ${SVG_PIXEL_WIDTH} ${SVG_PIXEL_HEIGHT}" ${wh} xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
<path d="m7.35 0c-4.06 0-7.33 3.27-7.33 7.33v23h-0.0197v70.7c0 4.06 3.27 7.33 7.33 7.33h182c4.06 0 7.33-3.27 7.33-7.33v-14.2h0.0197v-79.4c0-4.06-3.27-7.33-7.33-7.33z" <path d="m7.35 0c-4.06 0-7.33 3.27-7.33 7.33v23h-0.0197v70.7c0 4.06 3.27 7.33 7.33 7.33h182c4.06 0 7.33-3.27 7.33-7.33v-14.2h0.0197v-79.4c0-4.06-3.27-7.33-7.33-7.33z"
@ -55,15 +63,23 @@ export default class SVGRenderer {
return svgCode return svgCode
} }
/**
* Downloads the generated SVG code as an SVG file.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the SVG in pixels or millimeters.
* @param {number|null} [width=null] - The width of the SVG in pixels or millimeters (optional).
*/
static async downloadSVG(machineID, height, width = null) { static async downloadSVG(machineID, height, width = null) {
const svgCode = this.getCode(machineID, height, width) const svgCode = this.getCode(machineID, height, width)
const n = Utils.getFilename('svg') const n = Utils.getFilename('svg')
// Create a new Blob object with the SVG code
const b = new Blob([svgCode], { const b = new Blob([svgCode], {
type: 'image/svg+xml' type: 'image/svg+xml'
}) })
// Handle file download
if (window.navigator.msSaveOrOpenBlob) { if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(b, n) window.navigator.msSaveOrOpenBlob(b, n)
} else { } else {

View File

@ -1,6 +1,7 @@
import SVGRenderer from '/src/SVGRenderer.mjs' import SVGRenderer from '/src/SVGRenderer.mjs'
import {} from '/src/Checkbox.mjs' import {} from '/src/Checkbox.mjs'
// Create a template element to define the structure of the settings form component
const template = document.createElement('template') const template = document.createElement('template')
template.innerHTML = /* html */ ` template.innerHTML = /* html */ `
<style> @import url("/src/SettingsForm.css"); </style> <style> @import url("/src/SettingsForm.css"); </style>
@ -24,28 +25,42 @@ template.innerHTML = /* html */ `
<fabaccess-checkbox id="optimize-for-laser-printer" />Optimize Colors for Laser printing</fabaccess-checkbox>` <fabaccess-checkbox id="optimize-for-laser-printer" />Optimize Colors for Laser printing</fabaccess-checkbox>`
`
// Define a custom SettingsForm class that extends HTMLElement
class SettingsForm extends HTMLElement { class SettingsForm extends HTMLElement {
machineID = '' machineID = '' // The machine ID entered by the user
size = 25 size = 25 // The selected size for the QR code
optimizeForPrint = false optimizeForPrint = false // Whether to optimize colors for laser printing
#root = null #root = null // Private property to store the root node
/**
* Creates an instance of SettingsForm and initializes the component.
*/
constructor() { constructor() {
super() super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' }) this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true)) this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode() this.#root = this.shadowRoot.getRootNode()
// Initialize properties from attributes
this.machineID = this.getAttribute('machineid') || '' this.machineID = this.getAttribute('machineid') || ''
this.size = parseFloat(this.getAttribute('size') || 25) this.size = parseFloat(this.getAttribute('size') || 25)
this.optimizeForPrint = this.getAttribute('optimizeforprint') == 'true' || this.getAttribute('optimizeforprint') == '1' this.optimizeForPrint = this.getAttribute('optimizeforprint') == 'true' || this.getAttribute('optimizeforprint') == '1'
// Get form elements from the shadow DOM
const machineIdInput = this.#root.getElementById('machineID') const machineIdInput = this.#root.getElementById('machineID')
const optimizeForPrint = this.#root.getElementById('optimize-for-laser-printer') const optimizeForPrint = this.#root.getElementById('optimize-for-laser-printer')
const size = this.#root.getElementById('size') const size = this.#root.getElementById('size')
// Add event listener for input changes
machineIdInput.addEventListener('keyup', this.#handleInputChange.bind(this), { machineIdInput.addEventListener('keyup', this.#handleInputChange.bind(this), {
passive: false passive: false
}) })
@ -58,13 +73,24 @@ class SettingsForm extends HTMLElement {
passive: false passive: false
}) })
// Set focus to the machine ID input after the element is rendered
setTimeout(() => machineIdInput.focus(), 0) setTimeout(() => machineIdInput.focus(), 0)
} }
/**
* Observed attributes for the custom element.
* @returns {Array<string>} - The list of attributes to observe.
*/
static get observedAttributes() { static get observedAttributes() {
return ['value', 'size'] return ['value', 'size']
} }
/**
* Called when one of the observed attributes changes.
* @param {string} name - The name of the attribute that changed.
* @param {string} oldValue - The old value of the attribute.
* @param {string} newValue - The new value of the attribute.
*/
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
if (newValue != oldValue) { if (newValue != oldValue) {
switch (name) { switch (name) {
@ -78,11 +104,17 @@ class SettingsForm extends HTMLElement {
} }
} }
/**
* Handles input changes in the machine ID input field.
* @param {Event} e - The event object.
* @private
*/
#handleInputChange(e) { #handleInputChange(e) {
e.stopPropagation() e.stopPropagation()
const val = e.target.value const val = e.target.value
this.machineID = val this.machineID = val
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('change', { new CustomEvent('change', {
detail: { detail: {
@ -95,11 +127,17 @@ class SettingsForm extends HTMLElement {
) )
} }
/**
* Handles changes in the size select dropdown.
* @param {Event} e - The event object.
* @private
*/
#handleSelectChange(e) { #handleSelectChange(e) {
e.stopPropagation() e.stopPropagation()
const val = e.target.value const val = e.target.value
this.size = parseInt(val, 10) this.size = parseInt(val, 10)
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('change', { new CustomEvent('change', {
detail: { detail: {
@ -112,11 +150,17 @@ class SettingsForm extends HTMLElement {
) )
} }
/**
* Handles changes in the optimize for print checkbox.
* @param {Event} e - The event object.
* @private
*/
#handleCheckboxChange(e) { #handleCheckboxChange(e) {
e.stopPropagation() e.stopPropagation()
const val = e.detail.checked const val = e.detail.checked
this.optimizeForPrint = val this.optimizeForPrint = val
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('change', { new CustomEvent('change', {
detail: { detail: {
@ -130,4 +174,5 @@ class SettingsForm extends HTMLElement {
} }
} }
// Define the custom element 'fabaccess-settings-form' associated with the SettingsForm class
customElements.define('fabaccess-settings-form', SettingsForm) customElements.define('fabaccess-settings-form', SettingsForm)

View File

@ -1,32 +1,55 @@
import ColorUtils from '/src/ColorUtils.mjs' import ColorUtils from '/src/ColorUtils.mjs' // Import the ColorUtils module
// Define the Utils class providing utility functions
export default class Utils { export default class Utils {
/**
* Replaces all occurrences of specified substrings in a string with their corresponding replacements.
* @param {string} d - The original string to perform replacements on.
* @param {Object} r - An object where keys are substrings to replace, and values are their replacements.
* @returns {string} - The modified string with replacements made.
*/
static replace(d, r) { static replace(d, r) {
// Iterate over each key in the replacements object
for (const k of Object.keys(r)) { for (const k of Object.keys(r)) {
// Replace all occurrences of the key with its corresponding value
d = d.replaceAll(k, r[k]) d = d.replaceAll(k, r[k])
} }
return d return d // Return the modified string
} }
/**
* Generates a filename based on the current date and time and the specified file type.
* @param {string} type - The file extension or type (e.g., 'svg', 'png', 'pdf').
* @returns {string} - The generated filename.
*/
static getFilename(type) { static getFilename(type) {
// Get the current date and time in ISO format and remove special characters
const dateTime = this.replace(new Date().toISOString().slice(0, 19), { const dateTime = this.replace(new Date().toISOString().slice(0, 19), {
':': '', ':': '',
'-': '', '-': '',
T: '-' T: '-'
}) })
// Determine the filename format based on the file type
switch (type) { switch (type) {
case 'svg': case 'svg':
case 'png': case 'png':
return `fabaccess-qrcode-${dateTime}.${type}` return `fabaccess-qrcode-${dateTime}.${type}` // For SVG and PNG files
case 'pdf': case 'pdf':
return `fabaccess-qrcodes-${dateTime}.pdf` return `fabaccess-qrcodes-${dateTime}.pdf` // For PDF files
} }
// Default filename format if type doesn't match known cases
return `fabaccess-${dateTime}.${type}` return `fabaccess-${dateTime}.${type}`
} }
/**
* Retrieves the computed CSS value of a given property.
* @param {string} prop - The CSS property name.
* @returns {string} - The computed CSS value.
*/
static getCssValue(prop) { static getCssValue(prop) {
// Get the computed style of the root document element for the given property
const cssColorFunction = getComputedStyle(document.documentElement).getPropertyValue(prop) const cssColorFunction = getComputedStyle(document.documentElement).getPropertyValue(prop)
return cssColorFunction return cssColorFunction // Return the computed CSS value
//return ColorUtils.convertColor(cssColorFunction) // fuuuu Firefox //return ColorUtils.convertColor(cssColorFunction) // fuuuu Firefox
} }
} }