Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@
- Added PDF, PNG, and JPEG template previews with draggable spreadsheet fields.
- Added row previews, field styling, PDF/PNG generation, collision-safe filenames, ZIP packaging, and a data-minimized manifest.
- Added responsive studio UI, explicit privacy boundaries, tests, cross-platform CI, CodeQL, Pages deployment, and release packaging.

## Version 1.2.0: reviewed improvements

Add placement guides, keyboard/numeric positioning, duplicate fields and a cancellable selected-record ZIP queue.

The preview toolbar wraps at 390 pixels. Fields support numeric percentages, arrow-key nudging (0.1%, Shift 1%), page-aware edge/center guides and duplication. XLSX worksheet selection and explicit record ranges control exports, with original worksheet row numbers retained in manifests and preflight. ZIP parts render sequentially, await an explicit download before continuing, and cap buffered document bytes at 32 MB per part with 1–50 records per part. Cancellation discards the current undownloaded part and stops subsequent work; previously downloaded parts remain. Inputs, parser memory, a single document render and compression overhead are outside that output-buffer cap. Large parts fail with guidance to select fewer records. Collision-safe names span the queue. Templates, rows and fonts stay local; a blank sample template supports an entirely local walkthrough. Automated PDF/ZIP tests verify numbering, cancellation and limits; desktop/mobile browser QA exercised numeric placement, duplication and queue cancellation. File-scheme launch, physical print accuracy and every XLSX producer are not claimed.

Validation: `npm run check`, `npm audit`, and `npm run package`.
8 changes: 8 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ Files are held only in page memory. The application deliberately has no persiste
## Verification

Use the exact commands in [TESTING.md](TESTING.md). Claims in the README must not exceed tested behavior. Release artifacts are built from `dist/` by `scripts/package.mjs` and receive a SHA-256 sidecar.

## Version 1.2.0: reviewed improvements

Add placement guides, keyboard/numeric positioning, duplicate fields and a cancellable selected-record ZIP queue.

The preview toolbar wraps at 390 pixels. Fields support numeric percentages, arrow-key nudging (0.1%, Shift 1%), page-aware edge/center guides and duplication. XLSX worksheet selection and explicit record ranges control exports, with original worksheet row numbers retained in manifests and preflight. ZIP parts render sequentially, await an explicit download before continuing, and cap buffered document bytes at 32 MB per part with 1–50 records per part. Cancellation discards the current undownloaded part and stops subsequent work; previously downloaded parts remain. Inputs, parser memory, a single document render and compression overhead are outside that output-buffer cap. Large parts fail with guidance to select fewer records. Collision-safe names span the queue. Templates, rows and fonts stay local; a blank sample template supports an entirely local walkthrough. Automated PDF/ZIP tests verify numbering, cancellation and limits; desktop/mobile browser QA exercised numeric placement, duplication and queue cancellation. File-scheme launch, physical print accuracy and every XLSX producer are not claimed.

Validation: `npm run check`, `npm audit`, and `npm run package`.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

Batch Document Studio turns a PDF or image template and a CSV/XLSX spreadsheet into a ZIP of personalized PDF or PNG documents. It runs entirely in the browser: templates, recipient rows, rendered documents, and ZIP contents stay on the device.

Use it for certificates, award cards, event credentials, badges, name tags, or other one-page documents that repeat a visual design while changing text.
Use it for certificates, award cards, event credentials, badges, name tags, or other personalized documents that repeat a visual design while changing text.

## Three-minute path

1. Open the [browser application](https://loganpendragonmultiverse.github.io/batch-document-studio/) or download the release ZIP and open `index.html` locally.
2. Choose a one-page PDF, PNG, or JPEG template up to 25 MB.
3. Choose a CSV or XLSX spreadsheet. The first row supplies field names.
1. Open the [browser application](https://loganpendragonmultiverse.github.io/batch-document-studio/) or serve the downloaded release ZIP with a local static server.
2. Choose a multi-page PDF, PNG, or JPEG template up to 25 MB.
3. Choose a CSV or XLSX spreadsheet. Select the XLSX worksheet. The first row supplies field names.
4. Add fields, select their spreadsheet columns, choose wrapping or shrink-to-fit, and drag them
onto any template page.
5. Preview multiple records, choose PDF or PNG, set a filename pattern, and select **Build ZIP**.
5. Preview multiple records, choose PDF or PNG, set a filename pattern, and select records and **Build ZIP queue**.

The ZIP contains one file per non-empty spreadsheet row plus a JSON manifest connecting each filename to its original spreadsheet row number.

## What version 1 does

- Reads the first worksheet from CSV and XLSX recipient files.
- Reads CSV and explicitly selected XLSX worksheets.
- Preserves duplicate or blank spreadsheet headers by assigning stable unique names.
- Previews the first page of PDF templates and full PNG/JPEG templates.
- Previews each page of PDF templates and full PNG/JPEG templates.
- Maps any spreadsheet column to draggable text fields with size, color, alignment, and width controls.
- Places fields independently across every page of a PDF template.
- Wraps multiline text, shrinks long text to a chosen minimum size, or flags possible clipping.
Expand Down Expand Up @@ -88,3 +88,11 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a change. For usage que
## License

MIT. See [LICENSE](LICENSE).

## Version 1.2.0: reviewed improvements

Add placement guides, keyboard/numeric positioning, duplicate fields and a cancellable selected-record ZIP queue.

The preview toolbar wraps at 390 pixels. Fields support numeric percentages, arrow-key nudging (0.1%, Shift 1%), page-aware edge/center guides and duplication. XLSX worksheet selection and explicit record ranges control exports, with original worksheet row numbers retained in manifests and preflight. ZIP parts render sequentially, await an explicit download before continuing, and cap buffered document bytes at 32 MB per part with 1–50 records per part. Cancellation discards the current undownloaded part and stops subsequent work; previously downloaded parts remain. Inputs, parser memory, a single document render and compression overhead are outside that output-buffer cap. Large parts fail with guidance to select fewer records. Collision-safe names span the queue. Templates, rows and fonts stay local; a blank sample template supports an entirely local walkthrough. Automated PDF/ZIP tests verify numbering, cancellation and limits; desktop/mobile browser QA exercised numeric placement, duplication and queue cancellation. File-scheme launch, physical print accuracy and every XLSX producer are not claimed.

Validation: `npm run check`, `npm audit`, and `npm run package`.
8 changes: 8 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ Manual release smoke check:

Automated tests do not establish visual print accuracy, legal credential validity, font licensing,
complex-script shaping, mobile precision, or compatibility with every spreadsheet producer.

## Version 1.2.0: reviewed improvements

Add placement guides, keyboard/numeric positioning, duplicate fields and a cancellable selected-record ZIP queue.

The preview toolbar wraps at 390 pixels. Fields support numeric percentages, arrow-key nudging (0.1%, Shift 1%), page-aware edge/center guides and duplication. XLSX worksheet selection and explicit record ranges control exports, with original worksheet row numbers retained in manifests and preflight. ZIP parts render sequentially, await an explicit download before continuing, and cap buffered document bytes at 32 MB per part with 1–50 records per part. Cancellation discards the current undownloaded part and stops subsequent work; previously downloaded parts remain. Inputs, parser memory, a single document render and compression overhead are outside that output-buffer cap. Large parts fail with guidance to select fewer records. Collision-safe names span the queue. Templates, rows and fonts stay local; a blank sample template supports an entirely local walkthrough. Automated PDF/ZIP tests verify numbering, cancellation and limits; desktop/mobile browser QA exercised numeric placement, duplication and queue cancellation. File-scheme launch, physical print accuracy and every XLSX producer are not claimed.

Validation: `npm run check`, `npm audit`, and `npm run package`.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "batch-document-studio",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
10 changes: 5 additions & 5 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ export function preflightBatch(options: {
findings.push({
severity: "error",
code: "invalid-page",
rowNumber: rowIndex + 2,
rowNumber: options.dataset.rowNumbers?.[rowIndex] ?? rowIndex + 2,
fieldId: field.id,
});
if (!value.trim())
findings.push({
severity: "warning",
code: "missing-value",
rowNumber: rowIndex + 2,
rowNumber: options.dataset.rowNumbers?.[rowIndex] ?? rowIndex + 2,
fieldId: field.id,
});
const availableWidth = page ? field.width * page.width : 0;
Expand Down Expand Up @@ -168,22 +168,22 @@ export function preflightBatch(options: {
findings.push({
severity: "warning",
code: "text-overflow",
rowNumber: rowIndex + 2,
rowNumber: options.dataset.rowNumbers?.[rowIndex] ?? rowIndex + 2,
fieldId: field.id,
});
}
if (options.format === "png" && field.pageIndex > 0)
findings.push({
severity: "warning",
code: "png-extra-page",
rowNumber: rowIndex + 2,
rowNumber: options.dataset.rowNumbers?.[rowIndex] ?? rowIndex + 2,
fieldId: field.id,
});
if (field.fontFamily === "custom" && !options.hasCustomFont)
findings.push({
severity: "error",
code: "custom-font-missing",
rowNumber: rowIndex + 2,
rowNumber: options.dataset.rowNumbers?.[rowIndex] ?? rowIndex + 2,
fieldId: field.id,
});
}
Expand Down
56 changes: 56 additions & 0 deletions src/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { clamp } from "./core";
import type { TextField } from "./types";
export function placeField(field: TextField, x: number, y: number): TextField {
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Position must be finite.");
return { ...field, x: clamp(x, 0, 1 - field.width), y: clamp(y, 0, 1 - field.height) };
}
export function alignmentGuides(
field: TextField,
others: TextField[],
): { x: number[]; y: number[] } {
const result = { x: [] as number[], y: [] as number[] };
for (const axis of ["x", "y"] as const) {
const size = axis === "x" ? "width" : "height";
const anchors = [
0,
0.5,
1,
...others
.filter((f) => f.id !== field.id && f.pageIndex === field.pageIndex)
.flatMap((f) => [f[axis], f[axis] + f[size] / 2, f[axis] + f[size]]),
];
result[axis] = [
...new Set(
anchors.filter((a) =>
[field[axis], field[axis] + field[size] / 2, field[axis] + field[size]].some(
(b) => Math.abs(a - b) < 0.006,
),
),
),
];
}
return result;
}
export function selectedDataset<
T extends { rows: Record<string, string>[]; rowNumbers?: number[] },
>(dataset: T, selection: string): T & { rowNumbers: number[] } {
if (!selection.trim()) throw new Error("Select record numbers, for example 1-3,5, or all.");
const indices = new Set<number>();
if (selection.trim().toLowerCase() === "all") dataset.rows.forEach((_, i) => indices.add(i));
else
for (const part of selection.split(",")) {
const match = /^\s*(\d+)(?:\s*-\s*(\d+))?\s*$/.exec(part);
if (!match) throw new Error("Use record numbers such as 1-3,5.");
const first = Number(match[1]),
last = Number(match[2] ?? match[1]);
if (first < 1 || last < first || last > dataset.rows.length)
throw new Error("Record selection is outside this worksheet.");
for (let n = first; n <= last; n++) indices.add(n - 1);
}
const order = [...indices].sort((a, b) => a - b);
return {
...dataset,
rows: order.map((i) => dataset.rows[i]!),
rowNumbers: order.map((i) => dataset.rowNumbers?.[i] ?? i + 2),
};
}
Loading