#!/usr/bin/env bun /** * Thai Font Patcher for picture-it * * Detects if Thai font entries are missing from picture-it's FONT_FILES array * in dist/index.js, and patches them if needed. * * This is idempotent — safe to run multiple times. * Run this after every `picture-it` update. * * Usage: * bun thai-font-patch.ts # patch if needed * bun thai-font-patch.ts --check # just check, don't patch * bun thai-font-patch.ts --force # re-patch even if already patched */ import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync } from "fs"; import { join, dirname } from "path"; import { execSync } from "child_process"; const THAI_FONT_FILES_SIGNATURE = "thai/NotoSansThai-Regular.ttf"; const THAI_FONTS_ENTRY = ` }, // Thai fonts { name: "Noto Sans Thai", file: "thai/NotoSansThai-Regular.ttf", weight: 400, style: "normal", url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU5RtpzE.ttf" }, { name: "Noto Sans Thai", file: "thai/NotoSansThai-Bold.ttf", weight: 700, style: "normal", url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU3NqpzE.ttf" }, { name: "Kanit", file: "thai/Kanit-Regular.ttf", weight: 400, style: "normal", url: "https://fonts.gstatic.com/s/kanit/v17/nKKZ-Go6G5tXcoaS.ttf" }, { name: "Kanit", file: "thai/Kanit-Bold.ttf", weight: 700, style: "normal", url: "https://fonts.gstatic.com/s/kanit/v17/nKKU-Go6G5tXcr4uPiWg.ttf" } ];` // Thai font download URLs const THAI_FONTS_TO_DOWNLOAD = [ { name: "NotoSansThai-Regular.ttf", url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU5RtpzE.ttf", }, { name: "NotoSansThai-Bold.ttf", url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU3NqpzE.ttf", }, { name: "Kanit-Regular.ttf", url: "https://fonts.gstatic.com/s/kanit/v17/nKKZ-Go6G5tXcoaS.ttf", }, { name: "Kanit-Bold.ttf", url: "https://fonts.gstatic.com/s/kanit/v17/nKKU-Go6G5tXcr4uPiWg.ttf", }, ]; function findPictureItDist(): string | null { const home = process.env.HOME || ""; const searchPaths = [ // bun-js snap (primary) join(home, "snap/bun-js/87/.bun/install/global/node_modules/picture-it/dist/index.js"), // npm global join(home, ".npm-global/lib/node_modules/picture-it/dist/index.js"), ]; for (const p of searchPaths) { if (existsSync(p)) return p; } return null; } function getPictureItVersion(installDir: string): string { try { const pkgPath = join(installDir, "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); return pkg.version || "unknown"; } catch { return "unknown"; } } function getInstallDir(distPath: string): string { return dirname(dirname(distPath)); } function isAlreadyPatched(distPath: string): boolean { const content = readFileSync(distPath, "utf-8"); return content.includes(THAI_FONT_FILES_SIGNATURE); } async function downloadThaiFonts(fontDir: string): Promise { mkdirSync(fontDir, { recursive: true }); const downloaded: string[] = []; for (const font of THAI_FONTS_TO_DOWNLOAD) { const outPath = join(fontDir, font.name); if (existsSync(outPath)) { const stat = readFileSync(outPath); if (stat.length > 1000) { console.log(` Already exists: ${font.name}`); downloaded.push(font.name); continue; } } console.log(` Downloading: ${font.name}`); const res = await fetch(font.url); if (!res.ok) throw new Error(`Failed to download ${font.name}: ${res.status}`); const buf = await res.arrayBuffer(); writeFileSync(outPath, Buffer.from(buf)); downloaded.push(font.name); } return downloaded; } function patchDist(distPath: string): boolean { const content = readFileSync(distPath, "utf-8"); // Find DM Serif Display closing brace in FONT_FILES array // Pattern: the last entry in FONT_FILES ends with: // url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/..." // } // ]; // We need to replace the closing `}` with Thai fonts entries + ]; const dmSerifEntryEnd = `name: "DM Serif Display", file: "DMSerifDisplay-Regular.ttf", weight: 400, style: "normal", url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/-nFnOHM81r4j6k0gjAW3mujVU2B2K_c.ttf" }`; // Find the closing of the array after DM Serif Display // We look for the pattern: DM Serif Display entry ... }; var cachedFonts // The `];` after the closing `}` of DM Serif Display is the end of FONT_FILES const fontFilesEndPattern = /(\n\s+url: "https:\/\/fonts\.gstatic\.com\/s\/dmserifdisplay\/v17\/[^"]+"\n\s+}\n)\];/; const match = content.match(fontFilesEndPattern); if (!match) { // Try alternate pattern (might differ in minified versions) const altPattern = /(\}\n)\];/; const altMatch = content.match(altPattern); if (!altMatch) { throw new Error("Could not find end of FONT_FILES array. picture-it format may have changed."); } // Just replace the first occurrence of ]; after a } const newContent = content.replace(altPattern, THAI_FONTS_ENTRY.replace(" },", match?.[1] || " }").endsWith("}") ? THAI_FONTS_ENTRY : ` },${THAI_FONTS_ENTRY.split(" },")[1]}`); writeFileSync(distPath, newContent); return true; } const patched = content.replace( fontFilesEndPattern, THAI_FONTS_ENTRY ); if (patched === content) { return false; } writeFileSync(distPath, patched); return true; } async function main() { const args = process.argv.slice(2); const checkOnly = args.includes("--check"); const force = args.includes("--force"); console.log("[Thai Font Patcher for picture-it]"); console.log(""); const distPath = findPictureItDist(); if (!distPath) { console.log("ERROR: picture-it not found."); console.log("Is picture-it installed? Run: bun install -g picture-it"); process.exit(1); } const installDir = getInstallDir(distPath); const version = getPictureItVersion(installDir); console.log(`picture-it found: v${version}`); console.log(`Install dir: ${installDir}`); console.log(`dist path: ${distPath}`); console.log(""); const alreadyPatched = isAlreadyPatched(distPath); if (alreadyPatched && !force) { console.log("Thai fonts: ALREADY PATCHED ✓"); console.log("No action needed."); process.exit(0); } if (checkOnly) { console.log("Thai fonts: NOT PATCHED ✗"); console.log("Run without --check to apply patch."); process.exit(1); } if (force && alreadyPatched) { console.log("Force mode: re-patching..."); } // Step 1: Download Thai fonts console.log("Step 1: Ensuring Thai fonts are downloaded..."); const fontDir = join(process.env.HOME || "", ".picture-it/fonts/thai"); try { await downloadThaiFonts(fontDir); } catch (e: any) { console.error(` Failed to download fonts: ${e.message}`); console.error("You can download them manually or try again."); } console.log(""); // Step 2: Patch dist/index.js console.log("Step 2: Patching dist/index.js..."); try { const success = patchDist(distPath); if (success) { console.log(" Patched dist/index.js ✓"); } else { console.log(" No changes needed (or patch failed silently)"); } } catch (e: any) { console.error(` Patch failed: ${e.message}`); console.error("Please report this issue."); process.exit(1); } console.log(""); // Verify const verifyPatched = isAlreadyPatched(distPath); if (verifyPatched) { console.log("RESULT: Thai font patch applied successfully ✓"); console.log(""); console.log("You can now use Thai fonts:"); console.log(' picture-it text -i input.png --title "ทดสอบ" --font "Noto Sans Thai" -o out.png'); console.log(' picture-it text -i input.png --title "ทดสอบ" --font "Kanit" -o out.png'); } else { console.error("RESULT: Patch verification failed ✗"); console.error("The patch may not have been applied correctly."); process.exit(1); } } main().catch((e: Error) => { console.error(`Error: ${e.message}`); process.exit(1); });