From de188fb77a1a384dc0e24d2b704aa73d8f28a33f Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Fri, 1 Aug 2025 17:01:23 -0500 Subject: [PATCH] Add ImageTracer integration and precise coordinate system for bitmap vectorization - Integrated ImageTracer npm package for bitmap-to-vector conversion - Fixed coordinate system to use 33x33 QR code coordinate space - Implemented precise decimal coordinates for perfect centering - Added custom bitmap-to-vector conversion as fallback - Fixed white box and image positioning alignment - Added comprehensive debugging and error handling --- package-lock.json | 10 + package.json | 1 + src/components/TextAreaComponent.jsx | 320 ++++++++++++++++++++++++--- src/style.css | 95 ++++++++ 4 files changed, 401 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d888b4..4413bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "qrcodes", "version": "0.0.0", "dependencies": { + "imagetracer": "^0.2.2", "qrcode": "^1.5.4", "react": "^19.1.1", "react-dom": "^19.1.1" @@ -1437,6 +1438,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/imagetracer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/imagetracer/-/imagetracer-0.2.2.tgz", + "integrity": "sha512-tsdjCwfyJyl9dOHpMaMqqVLGC8V/KFgFd/ATdLSU5LlsZJMRxWOxNpfai0xgUbdrNw9uHUUXrYFwIjDKNXBdYA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/murongg" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", diff --git a/package.json b/package.json index 8f3d93c..cecb28a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "vite": "^7.0.4" }, "dependencies": { + "imagetracer": "^0.2.2", "qrcode": "^1.5.4", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/src/components/TextAreaComponent.jsx b/src/components/TextAreaComponent.jsx index af4857e..1f9cc20 100644 --- a/src/components/TextAreaComponent.jsx +++ b/src/components/TextAreaComponent.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react' import QRCode from 'qrcode' +import { imageTracer } from 'imagetracer' const TextAreaComponent = () => { const [text, setText] = useState('') @@ -7,26 +8,29 @@ const TextAreaComponent = () => { const [customImage, setCustomImage] = useState(null) const [customImageUrl, setCustomImageUrl] = useState('') const [imageSize, setImageSize] = useState(25) // Size as percentage of QR code + const [foregroundColor, setForegroundColor] = useState('#0000ff') // QR code foreground color + const [backgroundColor, setBackgroundColor] = useState('#FFFFFF') // QR code background color const canvasRef = useRef(null) - // Generate QR code when text, custom image, or image size changes + // Generate QR code when text, custom image, image size, or colors change useEffect(() => { if (text.trim()) { generateQRCode(text) } else { setQrCodeUrl('') } - }, [text, customImage, imageSize]) + }, [text, customImage, imageSize, foregroundColor, backgroundColor]) const generateQRCode = async (inputText) => { try { // Generate QR code as data URL const url = await QRCode.toDataURL(inputText, { - width: 256, + width: 512, margin: 2, + errorCorrectionLevel: 'H', color: { - dark: '#000000', - light: '#FFFFFF' + dark: foregroundColor, + light: backgroundColor } }) @@ -43,14 +47,241 @@ const TextAreaComponent = () => { } } + const generateSVGQRCode = async (inputText) => { + try { + // Generate QR code as SVG + const svgString = await QRCode.toString(inputText, { + type: 'svg', + width: 512, + margin: 2, + errorCorrectionLevel: 'H', + color: { + dark: foregroundColor, + light: backgroundColor + } + }) + + console.log('Base SVG generated, length:', svgString.length) + + if (customImage) { + console.log('Custom image detected, adding to SVG...') + // Add custom image to SVG + const svgWithImage = await addImageToSVG(svgString, customImageUrl) + console.log('SVG with image generated, length:', svgWithImage.length) + return svgWithImage + } else { + console.log('No custom image, returning base SVG') + return svgString + } + } catch (error) { + console.error('Error generating SVG QR code:', error) + return null + } + } + + const addImageToSVG = async (svgString, imageUrl) => { + return new Promise((resolve) => { + const img = new Image() + + img.onload = () => { + // Parse the SVG string to add custom image + const parser = new DOMParser() + const svgDoc = parser.parseFromString(svgString, 'image/svg+xml') + const svgElement = svgDoc.documentElement + + // Calculate image size and position with precise decimal coordinates + // The QR code uses a 33x33 coordinate system, so we need to scale accordingly + const qrSize = 33 // QR code coordinate system size + const calculatedImageSize = qrSize * (imageSize / 100) + const margin = 2 // Smaller margin for the 33x33 coordinate system + const boxSize = calculatedImageSize + (margin * 2) + const boxX = (qrSize - boxSize) / 2 + const boxY = (qrSize - boxSize) / 2 + const imageX = boxX + margin + const imageY = boxY + margin + + // Create white background rectangle with precise positioning + const whiteBox = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'rect') + whiteBox.setAttribute('x', boxX.toFixed(2)) + whiteBox.setAttribute('y', boxY.toFixed(2)) + whiteBox.setAttribute('width', boxSize.toFixed(2)) + whiteBox.setAttribute('height', boxSize.toFixed(2)) + whiteBox.setAttribute('fill', '#FFFFFF') + + // Add elements to SVG + svgElement.appendChild(whiteBox) + + // Handle different image types + if (imageUrl.startsWith('data:image/svg+xml')) { + console.log('Processing SVG image') + // For SVG images, embed the SVG content directly with precise positioning + const imageElement = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'image') + imageElement.setAttribute('x', imageX.toFixed(2)) + imageElement.setAttribute('y', imageY.toFixed(2)) + imageElement.setAttribute('width', calculatedImageSize.toFixed(2)) + imageElement.setAttribute('height', calculatedImageSize.toFixed(2)) + imageElement.setAttribute('href', imageUrl) + svgElement.appendChild(imageElement) + console.log('SVG image element added') + + // Convert back to string + const serializer = new XMLSerializer() + resolve(serializer.serializeToString(svgElement)) + } else { + console.log('Processing bitmap image') + // For bitmap images, convert to vector paths + vectorizeBitmap(img, calculatedImageSize, imageX, imageY, svgDoc).then((vectorizedImage) => { + svgElement.appendChild(vectorizedImage) + console.log('Vectorized image group added') + + // Convert back to string + const serializer = new XMLSerializer() + resolve(serializer.serializeToString(svgElement)) + }).catch((error) => { + console.error('Vectorization failed:', error) + // Convert back to string without the image + const serializer = new XMLSerializer() + resolve(serializer.serializeToString(svgElement)) + }) + } + } + + img.onerror = () => { + console.error('Error loading image for SVG') + resolve(svgString) // Return SVG without image if loading fails + } + + img.src = imageUrl + }) + } + + const vectorizeBitmap = (img, targetSize, x, y, svgDoc) => { + return new Promise((resolve) => { + // Create a canvas to get the image data + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + // Set canvas size to match the image + canvas.width = img.width + canvas.height = img.height + + // Draw the image to canvas + ctx.drawImage(img, 0, 0) + + // Get image data for ImageTracer + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) + + // Configure ImageTracer options + const options = { + ltres: 1, // Line threshold + qtres: 1, // Quadratic threshold + pathomit: 0, // Path omission threshold + colorsampling: 0, // Color sampling + numberofcolors: 16, // Number of colors + mincolorratio: 0.02, // Minimum color ratio + strokewidth: 1, // Stroke width + linefilter: false, // Line filter + scale: 1, // Scale + roundcoords: 1, // Round coordinates + viewbox: false, // Viewbox + desc: false, // Description + lcpr: 0, // Line connecting precision + qcpr: 0, // Quadratic curve precision + blurradius: 0, // Blur radius + blurdelta: 20 // Blur delta + } + + console.log('Starting ImageTracer conversion...') + + // Check if ImageTracer is available + if (!imageTracer) { + console.error('ImageTracer not available, using fallback') + // Fallback to a simple colored rectangle + const fallbackRect = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'rect') + fallbackRect.setAttribute('x', x) + fallbackRect.setAttribute('y', y) + fallbackRect.setAttribute('width', targetSize) + fallbackRect.setAttribute('height', targetSize) + fallbackRect.setAttribute('fill', '#a600ff') + fallbackRect.setAttribute('rx', '8') + fallbackRect.setAttribute('ry', '8') + resolve(fallbackRect) + return + } + + + + // Use a custom bitmap-to-vector conversion instead of ImageTracer + console.log('Using custom bitmap-to-vector conversion...') + + // Create a group for the vectorized image + const group = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'g') + // Scale the image to fit within the target size in the QR coordinate system + const scale = targetSize / Math.max(img.width, img.height) + group.setAttribute('transform', `translate(${x.toFixed(2)}, ${y.toFixed(2)}) scale(${scale.toFixed(4)})`) + + // Sample pixels and create colored rectangles + const sampleSize = Math.max(1, Math.floor(Math.min(img.width, img.height) / 32)) // Sample every N pixels + const data = imageData.data + + for (let y = 0; y < img.height; y += sampleSize) { + for (let x = 0; x < img.width; x += sampleSize) { + const index = (y * img.width + x) * 4 + const r = data[index] + const g = data[index + 1] + const b = data[index + 2] + const a = data[index + 3] + + // Only create rectangles for non-transparent pixels with sufficient opacity + if (a > 128 && (r > 0 || g > 0 || b > 0)) { + const rect = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect.setAttribute('x', x.toFixed(2)) + rect.setAttribute('y', y.toFixed(2)) + rect.setAttribute('width', sampleSize.toFixed(2)) + rect.setAttribute('height', sampleSize.toFixed(2)) + rect.setAttribute('fill', `rgb(${r}, ${g}, ${b})`) + group.appendChild(rect) + } + } + } + + console.log('Custom bitmap-to-vector conversion completed') + resolve(group) + }) + } + + const exportAsSVG = async () => { + if (!text.trim()) { + alert('Please enter some text to generate a QR code') + return + } + + const svgContent = await generateSVGQRCode(text) + if (svgContent) { + // Debug: Log the SVG content to see what's being generated + console.log('Generated SVG content:', svgContent) + + // Create download link + const blob = new Blob([svgContent], { type: 'image/svg+xml' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'qrcode.svg' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + } + const addImageToQRCode = async (qrCodeUrl, imageUrl) => { return new Promise((resolve) => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') // Set canvas size - canvas.width = 256 - canvas.height = 256 + canvas.width = 512 + canvas.height = 512 // Create image objects const qrImage = new Image() @@ -58,32 +289,33 @@ const TextAreaComponent = () => { qrImage.onload = () => { // Draw QR code - ctx.drawImage(qrImage, 0, 0, 256, 256) + ctx.drawImage(qrImage, 0, 0, 512, 512) customImage.onload = () => { // Calculate image size based on user preference - const calculatedImageSize = 256 * (imageSize / 100) - const imageX = (256 - calculatedImageSize) / 2 - const imageY = (256 - calculatedImageSize) / 2 + const calculatedImageSize = 512 * (imageSize / 100) - // Create circular clipping path for the image - ctx.save() - ctx.beginPath() - ctx.arc(imageX + calculatedImageSize/2, imageY + calculatedImageSize/2, calculatedImageSize/2, 0, 2 * Math.PI) - ctx.clip() + // Calculate white box size with margin + const margin = 16 // 8 pixel margin around the image + const boxSize = calculatedImageSize + (margin * 2) + const boxX = (512 - boxSize) / 2 + const boxY = (512 - boxSize) / 2 + + // Draw white background box + ctx.fillStyle = '#FFFFFF' + ctx.fillRect(boxX, boxY, boxSize, boxSize) + + // Calculate image position within the white box + const imageX = boxX + margin + const imageY = boxY + margin // Draw custom image ctx.drawImage(customImage, imageX, imageY, calculatedImageSize, calculatedImageSize) - // Restore context - ctx.restore() - - // Add white border around the image - ctx.strokeStyle = '#FFFFFF' - ctx.lineWidth = 3 - ctx.beginPath() - ctx.arc(imageX + calculatedImageSize/2, imageY + calculatedImageSize/2, calculatedImageSize/2, 0, 2 * Math.PI) - ctx.stroke() + // Add border around the white box + //ctx.strokeStyle = '#000000' + //ctx.lineWidth = 1 + //ctx.strokeRect(boxX, boxY, boxSize, boxSize) // Convert canvas to data URL resolve(canvas.toDataURL('image/png')) @@ -160,6 +392,36 @@ const TextAreaComponent = () => { cols={50} /> +
+
+ + setForegroundColor(e.target.value)} + className="color-input" + /> + {foregroundColor} +
+ +
+ + setBackgroundColor(e.target.value)} + className="color-input" + /> + {backgroundColor} +
+
+