// server.js import express from "express"; import sharp from "sharp"; import { execFile } from "child_process"; import fs from "fs/promises"; import { randomUUID } from "crypto"; import libheifModule from "libheif-js"; import exifReader from "exif-reader"; const libheif = libheifModule?.default ?? libheifModule; const app = express(); app.use(express.raw({ type: "*/*", limit: "30mb" })); app.get("/health", (_req, res) => res.status(200).send("ok")); /* ------------------------------------------------------------------ */ /* Request context / logging */ /* ------------------------------------------------------------------ */ const DEFAULT_REQ_TIMEOUT_MS = clampInt( process.env.REQ_TIMEOUT_MS, 5_000, 10 * 60_000, 120_000 ); const DEFAULT_REQ_TIMEOUT_PDF_MS = clampInt( process.env.REQ_TIMEOUT_PDF_MS, 10_000, 30 * 60_000, 5 * 60_000 ); app.use((req, res, next) => { const requestId = String(req.headers["x-request-id"] || "").trim() || randomUUID(); req.requestId = requestId; res.setHeader("x-request-id", requestId); const started = Date.now(); req.setTimeout(DEFAULT_REQ_TIMEOUT_MS); res.setTimeout(DEFAULT_REQ_TIMEOUT_MS); res.on("finish", () => { const ms = Date.now() - started; const len = Number(req.headers["content-length"] || 0) || (req.body?.length ?? 0) || 0; console.log( JSON.stringify({ requestId, method: req.method, path: req.originalUrl, status: res.statusCode, bytesIn: len, ms, }) ); }); next(); }); function isAborted(req, res) { return Boolean(req.aborted || res.writableEnded || res.destroyed); } function sendError(res, status, code, message, requestId) { if (res.headersSent) { try { res.end(); } catch {} return; } res.status(status).json({ error: code, message, requestId }); } /* ------------------------------------------------------------------ */ /* Auth */ /* ------------------------------------------------------------------ */ function requireAuth(req, res) { const token = process.env.CONVERTER_TOKEN; const auth = req.headers.authorization || ""; if (!token || auth !== `Bearer ${token}`) { sendError(res, 401, "unauthorized", "Unauthorized", req.requestId); return false; } return true; } /* ------------------------------------------------------------------ */ /* Type detection */ /* ------------------------------------------------------------------ */ function isPdfRequest(req) { const ct = String(req.headers["content-type"] || "").toLowerCase(); const fn = String(req.headers["x-filename"] || "").toLowerCase(); return ct.startsWith("application/pdf") || fn.endsWith(".pdf"); } function looksLikeHeic(buf) { if (!buf || buf.length < 16) return false; if (buf.toString("ascii", 4, 8) !== "ftyp") return false; const brands = buf.toString("ascii", 8, Math.min(buf.length, 256)); return ( brands.includes("heic") || brands.includes("heif") || brands.includes("heix") || brands.includes("hevc") || brands.includes("hevx") || brands.includes("mif1") || brands.includes("msf1") ); } async function assertSupportedRaster(input) { if (looksLikeHeic(input)) return; try { await sharp(input, { failOnError: false }).metadata(); } catch { throw Object.assign(new Error("Unsupported image input"), { statusCode: 415, code: "unsupported_media_type", }); } } /* ------------------------------------------------------------------ */ /* Options */ /* ------------------------------------------------------------------ */ function parseBool(v, fallback = false) { if (v == null) return fallback; const s = String(v).toLowerCase().trim(); if (["1", "true", "yes", "y", "on"].includes(s)) return true; if (["0", "false", "no", "n", "off"].includes(s)) return false; return fallback; } function parseOptions(req) { return { quality: clampInt(req.headers["x-jpeg-quality"], 40, 100, 85), maxDim: clampInt(req.headers["x-max-dimension"], 500, 6000, 2000), withoutEnlargement: parseBool(req.headers["x-without-enlargement"], true), pdfDpi: clampInt(req.headers["x-pdf-dpi"], 72, 600, 300), }; } /* ------------------------------------------------------------------ */ /* Vision-safe normalization */ /* ------------------------------------------------------------------ */ function normalizeForVision(input, opts) { const sharpInputOpts = { failOnError: false, limitInputPixels: 200e6, ...(opts?.raw ? { raw: opts.raw } : {}), }; let pipeline = sharp(input, sharpInputOpts).rotate(); // Normalize into sRGB (NOT "rgb"). This avoids: // vips_colourspace: no known route from 'srgb' to 'rgb' pipeline = pipeline.toColorspace("srgb"); // If input has alpha, flatten to white so OCR/vision doesn't get weird transparency artifacts. pipeline = pipeline.flatten({ background: { r: 255, g: 255, b: 255 } }); if (opts.maxDim) { pipeline = pipeline.resize({ width: opts.maxDim, height: opts.maxDim, fit: "inside", withoutEnlargement: opts.withoutEnlargement, }); } return pipeline .jpeg({ quality: opts.quality, chromaSubsampling: "4:4:4", mozjpeg: true, progressive: true, }) .withMetadata(false) .toBuffer(); } /* ------------------------------------------------------------------ */ /* HEIC via WASM */ /* ------------------------------------------------------------------ */ function heifDisplayToRGBA(img) { return new Promise((resolve, reject) => { try { const w = img.get_width(); const h = img.get_height(); const rgba = new Uint8Array(w * h * 4); img.display({ data: rgba, width: w, height: h, channels: 4 }, () => resolve({ width: w, height: h, rgba }) ); } catch (e) { reject(e); } }); } async function heicToJpeg(input, opts) { if (!libheif?.HeifDecoder) throw new Error("libheif-js unavailable"); const dec = new libheif.HeifDecoder(); const imgs = dec.decode(input); if (!imgs?.length) throw new Error("HEIC decode failed"); const { width, height, rgba } = await heifDisplayToRGBA(imgs[0]); // Feed Sharp raw pixel metadata so it doesn't treat the buffer as an encoded image. return normalizeForVision(Buffer.from(rgba), { ...opts, raw: { width, height, channels: 4 }, }); } /* ------------------------------------------------------------------ */ /* PDF handling */ /* ------------------------------------------------------------------ */ async function pdfFirstPageToJpeg(input, opts) { const id = randomUUID(); const pdf = `/tmp/${id}.pdf`; const out = `/tmp/${id}.jpg`; try { await fs.writeFile(pdf, input); await execFilePromise( "pdftoppm", ["-jpeg", "-singlefile", "-r", String(opts.pdfDpi), pdf, `/tmp/${id}`], DEFAULT_REQ_TIMEOUT_PDF_MS ); const buf = await fs.readFile(out); return normalizeForVision(buf, opts); } finally { await safeUnlink(pdf); await safeUnlink(out); } } /* ------------------------------------------------------------------ */ /* Single-flight per machine (ONLY for /convert) */ /* ------------------------------------------------------------------ */ const MAX_CONVERT_INFLIGHT = 1; let convertInflight = 0; async function withConvertSingleFlight(req, res, fn) { if (convertInflight >= MAX_CONVERT_INFLIGHT) { res.setHeader("Retry-After", "1"); return sendError(res, 429, "busy", "Converter busy; retry shortly", req.requestId); } convertInflight++; try { return await fn(); } finally { convertInflight--; } } /* ------------------------------------------------------------------ */ /* Routes */ /* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */ /* Web UI */ /* ------------------------------------------------------------------ */ app.get("/", (_req, res) => { res.setHeader("Content-Type", "text/html"); res.send(` PostConvert – Strip Metadata `); }); async function extractMetadata(buf) { const items = []; try { const meta = await sharp(buf, { failOnError: false }).metadata(); if (meta.exif) { try { const exif = exifReader(meta.exif); // Scary / identifiable stuff first if (exif.GPSInfo?.GPSLatitude && exif.GPSInfo?.GPSLongitude) { const lat = exif.GPSInfo.GPSLatitude; const lng = exif.GPSInfo.GPSLongitude; const latRef = exif.GPSInfo.GPSLatitudeRef || "N"; const lngRef = exif.GPSInfo.GPSLongitudeRef || "E"; const latVal = typeof lat === "number" ? lat : (lat[0] + lat[1] / 60 + lat[2] / 3600); const lngVal = typeof lng === "number" ? lng : (lng[0] + lng[1] / 60 + lng[2] / 3600); const latFinal = latRef === "S" ? -latVal : latVal; const lngFinal = lngRef === "W" ? -lngVal : lngVal; let loc = `${latFinal.toFixed(4)}, ${lngFinal.toFixed(4)}`; try { const geoRes = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${latFinal}&lon=${lngFinal}&format=json&zoom=14`, { headers: { "User-Agent": "PostConvert/1.0" }, signal: AbortSignal.timeout(3000), }); if (geoRes.ok) { const geo = await geoRes.json(); if (geo.display_name) loc = geo.display_name; } } catch {} items.push(["GPS Location", loc]); items.push(["GPS Coordinates", `${latFinal.toFixed(6)}, ${lngFinal.toFixed(6)}`]); } if (exif.Photo?.DateTimeOriginal) { const d = exif.Photo.DateTimeOriginal; items.push(["Date Taken", d instanceof Date ? d.toISOString().slice(0, 19).replace("T", " ") : String(d)]); } if (exif.Image?.Make && exif.Image?.Model) { items.push(["Camera", `${exif.Image.Make} ${exif.Image.Model}`]); } else if (exif.Image?.Model) { items.push(["Camera", String(exif.Image.Model)]); } if (exif.Image?.HostComputer) items.push(["Device", String(exif.Image.HostComputer)]); if (exif.Photo?.LensModel) items.push(["Lens", String(exif.Photo.LensModel)]); if (exif.Photo?.ImageUniqueID) items.push(["Unique ID", String(exif.Photo.ImageUniqueID)]); // Camera settings const settings = []; if (exif.Photo?.FNumber) settings.push(`f/${Math.round(exif.Photo.FNumber * 100) / 100}`); if (exif.Photo?.ExposureTime) settings.push(`1/${Math.round(1 / exif.Photo.ExposureTime)}s`); if (exif.Photo?.ISOSpeedRatings) settings.push(`ISO ${exif.Photo.ISOSpeedRatings}`); if (exif.Photo?.FocalLength) settings.push(`${Math.round(exif.Photo.FocalLength * 100) / 100}mm`); if (settings.length) items.push(["Settings", settings.join(" ")]); } catch {} } // Technical details at the bottom if (meta.width && meta.height) items.push(["Dimensions", `${meta.width} × ${meta.height}`]); if (meta.hasProfile && meta.icc) items.push(["ICC Profile", `${(meta.icc.length / 1024).toFixed(1)} KB`]); if (meta.exif) items.push(["EXIF Data", `${(meta.exif.length / 1024).toFixed(1)} KB`]); if (meta.xmp) items.push(["XMP Data", `${(meta.xmp.length / 1024).toFixed(1)} KB`]); if (meta.iptc) items.push(["IPTC Data", `${(meta.iptc.length / 1024).toFixed(1)} KB`]); } catch {} // Convert to ordered object const removed = {}; for (const [k, v] of items) removed[k] = v; return removed; } app.post("/strip", async (req, res) => { res.setHeader("Connection", "close"); return withConvertSingleFlight(req, res, async () => { try { if (!requireAuth(req, res)) return; if (!req.body?.length) { return sendError(res, 400, "empty_body", "Empty body", req.requestId); } const opts = { quality: clampInt(req.headers["x-jpeg-quality"], 40, 100, 92), maxDim: 0, withoutEnlargement: true, }; // Extract metadata before stripping const removed = await extractMetadata(req.body); let jpeg; if (looksLikeHeic(req.body)) { if (isAborted(req, res)) return; jpeg = await heicToJpeg(req.body, opts); } else { await assertSupportedRaster(req.body); if (isAborted(req, res)) return; jpeg = await normalizeForVision(req.body, opts); } if (isAborted(req, res)) return; // If client wants JSON, return image + metadata together const wantsJson = String(req.headers["accept"] || "").includes("application/json"); if (wantsJson) { res.setHeader("Content-Type", "application/json"); return res.json({ image: jpeg.toString("base64"), metadata: removed, }); } res.setHeader("Content-Type", "image/jpeg"); return res.send(jpeg); } catch (e) { const status = e?.statusCode || 500; const code = e?.code || "strip_failed"; console.error(JSON.stringify({ requestId: req.requestId, err: String(e?.stack || e) })); return sendError( res, status, code, status === 415 ? "Unsupported media type" : "Metadata strip failed", req.requestId ); } }); }); app.post("/convert", async (req, res) => { res.setHeader("Connection", "close"); return withConvertSingleFlight(req, res, async () => { try { if (!requireAuth(req, res)) return; if (!req.body?.length) { return sendError(res, 400, "empty_body", "Empty body", req.requestId); } const opts = parseOptions(req); if (isPdfRequest(req)) { if (isAborted(req, res)) return; const jpeg = await pdfFirstPageToJpeg(req.body, opts); if (isAborted(req, res)) return; res.setHeader("Content-Type", "image/jpeg"); return res.send(jpeg); } if (looksLikeHeic(req.body)) { if (isAborted(req, res)) return; const jpeg = await heicToJpeg(req.body, opts); if (isAborted(req, res)) return; res.setHeader("Content-Type", "image/jpeg"); return res.send(jpeg); } await assertSupportedRaster(req.body); if (isAborted(req, res)) return; const jpeg = await normalizeForVision(req.body, opts); if (isAborted(req, res)) return; res.setHeader("Content-Type", "image/jpeg"); return res.send(jpeg); } catch (e) { const status = e?.statusCode || 500; const code = e?.code || "conversion_failed"; console.error(JSON.stringify({ requestId: req.requestId, err: String(e?.stack || e) })); return sendError( res, status, code, status === 415 ? "Unsupported media type" : "Conversion failed", req.requestId ); } }); }); /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function execFilePromise(cmd, args, timeoutMs) { return new Promise((resolve, reject) => { execFile(cmd, args, { timeout: timeoutMs }, (err, _stdout, stderr) => { if (err) { if (err.code === "ENOENT") return reject(new Error(`Missing dependency: ${cmd}`)); if (err.killed || err.signal === "SIGTERM") { return reject(new Error(`${cmd} timed out after ${timeoutMs}ms`)); } return reject(new Error(stderr || String(err))); } resolve(); }); }); } function clampInt(v, min, max, fallback) { const n = Number(v); if (!Number.isFinite(n)) return fallback; return Math.max(min, Math.min(max, Math.floor(n))); } async function safeUnlink(p) { try { await fs.unlink(p); } catch {} } /* ------------------------------------------------------------------ */ const port = Number(process.env.PORT) || 8080; const server = app.listen(port, "0.0.0.0", () => console.log(`converter listening on :${port}`) ); server.keepAliveTimeout = 5_000; server.headersTimeout = 10_000;