/** * convert gambar ke pdf * base: https://tools.pdf24.org/id/gambar-ke-pdf * Creator: ShanMolvyr * Jangan Hapus Kreator hargai rakyat kecil * Note: support dari direct link image dan image lokal * Sumber: https://whatsapp.com/channel/0029VbB4Kw8EFeXfeExaXc3Q */ const fs = require("fs"); const path = require("path"); const axios = require("axios"); const FormData = require("form-data"); const API = "https://filetools7.pdf24.org/client.php"; const H = { Origin: "https://tools.pdf24.org", Referer: "https://tools.pdf24.org/", "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36", }; const jar = {}; function saveCookies(headers) { const raw = headers["set-cookie"]; if (!raw) return; for (const c of (Array.isArray(raw) ? raw : [raw])) { const [pair] = c.split(";"); const idx = pair.indexOf("="); if (idx < 0) continue; jar[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim(); } } function ch() { const s = Object.entries(jar).map(([k, v]) => `${k}=${v}`).join("; "); return s ? { Cookie: s } : {}; } const isUrl = (s) => /^https?:\/\//i.test(s); const isWebp = (s) => /\.webp$/i.test(isUrl(s) ? new URL(s).pathname : s); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const out = (data) => process.stdout.write(JSON.stringify(data) + "\n"); async function fetchStream(url) { const res = await axios.get(url, { responseType: "stream", timeout: 30000 }); let filename = path.basename(new URL(url).pathname) || "image"; const cd = res.headers["content-disposition"]; if (cd) { const m = cd.match(/filename[^;=\n]*=(["']?)(.+)\1/i); if (m) filename = m[2].trim(); } if (!path.extname(filename)) filename += `.${(res.headers["content-type"] || "").split("/")[1]?.split(";")[0] || "jpg"}`; return { stream: res.data, filename, contentType: res.headers["content-type"] || "image/jpeg" }; } async function upload(source) { const form = new FormData(); if (isUrl(source)) { const { stream, filename, contentType } = await fetchStream(source); form.append("file", stream, { filename, contentType }); } else { if (!fs.existsSync(source)) throw new Error(`File not found: ${source}`); form.append("file", fs.createReadStream(source), { filename: path.basename(source) }); } const res = await axios.post(`${API}?action=upload`, form, { headers: { ...form.getHeaders(), ...H, ...ch(), Accept: "application/json" }, maxBodyLength: Infinity, timeout: 60000, }); saveCookies(res.headers); return Array.isArray(res.data) ? res.data[0] : res.data; } async function pollJob(jobId) { const deadline = Date.now() + 120000; while (Date.now() < deadline) { const res = await axios.post(`${API}?action=getStatus`, new URLSearchParams({ jobId }).toString(), { headers: { ...H, ...ch(), "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", Accept: "*/*" }, timeout: 15000, }); saveCookies(res.headers); if (res.data.status === "done") return res.data.job; if (res.data.status === "error") throw new Error(`job error: ${JSON.stringify(res.data)}`); await sleep(2000); } throw new Error("timeout"); } async function convertToJpg(f) { const res = await axios.post(`${API}?action=convertImageFormat`, { files: [{ file: f.file, size: f.size, ctime: f.ctime, host: f.host, name: f.name }], colorSpace: "RGB", dpi: 150, imageFormat: "jpg", jpgQuality: 95, }, { headers: { ...H, ...ch(), "Content-Type": "application/json;charset=UTF-8", Accept: "*/*" }, timeout: 30000 }); saveCookies(res.headers); const job = await pollJob(res.data.jobId); return { file: job["0.out.file"], size: parseInt(job["0.out.size"] || 0), ctime: f.ctime, host: job["0.out.host"] || f.host, name: f.name.replace(/\.webp$/i, ".jpg") }; } async function imagesToPdf(sources, output = "result.pdf", opts = {}) { if (!sources?.length) throw new Error("no sources"); const files = []; for (const src of sources) { let f = await upload(src); if (isWebp(src)) f = await convertToJpg(f); files.push(f); } const convRes = await axios.post(`${API}?action=imagesToPdf`, { files: files.map((f) => ({ file: f.file, size: f.size, ctime: f.ctime, host: f.host, name: f.name })), rotations: files.map(() => 0), joinFiles: true, createBookmarks: false, pageSize: opts.pageSize ?? "auto", pageOrientation: opts.pageOrientation ?? "auto", margin: opts.margin ?? "0", }, { headers: { ...H, ...ch(), "Content-Type": "application/json;charset=UTF-8", Accept: "*/*" }, timeout: 30000 }); saveCookies(convRes.headers); const job = await pollJob(convRes.data.jobId); const dlRes = await axios.get(`${API}?action=getFile&file=${job["out.file"]}`, { headers: { ...H, ...ch() }, responseType: "arraybuffer", timeout: 60000, }); fs.writeFileSync(output, dlRes.data); return { ok: true, output, size: dlRes.data.byteLength }; } module.exports = { imagesToPdf }; if (require.main === module) { const args = process.argv.slice(2); const sources = [], opts = {}; let output = "result.pdf"; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === "-o") output = args[++i]; else if (a === "--page-size") opts.pageSize = args[++i]; else if (a === "--orientation") opts.pageOrientation = args[++i]; else if (a === "--margin") opts.margin = args[++i]; else if (!a.startsWith("-")) sources.push(a); } imagesToPdf(sources, output, opts) .then(out) .catch((e) => { out({ ok: false, error: e.message }); process.exit(1); }); }