Skip to content

Commit d2767c5

Browse files
authored
Multi pages support (#7)
1 parent 0a2af33 commit d2767c5

File tree

7 files changed

+95
-75
lines changed

7 files changed

+95
-75
lines changed

.github/workflows/publish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Install Node, NPM and Yarn
2121
uses: actions/setup-node@v1
2222
with:
23-
node-version: 15
23+
node-version: 14
2424

2525
- name: Get yarn cache directory path
2626
id: yarn-cache-dir-path

.github/workflows/test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Test
22

3-
on: [push, pull_request]
3+
on: [pull_request]
44

55
jobs:
66
release:
@@ -17,7 +17,7 @@ jobs:
1717
- name: Install Node.js, NPM and Yarn
1818
uses: actions/setup-node@v1
1919
with:
20-
node-version: 15
20+
node-version: 14
2121

2222
- name: yarn install
2323
run: |

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
## Features
1111

1212
- Drag & drop fields to customize PDF template
13-
- Support `.xlsx`, `.xls`, `.ods` Excel files
14-
- Support PDF forms
13+
- Support native PDF forms
14+
- Edit multiple-pages PDFs
15+
- Support `.xlsx`, `.xls` and `.ods` Excel files
16+
- Cross-platform: macOS, Windows, Linux
1517
- Work offline
16-
- Cross-platform
1718

1819
## Demo
1920

src/components/pdf/PdfEditor.tsx

+53-22
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ interface MyTextbox extends Textbox {
3333
}
3434
export interface CanvasObjects {
3535
objects: [MyTextbox | Rect];
36+
clientWidth: number;
3637
}
3738
export interface RenderPdfState {
3839
pdfFile: string;
3940
excelFile: string;
4041
combinePdf: boolean;
4142
pageNumber: number;
42-
canvasData?: CanvasObjects;
43-
canvasWidth?: number;
43+
canvasData?: Record<number, CanvasObjects>;
4444
formData?: Record<string, number>;
4545
}
4646

@@ -151,22 +151,34 @@ const PdfEditor = () => {
151151
};
152152

153153
const getCurrentState = (): RenderPdfState => {
154+
let canvasData = currentState?.canvasData;
155+
if (parentRef.current) {
156+
canvasData = {
157+
...canvasData,
158+
[pageNumber]: {
159+
...editor?.dump(),
160+
clientWidth: parentRef.current.clientWidth,
161+
},
162+
};
163+
}
164+
165+
const formData = formFields.reduce(
166+
(p, c) => ({ ...p, [c.name]: c.index }),
167+
{}
168+
);
169+
154170
return {
155171
pdfFile,
156172
pageNumber,
157173
excelFile,
158174
combinePdf,
159-
formData: formFields.reduce((p, c) => ({ ...p, [c.name]: c.index }), {}),
160-
161-
// FIXME: Better way to keep canvas state?
162-
canvasData: parentRef.current ? editor?.dump() : currentState?.canvasData,
163-
canvasWidth: parentRef.current
164-
? parentRef.current?.clientWidth
165-
: currentState?.canvasWidth,
175+
formData,
176+
canvasData,
166177
};
167178
};
168179

169180
const handleRender = async (action: string) => {
181+
setProgressTotal(pages);
170182
await ipcRenderer.invoke(action, getCurrentState());
171183
};
172184

@@ -184,6 +196,18 @@ const PdfEditor = () => {
184196
setPageLoaded(true);
185197
};
186198

199+
const handleClickNext = () => {
200+
setCurrentState(getCurrentState());
201+
setShowCanvas(false);
202+
setPageNumber(pageNumber + 1);
203+
};
204+
205+
const handleClickBack = () => {
206+
setCurrentState(getCurrentState());
207+
setShowCanvas(false);
208+
setPageNumber(pageNumber - 1);
209+
};
210+
187211
const handlePageLoadSuccess = () => {
188212
setShowCanvas(true);
189213
};
@@ -259,15 +283,6 @@ const PdfEditor = () => {
259283
}
260284
};
261285

262-
ipcRenderer.on('keydown', (_event, key) => {
263-
handleKeyDown(key);
264-
});
265-
266-
ipcRenderer.on('render-progress', (_event, p) => {
267-
setProgressPage(p.page);
268-
setProgressTotal(p.total);
269-
});
270-
271286
const handleChangeFormField = (e: any, fld: FieldType) => {
272287
setFormFields(
273288
formFields.map((f) =>
@@ -321,8 +336,13 @@ const PdfEditor = () => {
321336
}, [currentState]);
322337

323338
useEffect(() => {
324-
if (editor && currentState && currentState.canvasData) {
325-
editor.load(currentState.canvasData);
339+
if (
340+
editor &&
341+
currentState &&
342+
currentState.canvasData &&
343+
currentState.canvasData[pageNumber]
344+
) {
345+
editor.load(currentState.canvasData[pageNumber]);
326346
}
327347
}, [editor]);
328348

@@ -369,6 +389,17 @@ const PdfEditor = () => {
369389
}
370390
}, [searchField]);
371391

392+
useEffect(() => {
393+
ipcRenderer.on('keydown', (_event, key) => {
394+
handleKeyDown(key);
395+
});
396+
397+
ipcRenderer.on('render-progress', (_event, p) => {
398+
setProgressPage(p.page);
399+
setProgressTotal(p.total);
400+
});
401+
}, []);
402+
372403
return (
373404
<div className="flex flex-1">
374405
<section className="flex flex-col flex-shrink-0 p-4 space-y-4 bg-gray-200 w-60">
@@ -691,7 +722,7 @@ const PdfEditor = () => {
691722
{pageNumber > 1 ? (
692723
<button
693724
type="button"
694-
onClick={() => setPageNumber(pageNumber - 1)}
725+
onClick={handleClickBack}
695726
className="btn-link"
696727
>
697728
&lt; Back
@@ -705,7 +736,7 @@ const PdfEditor = () => {
705736
{pageNumber < numPages ? (
706737
<button
707738
type="button"
708-
onClick={() => setPageNumber(pageNumber + 1)}
739+
onClick={handleClickNext}
709740
className="btn-link"
710741
>
711742
Next &gt;

src/main.dev.ts

+7-23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
/* eslint global-require: off, no-console: off */
23

34
/**
@@ -195,15 +196,7 @@ const savePdf = async (params: RenderPdfState) => {
195196
console.error(e);
196197
}
197198

198-
const {
199-
pdfFile,
200-
pageNumber,
201-
excelFile,
202-
combinePdf,
203-
canvasData,
204-
canvasWidth,
205-
formData,
206-
} = params;
199+
const { pdfFile, excelFile, combinePdf, canvasData, formData } = params;
207200

208201
const file = await dialog.showSaveDialog({
209202
filters: [{ name: 'PDF Files', extensions: ['pdf'] }],
@@ -215,12 +208,10 @@ const savePdf = async (params: RenderPdfState) => {
215208
const created = await renderPdf(
216209
file.filePath,
217210
pdfFile,
218-
pageNumber - 1,
219211
excelFile,
220212
getRowsLimit(),
221213
combinePdf,
222214
canvasData,
223-
canvasWidth,
224215
formData,
225216
(o) => mainWindow?.webContents.send('render-progress', o)
226217
);
@@ -264,32 +255,25 @@ const previewPdf = async (params: RenderPdfState) => {
264255
console.error(e);
265256
}
266257

267-
const {
268-
pdfFile,
269-
pageNumber,
270-
excelFile,
271-
combinePdf,
272-
canvasData,
273-
canvasWidth,
274-
formData,
275-
} = params;
258+
const { pdfFile, excelFile, combinePdf, canvasData, formData } = params;
276259

277260
try {
278261
const filePath = path.join(
279262
app.getPath('temp'),
280263
`preview-${path.basename(pdfFile)}`
281264
);
265+
const updateProgress = (o: any) =>
266+
mainWindow?.webContents.send('render-progress', o);
267+
282268
await renderPdf(
283269
filePath,
284270
pdfFile,
285-
pageNumber - 1,
286271
excelFile,
287272
1,
288273
combinePdf,
289274
canvasData,
290-
canvasWidth,
291275
formData || {},
292-
(o) => mainWindow?.webContents.send('render-progress', o)
276+
updateProgress
293277
);
294278
openPdf(filePath);
295279
} catch (e) {

src/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "plainmerge",
33
"productName": "PlainMerge",
4-
"version": "0.0.13",
4+
"version": "0.0.14",
55
"description": "Offline PDF mail merger",
66
"main": "./main.prod.js",
77
"author": {

src/render.ts

+27-23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
/* eslint-disable @typescript-eslint/no-loop-func */
23
/* eslint-disable no-await-in-loop */
34
import { Rect, Textbox } from 'fabric/fabric-impl';
@@ -29,21 +30,21 @@ interface MyTextbox extends Textbox {
2930
}
3031
export interface CanvasObjects {
3132
objects: [MyTextbox | Rect];
33+
clientWidth: number;
3234
}
3335

3436
export interface RenderPdfState {
3537
pdfFile: string;
3638
excelFile: string;
3739
combinePdf: boolean;
38-
pageNumber: number;
39-
canvasData?: CanvasObjects;
40-
canvasWidth?: number;
40+
canvasData?: CanvasMap;
4141
formData?: FormMap;
4242
}
4343

4444
type FontMap = Record<string, PDFFont>;
4545
type RowMap = Record<number, string>;
4646
type FormMap = Record<string, number>;
47+
type CanvasMap = Record<number, CanvasObjects>;
4748

4849
function hexToRgb(hex: string) {
4950
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -193,15 +194,14 @@ const renderPage = async (
193194
page: PDFPage,
194195
pdfDoc: PDFDocument,
195196
cachedFonts: FontMap,
196-
canvasData?: CanvasObjects,
197-
canvasWidth?: number
197+
canvasData?: CanvasObjects
198198
) => {
199-
if (!canvasData || !canvasWidth) {
199+
if (!canvasData) {
200200
return;
201201
}
202202

203203
const { width, height } = page.getSize();
204-
const ratio = width / canvasWidth;
204+
const ratio = width / canvasData.clientWidth;
205205

206206
for (let i = 0; i < canvasData.objects.length; i += 1) {
207207
const obj = canvasData.objects[i];
@@ -278,12 +278,10 @@ export const loadForm = async (filename: string) => {
278278
const renderPdf = async (
279279
output: string,
280280
pdfFile: string,
281-
pageIndex: number,
282281
excelFile: string,
283282
rowsLimit: number,
284283
combinePdf: boolean,
285-
canvasData?: CanvasObjects,
286-
canvasWidth?: number,
284+
canvasData?: CanvasMap,
287285
formData?: FormMap,
288286
updateProgress?: (o: any) => void
289287
) => {
@@ -294,22 +292,28 @@ const renderPdf = async (
294292

295293
const rows: RowMap[] = readFirstSheet(excelFile, rowsLimit);
296294
for (let i = 0; i < rows.length; i += 1) {
297-
// Render all with form
295+
// Step 1: Render pages with form
298296
renderForm(rows[i], formData, pdfDoc.getForm());
299297

300-
// Render 1 page with canvas for now
301-
// TODO: support multiple
302-
const page = pdfDoc.getPage(pageIndex);
303-
await renderPage(
304-
rows[i],
305-
page,
306-
pdfDoc,
307-
cachedFonts,
308-
canvasData,
309-
canvasWidth
310-
);
298+
// Step 2: Render pages with canvas for now
299+
if (canvasData) {
300+
await Promise.all(
301+
pdfDoc.getPageIndices().map(async (pageIndex) => {
302+
if (canvasData[pageIndex + 1]) {
303+
const page = pdfDoc.getPage(pageIndex);
304+
await renderPage(
305+
rows[i],
306+
page,
307+
pdfDoc,
308+
cachedFonts,
309+
canvasData[pageIndex + 1]
310+
);
311+
}
312+
})
313+
);
314+
}
311315

312-
// Copy to new pdf, load and save will remove fields, but retain value
316+
// Step 3: Copy to new pdf, load and save will remove fields, but retain value
313317
const newPages = await newDoc.copyPages(
314318
await PDFDocument.load(await pdfDoc.save()),
315319
pdfDoc.getPageIndices()

0 commit comments

Comments
 (0)