qreator 9.8.2 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,20 @@
4
4
 
5
5
  QR Code generator for browser and node.js with tree shaking and logo support
6
6
 
7
- ![image](https://github.com/Short-io/qreator/assets/75169/02b84738-56f2-44d8-8d11-f40e263302ed)
7
+ <table>
8
+ <tr>
9
+ <td align="center"><img src="showcase/classic.png" width="180" /><br /><b>Classic</b></td>
10
+ <td align="center"><img src="showcase/rounded.png" width="180" /><br /><b>Rounded</b></td>
11
+ <td align="center"><img src="showcase/drops.png" width="180" /><br /><b>Custom finders</b></td>
12
+ <td align="center"><img src="showcase/logo.png" width="180" /><br /><b>Logo overlay</b></td>
13
+ </tr>
14
+ <tr>
15
+ <td align="center"><img src="showcase/label-below.png" width="180" /><br /><b>Label: below</b></td>
16
+ <td align="center"><img src="showcase/label-pill.png" width="180" /><br /><b>Label: pill</b></td>
17
+ <td align="center"><img src="showcase/label-box.png" width="180" /><br /><b>Label: box</b></td>
18
+ <td align="center"><img src="showcase/branded.png" width="180" /><br /><b>Branded</b></td>
19
+ </tr>
20
+ </table>
8
21
 
9
22
  ## Overview
10
23
 
@@ -16,6 +29,8 @@ QR Code generator for browser and node.js with tree shaking and logo support
16
29
  - supports border-radius
17
30
  - supports corner mode (merged rounded corners)
18
31
  - supports finder pattern customization (shape + color)
32
+ - supports text labels (below, pill, box styles)
33
+ - optional React component
19
34
  - tree shaking support
20
35
  - browser / node.js
21
36
 
@@ -49,6 +64,15 @@ const pngBuffer = await getPNG("I love QR", {
49
64
  });
50
65
  ```
51
66
 
67
+ ### Label
68
+
69
+ ```javascript
70
+ const png = await getPNG("https://example.com", {
71
+ labelText: "SCAN ME",
72
+ labelStyle: "pill", // "below", "pill", or "box"
73
+ });
74
+ ```
75
+
52
76
  ### Finder pattern customization
53
77
 
54
78
  ```javascript
@@ -61,6 +85,16 @@ const svgString = await getSVG("I love QR", {
61
85
  });
62
86
  ```
63
87
 
88
+ ### React
89
+
90
+ ```jsx
91
+ import { QR } from "qreator/lib/react";
92
+
93
+ function App() {
94
+ return <QR text="https://example.com" labelText="SCAN ME" labelStyle="pill" />;
95
+ }
96
+ ```
97
+
64
98
  [More examples](./examples)
65
99
 
66
100
  ### Syntax
@@ -94,6 +128,12 @@ const svgString = await getSVG("I love QR", {
94
128
  | `finderInnerShape` | shape of the inner dot of finder patterns | string | `square`, `rounded`, `circle`, `drop` | `undefined` |
95
129
  | `finderColor` | color of finder patterns (overrides `color`) | number/string | same as `color` | `undefined` |
96
130
  | `noExcavate` | don't remove partially covered modules | boolean | `true`, `false` | `false` |
131
+ | `labelText` | text label to display below QR | string | - | `undefined` |
132
+ | `labelStyle` | label presentation style | string | `below`, `pill`, `box` | `below` |
133
+ | `labelColor` | label text color | number/string | same as `color` | `color` (below) / `bgColor` (pill/box) |
134
+ | `labelBgColor` | label background color (pill/box only) | number/string | same as `color` | same as `color` |
135
+ | `labelFontSize`| font size as multiple of module size | number | `1` - n | `5` |
136
+ | `labelFontFamily`| font family | string | - | `sans-serif` |
97
137
 
98
138
 
99
139
  ## Benchmarks
@@ -26828,6 +26828,12 @@
26828
26828
  const defaults = type === "png" ? BITMAP_OPTIONS : VECTOR_OPTIONS;
26829
26829
  return { ...defaults, ...inOptions };
26830
26830
  }
26831
+ function colorToHex(color) {
26832
+ if (typeof color === "string") {
26833
+ return colorString.to.hex(colorString.get.rgb(color));
26834
+ }
26835
+ return `#${(color >>> 8).toString(16).padStart(6, "0")}`;
26836
+ }
26831
26837
  const svgMove = (left, top) => ['M', left, top];
26832
26838
  const svgReturn = () => ['z'];
26833
26839
  const svgDeltaArc = (borderRadius, dx, dy, sweep = 0) => borderRadius > 0 ? ['a', borderRadius, borderRadius, 0, 0, sweep, dx, dy] : [];
@@ -26991,6 +26997,83 @@
26991
26997
  margin: 1,
26992
26998
  size: 0,
26993
26999
  };
27000
+ function computeLabelLayout(options, qrSizePx, marginPx, moduleSize) {
27001
+ if (!options.labelText)
27002
+ return null;
27003
+ const style = options.labelStyle ?? "below";
27004
+ const fontSize = (options.labelFontSize ?? 5) * moduleSize;
27005
+ const fontFamily = options.labelFontFamily ?? "sans-serif";
27006
+ const fgColor = colorToHex(options.color ?? 0x000000ff);
27007
+ const bgColorHex = colorToHex(options.bgColor ?? 0xffffffff);
27008
+ const textColor = options.labelColor
27009
+ ? colorToHex(options.labelColor)
27010
+ : style === "below" ? fgColor : bgColorHex;
27011
+ const labelBgColor = options.labelBgColor
27012
+ ? colorToHex(options.labelBgColor)
27013
+ : fgColor;
27014
+ if (style === "below") {
27015
+ const stripHeight = fontSize * 2.5;
27016
+ return {
27017
+ totalWidth: qrSizePx,
27018
+ totalHeight: qrSizePx + stripHeight,
27019
+ qrSize: qrSizePx,
27020
+ label: {
27021
+ text: options.labelText,
27022
+ x: qrSizePx / 2,
27023
+ y: qrSizePx + stripHeight / 2,
27024
+ width: qrSizePx,
27025
+ height: stripHeight,
27026
+ fontSize,
27027
+ fontFamily,
27028
+ textColor,
27029
+ bgColor: null,
27030
+ borderRadius: 0,
27031
+ },
27032
+ };
27033
+ }
27034
+ if (style === "pill") {
27035
+ const pillHeight = fontSize * 2.2;
27036
+ const estimatedTextWidth = options.labelText.length * fontSize * 0.7;
27037
+ const pillWidth = Math.min(Math.max(estimatedTextWidth + fontSize * 2, qrSizePx * 0.3), qrSizePx * 0.95);
27038
+ const stripHeight = pillHeight + fontSize * 0.6;
27039
+ return {
27040
+ totalWidth: qrSizePx,
27041
+ totalHeight: qrSizePx + stripHeight,
27042
+ qrSize: qrSizePx,
27043
+ label: {
27044
+ text: options.labelText,
27045
+ x: qrSizePx / 2,
27046
+ y: qrSizePx + stripHeight / 2,
27047
+ width: pillWidth,
27048
+ height: pillHeight,
27049
+ fontSize,
27050
+ fontFamily,
27051
+ textColor,
27052
+ bgColor: labelBgColor,
27053
+ borderRadius: pillHeight / 2,
27054
+ },
27055
+ };
27056
+ }
27057
+ const boxHeight = fontSize * 2.2;
27058
+ const stripHeight = boxHeight + fontSize * 0.4;
27059
+ return {
27060
+ totalWidth: qrSizePx,
27061
+ totalHeight: qrSizePx + stripHeight,
27062
+ qrSize: qrSizePx,
27063
+ label: {
27064
+ text: options.labelText,
27065
+ x: qrSizePx / 2,
27066
+ y: qrSizePx + stripHeight / 2,
27067
+ width: qrSizePx,
27068
+ height: boxHeight,
27069
+ fontSize,
27070
+ fontFamily,
27071
+ textColor,
27072
+ bgColor: labelBgColor,
27073
+ borderRadius: 0,
27074
+ },
27075
+ };
27076
+ }
26994
27077
 
26995
27078
  function zeroFillFinders(matrix) {
26996
27079
  matrix = structuredClone(matrix);
@@ -27038,7 +27121,11 @@
27038
27121
  if (options.logo && options.logoWidth && options.logoHeight && !options.noExcavate) {
27039
27122
  matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
27040
27123
  }
27041
- return PDF({ matrix, ...options });
27124
+ const pdfSize = 9;
27125
+ const marginPx = options.margin * pdfSize;
27126
+ const imageSizePx = matrix.length * pdfSize + 2 * marginPx;
27127
+ const layout = computeLabelLayout(options, imageSizePx, marginPx, pdfSize);
27128
+ return PDF({ matrix, ...options, labelLayout: layout });
27042
27129
  }
27043
27130
  function colorToRGB(color) {
27044
27131
  if (typeof color === "string") {
@@ -27079,15 +27166,20 @@
27079
27166
  page.getContentStream = page.prevGetContentStream;
27080
27167
  page.contentStream.push = page.contentStream.prevPush;
27081
27168
  }
27082
- async function PDF({ matrix, margin, logo, logoWidth, logoHeight, color, bgColor, borderRadius, cornerMode, finderOuterShape, finderInnerShape, finderColor, }) {
27169
+ async function PDF({ matrix, margin, logo, logoWidth, logoHeight, color, bgColor, borderRadius, cornerMode, finderOuterShape, finderInnerShape, finderColor, labelLayout, }) {
27083
27170
  const size = 9;
27084
27171
  const marginPx = margin * size;
27085
27172
  const matrixSizePx = matrix.length * size;
27086
27173
  const imageSizePx = matrixSizePx + 2 * marginPx;
27174
+ const totalWidth = labelLayout?.totalWidth ?? imageSizePx;
27175
+ const totalHeight = labelLayout?.totalHeight ?? imageSizePx;
27087
27176
  const document = await PDFDocument.create();
27088
- const page = document.addPage([imageSizePx, imageSizePx]);
27089
- page.drawSquare({
27090
- size: imageSizePx,
27177
+ const page = document.addPage([totalWidth, totalHeight]);
27178
+ page.drawRectangle({
27179
+ x: 0,
27180
+ y: 0,
27181
+ width: totalWidth,
27182
+ height: totalHeight,
27091
27183
  color: rgb(...colorToRGB(bgColor)),
27092
27184
  });
27093
27185
  page.moveTo(0, page.getHeight());
@@ -27133,8 +27225,50 @@
27133
27225
  height: logoHeightPx,
27134
27226
  });
27135
27227
  }
27228
+ if (labelLayout) {
27229
+ await drawPDFLabel(document, page, labelLayout, totalHeight);
27230
+ }
27136
27231
  return document.save();
27137
27232
  }
27233
+ async function drawPDFLabel(document, page, layout, totalHeight) {
27234
+ const { label } = layout;
27235
+ const font = await document.embedFont(StandardFonts.Helvetica);
27236
+ const pdfLabelCenterY = totalHeight - label.y;
27237
+ if (label.bgColor) {
27238
+ const [r, g, b] = colorToRGB(label.bgColor);
27239
+ const rectX = label.x - label.width / 2;
27240
+ const rectY = pdfLabelCenterY - label.height / 2;
27241
+ if (label.borderRadius > 0) {
27242
+ const w = label.width;
27243
+ const h = label.height;
27244
+ const rad = Math.min(label.borderRadius, w / 2, h / 2);
27245
+ const pillPath = `M ${rectX + rad} 0 h ${w - 2 * rad} a ${rad} ${rad} 0 0 1 ${rad} ${-rad} v ${-(h - 2 * rad)} a ${rad} ${rad} 0 0 1 ${-rad} ${-rad} h ${-(w - 2 * rad)} a ${rad} ${rad} 0 0 1 ${-rad} ${rad} v ${h - 2 * rad} a ${rad} ${rad} 0 0 1 ${rad} ${rad} z`;
27246
+ page.moveTo(0, rectY + label.height);
27247
+ page.drawSvgPath(pillPath, {
27248
+ color: rgb(r, g, b),
27249
+ });
27250
+ page.moveTo(0, page.getHeight());
27251
+ }
27252
+ else {
27253
+ page.drawRectangle({
27254
+ x: rectX,
27255
+ y: rectY,
27256
+ width: label.width,
27257
+ height: label.height,
27258
+ color: rgb(r, g, b),
27259
+ });
27260
+ }
27261
+ }
27262
+ const textWidth = font.widthOfTextAtSize(label.text, label.fontSize);
27263
+ const [tr, tg, tb] = colorToRGB(label.textColor);
27264
+ page.drawText(label.text, {
27265
+ x: label.x - textWidth / 2,
27266
+ y: pdfLabelCenterY - label.fontSize * 0.35,
27267
+ size: label.fontSize,
27268
+ font,
27269
+ color: rgb(tr, tg, tb),
27270
+ });
27271
+ }
27138
27272
 
27139
27273
  exports.getPDF = getPDF;
27140
27274