Last active
June 10, 2018 01:20
-
-
Save zpao/9e5ebcec2f13986d9001baf09ca78840 to your computer and use it in GitHub Desktop.
Making QRCodes with SVG smaller (using React)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This will generate 31329 <rect>s for a level 40 QR Code (177x177). | |
// This approach is totally fine in <canvas> (but should learn from Smarter impl below) | |
// Lorem Ipsum test content was ~2MB | |
class QRCodeSVG extends React.Component<Props> { | |
render() { | |
var {value, size, level, bgColor, fgColor} = this.props; | |
var qrcode = new QRCodeImpl(-1, ErrorCorrectLevel[level]); | |
qrcode.addData(value); | |
qrcode.make(); | |
var cells = qrcode.modules; | |
if (cells === null) { | |
return; | |
} | |
const modules = []; | |
cells.forEach(function(row, y) { | |
row.forEach(function(cell, x) { | |
modules.push( | |
<rect | |
key={`${x}-${y}`} | |
fill={cell ? fgColor : bgColor} | |
x={x} | |
y={y} | |
width={1} | |
height={1} | |
/> | |
); | |
}); | |
}); | |
return ( | |
<svg | |
shapeRendering="crispEdges" | |
height={size} | |
width={size} | |
viewBox={`0 0 ${cells.length} ${cells.length}`}> | |
{modules} | |
</svg> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Slightly smarter - single <rect> for bgcolor, then only draw dark modules | |
// Lorem Ipsum test content was unsurprisingly ~50% smaller. | |
class QRCodeSVG extends React.Component<Props> { | |
render() { | |
var {value, size, level, bgColor, fgColor} = this.props; | |
var qrcode = new QRCodeImpl(-1, ErrorCorrectLevel[level]); | |
qrcode.addData(value); | |
qrcode.make(); | |
var cells = qrcode.modules; | |
if (cells === null) { | |
return; | |
} | |
const modules = []; | |
cells.forEach(function(row, y) { | |
row.forEach(function(cell, x) { | |
if (cell) { | |
modules.push( | |
<rect | |
key={`${x}-${y}`} | |
fill={fgColor} | |
x={x} | |
y={y} | |
width={1} | |
height={1} | |
/> | |
); | |
} | |
}); | |
}); | |
return ( | |
<svg | |
shapeRendering="crispEdges" | |
height={size} | |
width={size} | |
viewBox={`0 0 ${cells.length} ${cells.length}`}> | |
<rect | |
fill={bgColor} | |
x={0} | |
y={0} | |
width={cells.length} | |
height={cells.length} | |
/> | |
{modules} | |
</svg> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Way smarter. Still a single node for the background with dark modules on top. | |
// But instead of a rect per module, we can make this into a <path> with appropriate | |
// description. We only draw Nx1 rects within the path, across each row so it could be | |
// smarter still but requires way more work. | |
// Lorem Ipsum test content was 115KB with a total of 2 nodes. | |
// This component updates instantly when value changes, while Naive took a couple seconds. | |
class QRCodeSVG extends React.Component<Props> { | |
render() { | |
var {value, size, level, bgColor, fgColor} = this.props; | |
var qrcode = new QRCodeImpl(-1, ErrorCorrectLevel[level]); | |
qrcode.addData(value); | |
qrcode.make(); | |
var cells = qrcode.modules; | |
if (cells === null) { | |
return; | |
} | |
// Drawing strategy: instead of a rect per module, we're going to create a | |
// single path for the dark modules and layer that on top of a light rect, | |
// for a total of 2 DOM nodes. We pay a bit more in string concat but that's | |
// way faster than DOM ops. | |
// For level 1, 441 nodes -> 2 | |
// For level 40, 31329 -> 2 | |
const ops = []; | |
cells.forEach(function(row, y) { | |
let lastIsDark = false; | |
let start = null; | |
row.forEach(function(cell, x) { | |
if (!cell && start !== null) { | |
// M0 0h7v1H0z injects the space with the move and dropd the comma, | |
// saving a char per operation | |
ops.push(`M${start} ${y}h${x - start}v1H${start}z`); | |
start = null; | |
return; | |
} | |
// end of row, clean up or skip | |
if (x === row.length - 1) { | |
if (!cell) { | |
// We would have closed the op above already so this can only mean | |
// 2+ light modules in a row. | |
return; | |
} | |
if (start === null) { | |
// Just a single dark module. | |
ops.push(`M${x},${y} h1v1H${x}z`); | |
} else { | |
// Otherwise finish the current line. | |
ops.push(`M${start},${y} h${x + 1 - start}v1H${start}z`); | |
} | |
return; | |
} | |
if (cell && start === null) { | |
start = x; | |
} | |
}); | |
}); | |
return ( | |
<svg | |
shapeRendering="crispEdges" | |
height={size} | |
width={size} | |
viewBox={`0 0 ${cells.length} ${cells.length}`}> | |
<path fill={bgColor} d={`M0,0 h${cells.length}v${cells.length}H0z`} /> | |
<path fill={fgColor} d={ops.join('')} /> | |
</svg> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment