Files
kickkingsadmin/KK_cardgen_INTEGRATION.md
T
2026-09-14 13:23:14 +00:00

206 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prompt: add KK Card Gen to this Next.js app
Implement the **KK Card Gen** feature inside **this** existing Next.js app. Do not create a second Next app. Do not iframe the old app. Merge it as a route + APIs + libs.
Source of truth if you can read files from disk: `F:\Projects\Node\kk_card_gen`
If you cannot access that folder, reimplement from this spec exactly.
## Product
Personal tool to stamp sequential user IDs onto a local base card image and download a zip of PNGs.
User flow:
1. Pick a base image (PNG/JPG/WebP, max 10MB) from disk or drop it.
2. Configure ID: prefix, suffix, start number, pad length 06, count 11000 (default 1000).
3. Style text: preset font or uploaded TTF/OTF, size, color, bold, italic, left/center/right.
4. Optional drop shadow: color, opacity, blur, offset X/Y.
5. Optional outline: color, opacity, width.
6. Position: drag the sample ID on the preview, click to place, or nudge X/Y %.
7. Click Generate. Server generates PNGs in parallel, then zips them.
8. UI shows live progress: `Generating 12 / 1000` then `Zipping 12 / 1000`.
9. When done, show a **Download ids.zip** link. Do **not** fetch the zip as a blob in JS and auto-download. That hung on the server.
10. Filenames = sanitized ID + `.png` (example `KK0001.png`).
No header/marketing chrome. No helper text like “Drop a card image with an empty ID area…”. Empty preview only needs “Select a base photo”.
## Hosting constraints (must follow)
- Route handlers: `export const runtime = "nodejs"` (never Edge).
- Native module `@napi-rs/canvas` + Node `worker_threads`.
- Host must be a real Node server (VPS / `next start`). Vercel-style serverless is a poor fit.
- `maxDuration = 300` on generate.
- Zip files live on disk under `os.tmpdir()/kk-card-jobs/<uuid>/` for 15 minutes so any Node worker on the same machine can serve the download. Do **not** store jobs in an in-memory `Map`.
## Dependencies
```bash
npm install @napi-rs/canvas archiver
npm install -D @types/archiver
```
`next.config` must include:
```ts
serverExternalPackages: ["@napi-rs/canvas"]
```
Merge with existing config. Do not remove other settings.
## Files to add
Adapt `@/` aliases if this repo uses `src/`. Keep the **worker file name and cwd-relative path** unless you update `generate-pool.ts` to match.
```
app/cards/page.tsx # or another unused route; render <Editor />
app/cards/editor.tsx # "use client" editor UI
app/api/generate/route.ts
app/api/download/[jobId]/route.ts
lib/id-config.ts
lib/draw-id.ts
lib/fonts.ts
lib/progress.ts
lib/jobs.ts
lib/create-zip.ts
lib/generate-pool.ts
lib/register-fonts.ts # optional if only workers register fonts
workers/generate-id.cjs # MUST be a real file on disk at repo root
public/fonts/Inter-Regular.ttf
public/fonts/Inter-Bold.ttf
public/fonts/Roboto-Regular.ttf
public/fonts/Roboto-Bold.ttf
public/fonts/Oswald-Regular.ttf
public/fonts/Oswald-Bold.ttf
public/fonts/Montserrat-Regular.ttf
public/fonts/Montserrat-Bold.ttf
public/fonts/PlayfairDisplay-Regular.ttf
public/fonts/PlayfairDisplay-Bold.ttf
```
If this app already has `app/page.tsx` you care about, **do not overwrite it**. Put the editor on `/cards` (or `/kk-card-gen`).
Copy fonts from `F:\Projects\Node\kk_card_gen\public\fonts` if possible. Otherwise download Inter / Roboto / Oswald / Montserrat / Playfair Display regular+bold TTF (latin 400 + 700) into those exact filenames.
Add matching `@font-face` rules to the apps global CSS so the **browser preview** uses the same files as the server. Families: `Inter`, `Roboto`, `Oswald`, `Montserrat`, `Playfair Display`.
## Shared drawing rules (preview MUST match export)
Both client canvas and worker canvas must use the same logic:
- Position: `x = xPercent/100 * width`, `y = yPercent/100 * height`
- `textBaseline = "alphabetic"`**never** `"middle"` (browser vs Skia disagree; export sat too low)
- Measure `actualBoundingBoxAscent/Descent` (fallback `fontSize * 0.8` / `0.2`)
- Alphabetic Y for visual center: `baselineY = y + (ascent - descent) / 2`
- Font string: `` `${italic?} ${bold?700:400} ${fontSize}px "${fontFamily}"` ``
- Outline: `strokeText` first (`lineJoin: "round"`), then clear shadow, then `fillText`
- Shadow via `shadowColor/Blur/OffsetX/OffsetY` (hex + opacity → rgba)
- Font size is pixels at **source image resolution**. Preview canvas internal size = naturalWidth/Height; CSS scales it. Mouse coords: map from `getBoundingClientRect()` to canvas pixels, then to percents.
ID string:
```ts
`${prefix}${String(n).padStart(pad, "0")}${suffix}`
```
Filename: strip `/ \ : * ? " < > |`, trim, fallback `"id"`.
Limits: count 11000, pad 06, start 01_000_000, fontSize 8400, image 10MB, font 5MB, hex colors `#rgb` / `#rrggbb` / `#rrggbbaa`.
Default config: prefix `KK`, start `1`, pad `4`, count `1000`, font `Inter`, size `48`, color `#111111`, align `center`, position 50/50, shadow/outline off.
Custom uploaded font family name: `CustomUpload` (FontFace in browser; `GlobalFonts.registerFromPath` in workers).
## Worker pool (required for speed)
Do **not** generate 1000 images on the Next.js request thread in a single loop.
`lib/generate-pool.ts`:
- Split tasks across `min(cpu, 8, taskCount)` workers
- Spawn with `createRequire(...).("node:worker_threads").Worker`**do not** `import { Worker } from "node:worker_threads"` then `new Worker(path)`. Turbopack/webpack intercepts `new Worker()` and breaks (`__dirname is not defined` / missing worker module).
- Worker path: `path.join(process.cwd(), "workers", "generate-id.cjs")`
- Each worker gets `{ imagePath, fonts: [{path,family}], style, tasks: [{text,outPath}] }`
- Worker posts `{ type: "progress" }` after each PNG
- Parent aggregates and streams generate progress
`workers/generate-id.cjs` must be **CommonJS** (`require("@napi-rs/canvas")`). ESM workers broke canvas. Worker: register fonts, `loadImage`, one canvas, draw, `encode("png")`, `writeFile`.
## Generate API — `POST /api/generate`
`multipart/form-data`:
- `image` File
- `font` File (only if custom)
- `config` JSON string of `GenerateConfig`
Validation errors: JSON `{ error }` with 4xx.
Success: `Content-Type: application/x-ndjson`, `Cache-Control: no-store`, `X-Accel-Buffering: no`.
Stream one JSON object per line:
```json
{"phase":"generate","current":12,"total":1000}
{"phase":"zip","current":12,"total":1000}
{"phase":"done","id":"<uuid>"}
{"phase":"error","message":"..."}
```
Pipeline:
1. `createJobDir()``{ id, dir, pngDir, zipPath }` under `tmpdir()/kk-card-jobs/<uuid>/`
2. Write base image to disk; write custom font if any
3. Build file list of `{ text, name, path }`
4. `generatePngsInWorkers` + stream generate events
5. Zip with `archiver` `ZipArchive` (`new ZipArchive({ zlib: { level: 6 } })`). `@types/archiver` has no default factory; import `{ ZipArchive } from "archiver"`.
6. Stream zip progress via archiver `progress`
7. `{"phase":"done","id"}` — zip stays on disk. Do not stream the zip in this response.
## Download API — `GET /api/download/[jobId]`
- Validate UUID (prevent path traversal)
- `stat` `ids.zip`; 404 if missing/empty
- Stream file with `Content-Type: application/zip`, `Content-Length`, `Content-Disposition: attachment; filename="ids.zip"`
- Do **not** delete the zip on first download (link should work if they click twice). TTL sweep on new jobs is enough.
- Next 16 params: `const { jobId } = await context.params`
## Editor UI
Client component. Two columns: preview left, controls right (base image, user ID, text style, drop shadow, outline, position, progress, download link, generate button).
Preview: canvas at natural image size, overlay sample ID (`prefix+pad(start)+suffix`), dashed selection box, drag/click-to-place.
Generate:
- POST FormData to `/api/generate`
- Read body as NDJSON (buffer split on `\n`)
- Update progress bar + button label (`Generating 12 / 1000`, `Zipping 12 / 1000`)
- On `done`, set `downloadHref = /api/download/${id}` and show `<a href={...} download="ids.zip">Download ids.zip</a>`
- Never `response.blob()` the zip
Match existing app look if there is a design system. If the app already has Tailwind, reuse it (dark zinc/indigo is fine). If not, add minimal CSS; do not force a full Tailwind install unless the app already has it.
Do not add a marketing header.
## Pitfalls
1. `textBaseline: "middle"` → generated text sits lower than preview.
2. Auto-download via `fetch` + `blob()` → hangs on server. Use a link.
3. In-memory job map → download 404/hang with multiple Node workers. Use disk + UUID.
4. `new Worker()` imported from `worker_threads` → Next bundles it and breaks. Use `createRequire` + `threads.Worker`.
5. Worker must be `.cjs` on disk at `workers/generate-id.cjs` relative to `process.cwd()` (the Next app root).
6. Preview fonts need `@font-face`; server fonts need TTF files under `public/fonts` with the names in `lib/fonts.ts`.
7. Do not put generate/download on Edge.
8. Archiver v8: `import { ZipArchive } from "archiver"` then `new ZipArchive(...)`.
## Verify before done
1. Typecheck / lint the new files.
2. Open the new route in the browser. No leftover header copy.
3. Upload a card, place the ID, generate **12** images. Confirm progress then a working **Download ids.zip** link. Unzip: `KK0001.png``KK0012.png`.
4. Confirm preview position matches a generated PNG (not shifted down).
5. Toggle shadow + outline and generate 1 image; both appear.
6. Confirm `/api/generate` is not Edge.
If `F:\Projects\Node\kk_card_gen` is readable, **copy those files and adapt imports/routes** rather than rewriting from scratch. Preserve worker + draw behavior.