From 8fb1e6a45c7dcd0dbfa71415a57b30cef8fb54e5 Mon Sep 17 00:00:00 2001 From: smiley Date: Tue, 3 Oct 2023 18:34:59 +0200 Subject: [PATCH] :sparkles: SVG conversion in javascript example --- examples/Readme.md | 3 +- examples/SVGConvert.js | 114 +++++++++++++++++++++++++++++++ examples/svgConvertViaCanvas.php | 91 ++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 examples/SVGConvert.js create mode 100644 examples/svgConvertViaCanvas.php diff --git a/examples/Readme.md b/examples/Readme.md index 0ea565de7..2b56dafc2 100644 --- a/examples/Readme.md +++ b/examples/Readme.md @@ -22,7 +22,6 @@ - [GD Image with rounded modules](./imageWithRoundedShapes.php): similar to the SVG "melted" modules example ([#215](https://github.com/chillerlan/php-qrcode/pull/215)) - [ImageMagick with logo](./imagickWithLogo.php): a logo on top of the QR Code - [ImageMagick with image as background](./imagickImageAsBackground.php): an image as full size background of the QR Code -- [ImageMagick SVG to raster conversion](./imagickConvertSVGtoPNG.php): use ImageMagick to convert SVG output to a raster image, e.g. PNG - [SVG with logo](./svgWithLogo.php): an SVG QR Code with embedded logo (that is also SVG) - [SVG with "melted" modules](./svgMeltedModules.php): an effect where the matrix appears to be like melted wax ([#127](https://github.com/chillerlan/php-qrcode/issues/127)) - [SVG with randomly colored modules](./svgRandomColoredDots.php): a visual effect using multiple colors for the matrix modules ([#136](https://github.com/chillerlan/php-qrcode/discussions/136)) @@ -35,6 +34,8 @@ - [Authenticator](./authenticator.php): create a QR Code that displays an URI for a mobile authenticator (featuring [`chillerlan/php-authenticator`](https://github.com/chillerlan/php-authenticator)) - [Interactive output](./qrcode-interactive.php): interactive demo (via [index.html](./index.html)) - [Custom module shapes](./shapes.svg): SVG paths to customize the module shapes ([#150](https://github.com/chillerlan/php-qrcode/discussions/150)) +- [ImageMagick SVG to raster conversion](./imagickConvertSVGtoPNG.php): uses ImageMagick to convert SVG output to a raster image, e.g. PNG ([#216](https://github.com/chillerlan/php-qrcode/discussions/216)) +- [HTML canvas SVG to PNG conversion](./svgConvertViaCanvas.php): converts an SVG element or a data URI fom and image element to a PNG image via the [HTML canvas element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas), includes [a javascript class](./SVGConvert.js) which handles the conversion. Please note that the examples are self-contained, meaning that all custom classes are defined in an example file, so they don't necessarily respect the PSR-4 one file = one class principle. diff --git a/examples/SVGConvert.js b/examples/SVGConvert.js new file mode 100644 index 000000000..fd0a1c7f9 --- /dev/null +++ b/examples/SVGConvert.js @@ -0,0 +1,114 @@ +/** + * @see https://stackoverflow.com/a/28226736 + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL + * + * @created 25.09.2023 + * @author smiley + * @copyright 2023 smiley + * @license MIT + */ + +export default class SVGConvert{ + + /** + * Converts the SVG image from the source element into a raster image with the given size and format + * and inserts the resulting data URI into the src attribute of the given destination image element. + * + * @param {SVGElement|HTMLImageElement} source + * @param {HTMLImageElement} destination + * @param {Number} width + * @param {Number} height + * @param {String} format + * @return {void} + */ + static toDataURI(source, destination, width, height, format){ + // format is whatever the fuck the specification allows: + // @see https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-todataurl-dev + format = (format || 'image/png'); + + if(!destination instanceof HTMLImageElement){ + throw new Error('destination is not an instance of HTMLImageElement'); + } + + if(source instanceof HTMLImageElement){ + source = SVGConvert.base64Decode(source); + } + + SVGConvert.toCanvas(source, width, height) + .then(canvas => destination.src = canvas.toDataURL(format)) + .catch(error => console.log('(╯°□°)╯彡┻━┻ ', error)); + } + + /** + * Draws the given SVG source on a canvas of the given size and returns the canvas element + * + * @param {SVGElement} source + * @param {Number} width + * @param {Number} height + * @return {Promise} + */ + static toCanvas(source, width, height){ + return new Promise((resolve, reject) => { + + if(!source instanceof SVGElement){ + throw new Error('source is not an instance of SVGElement'); + } + + // the source SVG element needs to be visible + source.style.display = 'inline-block'; + source.style.visibility = 'visible'; + + // add/fix the width/height of the SVG element + // @see https://bugzilla.mozilla.org/show_bug.cgi?id=700533#c39 + source.width.baseVal.valueAsString = width; + source.height.baseVal.valueAsString = height; + + try{ + // stringify the SVG element and create an object URL for it + let svgString = new XMLSerializer().serializeToString(source); + let svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); + let tempUrl = URL.createObjectURL(svgBlob); + // create a temporary image + let tempImg = new Image(width, height); + // trigger the onLoad event with the temporary blob URL + tempImg.src = tempUrl; + // process the conversion in the onLoad event + tempImg.onload = function(){ + // create a canvas element to draw the SVG on + let canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.getContext('2d').drawImage(tempImg, 0, 0); + // revoke the temporary blob + URL.revokeObjectURL(tempUrl); + // return the PNG image as data URI + resolve(canvas); + }; + } + catch(error){ + reject(error.message); + } + }); + } + + /** + * Converts the given SVG base64 data URI into a DOM element + * + * @param {HTMLImageElement} image + * @return {SVGElement} + */ + static base64Decode(image){ + + if(!image instanceof HTMLImageElement){ + throw new Error('image is not an instance of HTMLImageElement'); + } + + if(!image.src || !image.src.includes('base64,')){ + throw new Error('invalid image source'); + } + + return new DOMParser().parseFromString(atob(image.src.split(',')[1]), 'image/svg+xml').firstChild; + } + +} diff --git a/examples/svgConvertViaCanvas.php b/examples/svgConvertViaCanvas.php new file mode 100644 index 000000000..8eb618b3b --- /dev/null +++ b/examples/svgConvertViaCanvas.php @@ -0,0 +1,91 @@ + + * @copyright 2023 smiley + * @license MIT + */ + + +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\Output\QROutputInterface; +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\QROptions; + +require_once __DIR__.'/../vendor/autoload.php'; + +$options = new QROptions; + +$options->version = 7; +$options->outputType = QROutputInterface::MARKUP_SVG; +$options->outputBase64 = false; +$options->svgAddXmlHeader = false; +$options->drawLightModules = false; +$options->markupDark = ''; +$options->markupLight = ''; +$options->drawCircularModules = true; +$options->circleRadius = 0.4; +$options->connectPaths = true; + +$options->keepAsSquare = [ + QRMatrix::M_FINDER_DARK, + QRMatrix::M_FINDER_DOT, + QRMatrix::M_ALIGNMENT_DARK, +]; + +$options->svgDefs = ' + + + + + + + + + '; + + +$qrcode = (new QRCode($options))->addByteSegment('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); + +// render the SVG +$svg_raw = $qrcode->render(); +// switch to base64 output +$options->outputBase64 = true; +// render base64 +$svg_base64 = $qrcode->render(); + +// dump the output +header('Content-type: text/html'); + +?> + + + + + + QRCode javascript canvas conversion example + + + +
+ + +
+ +
+ + +
+ +