Skip to content

Commit 07f101a

Browse files
authored
Support QR code rendering (#13)
* Support qrcode type * Upgrade fabric * Handle type properly
1 parent c18cd0d commit 07f101a

File tree

11 files changed

+280
-3426
lines changed

11 files changed

+280
-3426
lines changed

.tool-versions

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
nodejs 14.17.5
1+
nodejs 14.18.1

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ You got 100 different PDFs for each employee now.
2323
- [x] Drag & drop fields to customize PDF template
2424
- [x] Support native PDF forms
2525
- [x] Support multiple-pages PDFs
26+
- [x] Support rendering QR code
2627
- [x] Support `.xlsx`, `.xls` and `.ods` Excel files
2728
- [x] Send out emails with PDF attachments
2829
- [x] Cross-platform: macOS, Windows, Linux

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,11 @@
168168
"@types/diff": "^5.0.1",
169169
"@types/enzyme": "^3.10.5",
170170
"@types/enzyme-adapter-react-16": "^1.0.6",
171-
"@types/fabric": "^4.5.1",
171+
"@types/fabric": "^4.5.7",
172172
"@types/history": "4.7.6",
173173
"@types/jest": "^26.0.15",
174174
"@types/node": "14.14.10",
175+
"@types/qrcode": "^1.4.2",
175176
"@types/qs": "^6.9.7",
176177
"@types/react": "^16.9.44",
177178
"@types/react-color": "^3.0.5",
@@ -199,7 +200,7 @@
199200
"css-loader": "^5.0.1",
200201
"css-minimizer-webpack-plugin": "^1.1.5",
201202
"detect-port": "^1.3.0",
202-
"electron": "^11.0.1",
203+
"electron": "^11.5.0",
203204
"electron-builder": "^22.10.5",
204205
"electron-devtools-installer": "^3.1.1",
205206
"electron-notarize": "^1.0.0",
@@ -265,6 +266,7 @@
265266
"jsqr": "^1.4.0",
266267
"nodemailer": "^6.6.3",
267268
"pdf-lib": "^1.16.0",
269+
"qrcode": "^1.5.0",
268270
"qs": "^6.10.1",
269271
"react": "^17.0.1",
270272
"react-color": "^2.19.3",

src/components/fabric/editor.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { fabric } from 'fabric';
33
import { ITextboxOptions, Textbox } from 'fabric/fabric-impl';
44

55
const TextOptions: ITextboxOptions = {
6-
type: 'text',
6+
type: 'textbox',
77
left: 100,
88
top: 100,
99
fontSize: 16,
@@ -22,12 +22,26 @@ const TextOptions: ITextboxOptions = {
2222
padding: 1,
2323
};
2424

25+
const props = [
26+
'lockScalingY',
27+
'lockSkewingX',
28+
'lockSkewingY',
29+
'lockRotation',
30+
'lockScalingFlip',
31+
'lockUniScaling',
32+
];
33+
34+
export interface Fieldbox extends Textbox {
35+
index: number;
36+
renderType: string;
37+
}
38+
2539
export interface FabricJSEditor {
2640
canvas: fabric.Canvas;
2741
dump: () => any;
2842
load: (data: any) => void;
2943
addText: (text: string, extraOptions?: ITextboxOptions) => void;
30-
updateText: (extraOptions?: Partial<Textbox>) => void;
44+
updateText: (extraOptions?: Partial<Fieldbox>) => void;
3145
deleteAll: () => void;
3246
deleteSelected: () => void;
3347
}
@@ -38,8 +52,9 @@ const buildEditor = (canvas: fabric.Canvas): FabricJSEditor => {
3852
dump: () => {
3953
return {
4054
objects: canvas.getObjects().map((o) => {
41-
const out = o.toJSON();
55+
const out = o.toJSON(props);
4256
out.index = o.data && parseInt(o.data.index, 10);
57+
out.renderType = o.data?.renderType || 'text';
4358
return out;
4459
}),
4560
};
@@ -48,7 +63,15 @@ const buildEditor = (canvas: fabric.Canvas): FabricJSEditor => {
4863
canvas.loadFromJSON(data, () => {});
4964
canvas.getObjects().forEach((o, i) => {
5065
if (data.objects[i].index !== undefined) {
51-
o.data = { index: data.objects[i].index };
66+
o.data = {
67+
index: data.objects[i].index,
68+
renderType: data.objects[i].renderType,
69+
};
70+
71+
// Handle legacy text type
72+
if (o.type === 'text') {
73+
o.type = 'textbox';
74+
}
5275
}
5376
});
5477
canvas.renderAll();
@@ -61,11 +84,15 @@ const buildEditor = (canvas: fabric.Canvas): FabricJSEditor => {
6184
object.set({ text });
6285
canvas.add(object);
6386
},
64-
updateText: (extraOptions?: Partial<Textbox>) => {
87+
updateText: (extraOptions?: Partial<Fieldbox>) => {
6588
const objects: any[] = canvas.getActiveObjects();
66-
if (objects.length && objects[0].type === 'text') {
67-
const textObject: fabric.Textbox = objects[0];
89+
if (objects.length && objects[0].type.includes('text')) {
90+
const textObject: Fieldbox = objects[0];
6891
if (extraOptions) {
92+
extraOptions.data = {
93+
...textObject.data,
94+
renderType: extraOptions.renderType,
95+
};
6996
textObject.set(extraOptions);
7097
canvas.renderAll();
7198
}

src/components/pdf/PdfEditor.tsx

+42-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ipcRenderer } from 'electron';
99
// @ts-ignore
1010
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
1111
import { StandardFonts, StandardFontValues } from 'pdf-lib';
12-
import { Rect, Textbox } from 'fabric/fabric-impl';
12+
import { Rect } from 'fabric/fabric-impl';
1313
import { TwitterPicker } from 'react-color';
1414
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1515
import { SizeMe } from 'react-sizeme';
@@ -18,6 +18,7 @@ import { useLocation } from 'react-router-dom';
1818
import { IconName } from '@fortawesome/fontawesome-svg-core';
1919
import { FabricJSCanvas, useFabricJSEditor } from '../fabric/Canvas';
2020
import { readExcelMeta } from '../utils/excel';
21+
import { Fieldbox } from '../fabric/editor';
2122

2223
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
2324
export interface DataHeader {
@@ -27,11 +28,8 @@ export interface DataHeader {
2728

2829
type Align = 'left' | 'center' | 'right';
2930

30-
interface MyTextbox extends Textbox {
31-
index: number;
32-
}
3331
export interface CanvasObjects {
34-
objects: [MyTextbox | Rect];
32+
objects: [Fieldbox | Rect];
3533
clientWidth: number;
3634
}
3735
export interface RenderPdfState {
@@ -66,6 +64,7 @@ const PdfEditor = () => {
6664
const [fill, setFill] = useState('#000');
6765
const [showPicker, setShowPicker] = useState(false);
6866
const [align, setAlign] = useState<Align>('left');
67+
const [renderType, setRenderType] = useState('text');
6968

7069
const [pdfFile, setPdfFile] = useState('');
7170
const [excelFile, setExcelFile] = useState('');
@@ -213,6 +212,10 @@ const PdfEditor = () => {
213212
}));
214213

215214
const fontSizes = [8, 10, 12, 14, 16, 18, 24, 30, 36, 48, 60];
215+
const renderTypes = [
216+
{ value: 'text', label: 'Text' },
217+
{ value: 'qrcode', label: 'QR code' },
218+
];
216219

217220
const handleKeyDown = (key: string) => {
218221
if (selectedObject) {
@@ -258,17 +261,19 @@ const PdfEditor = () => {
258261
fontSize,
259262
fill,
260263
textAlign: align as string,
264+
renderType,
261265
});
262266

263267
setCurrentState(getCurrentState());
264-
}, [fontFamily, fontSize, fill, align]);
268+
}, [fontFamily, fontSize, fill, align, renderType]);
265269

266270
useEffect(() => {
267-
const text = selectedObject as Textbox;
271+
const text = selectedObject as Fieldbox;
268272
setFontFamily(text?.fontFamily || 'Helvetica');
269273
setFontSize(text?.fontSize || 16);
270274
setFill((text?.fill as string) || '#000');
271275
setAlign((text?.textAlign as Align) || 'left');
276+
setRenderType(text?.renderType || 'text');
272277
}, [selectedObject]);
273278

274279
useEffect(() => {
@@ -292,7 +297,7 @@ const PdfEditor = () => {
292297
}
293298

294299
setCombinePdf(currentState.combinePdf);
295-
ipcRenderer.invoke('save-config', currentState);
300+
ipcRenderer.invoke('save-config', getCurrentState());
296301
}
297302
}, [currentState]);
298303

@@ -460,10 +465,23 @@ const PdfEditor = () => {
460465
selectedObject && !formLayout ? '' : 'opacity-50 cursor-default'
461466
}`}
462467
>
468+
<select
469+
className="w-24"
470+
onChange={(e) => setRenderType(e.target.value)}
471+
value={renderType}
472+
disabled={!selectedObject}
473+
>
474+
{renderTypes.map((r) => (
475+
<option value={r.value} key={r.value}>
476+
{r.label}
477+
</option>
478+
))}
479+
</select>
480+
463481
<select
464482
onChange={(e) => setFontFamily(e.target.value)}
465483
value={fontFamily}
466-
disabled={!selectedObject}
484+
disabled={!selectedObject || renderType !== 'text'}
467485
>
468486
{fonts.map(({ label, value }) => (
469487
<option value={value} key={value}>
@@ -478,7 +496,7 @@ const PdfEditor = () => {
478496
setFontSize(parseInt(e.target.value, 10) || 16)
479497
}
480498
value={fontSize}
481-
disabled={!selectedObject}
499+
disabled={!selectedObject || renderType !== 'text'}
482500
>
483501
{fontSizes.map((v) => (
484502
<option value={v} key={v}>
@@ -487,7 +505,11 @@ const PdfEditor = () => {
487505
))}
488506
</select>
489507

490-
<section className="flex items-center justify-center border-t border-b rounded-sm">
508+
<section
509+
className={`flex items-center justify-center border-t border-b rounded-sm ${
510+
renderType !== 'text' ? 'opacity-50' : ''
511+
}`}
512+
>
491513
{['left', 'center', 'right'].map((al) => (
492514
<button
493515
type="button"
@@ -505,12 +527,18 @@ const PdfEditor = () => {
505527
</section>
506528

507529
<div
508-
className="p-2 border-2 border-white rounded shadow outline-none w-7 h-7 focus:outline-none focus:ring-2 focus:ring-blue-500"
530+
className={`p-2 border-2 border-white rounded shadow outline-none w-7 h-7 focus:outline-none focus:ring-2 focus:ring-blue-500 ${
531+
renderType !== 'text' ? 'opacity-50 cursor-default' : ''
532+
}`}
509533
style={{ backgroundColor: fill }}
510-
onClick={() => selectedObject && setShowPicker(true)}
534+
onClick={() =>
535+
selectedObject && renderType === 'text' && setShowPicker(true)
536+
}
511537
role="button"
512538
aria-labelledby="pick"
513-
onKeyPress={() => selectedObject && setShowPicker(true)}
539+
onKeyPress={() =>
540+
selectedObject && renderType === 'text' && setShowPicker(true)
541+
}
514542
tabIndex={0}
515543
/>
516544
{showPicker ? (

src/main.dev.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const store = new Store();
4242
const writeFile = promisify(fs.writeFile);
4343
const readFile = promisify(fs.readFile);
4444

45-
const configSuffix = '.merge.json';
45+
const configSuffix = 'merge.json';
4646

4747
const getRowsLimit = () => {
4848
if (process.env.PAID) {

0 commit comments

Comments
 (0)