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

9.5 KiB
Raw Permalink Blame History

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

npm install @napi-rs/canvas archiver
npm install -D @types/archiver

next.config must include:

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:

`${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").Workerdo 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:

{"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.pngKK0012.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.