const __dirname = import.meta.dirname; import { promises as fsp, constants } from 'node:fs'; import path, { parse } from 'node:path'; import util from 'node:util'; import { mime } from './pdf-document-maker.js'; import got, { Response } from 'got'; import { PDFDocument, PageSizes, degrees } from 'pdf-lib'; import { formatConfig, rotateConfig } from './options.js'; const loadPDFfromFile = async (inputFN: string | URL) => { // About using fsp.readFile with a URL. // // That works for file://`pwd`/guide/PDF/guide.pdf // a.k.a. file:///home/david/Projects/akasharender/pdf-document-construction-set/guide/PDF/guide.pdf // // That fails for https://akashacms.github.io/pdf-document-construction-set/guide/guide.pdf // Because the URL must have scheme file // // Hence - HTTP URLs must be retrieved with Got // console.log(`loadPDFfromFile ${inputFN}`); let u : string | URL; try { if (inputFN instanceof URL) { u = inputFN; } else { u = new URL(inputFN); } } catch (err) { // console.log(`loadPDFFromFile loading ${inputFN} failed because ${err.message}`); u = inputFN; } let inpBytes; if (typeof u === 'string' || (u instanceof URL && u.protocol === 'file:') ) { // For a regular local file, just read it inpBytes = await fsp.readFile(u); } else if (u instanceof URL && ( u.protocol === 'http:' || u.protocol === 'https:' )) { // For a file retrieved by HTTP(S), retrieve // it using Got const promise = got.get(u, { responseType: 'buffer', followRedirect: true }); const resp = await promise; if (!( resp.complete && resp.statusCode === 200 && resp.statusMessage === 'OK' )) { throw new Error(`loadPDFFromFile FAIL ${resp.statusMessage} for ${u} ${util.inspect({ ok: resp.ok, complete: resp.complete, statusCode: resp.statusCode, // status: resp.status, statusMessage: resp.statusMessage, headers: resp.headers, contentType: resp.headers['content-type'], url: resp.url })}`); } if (resp.headers['content-type'] !== 'application/pdf') { throw new Error(`loadPDFFromFile ${u.toString()} did not correspond to a PDF, but ${resp.headers['content-type']}`); } inpBytes = (await promise.buffer()).buffer; } const donor = await PDFDocument.load(inpBytes); return donor; }; const copyPageToPDF = async (pdfDoc, donor, pagenm, options: formatConfig & rotateConfig) => { const copied = await pdfDoc.copyPages(donor, [pagenm]); // console.log(`copyPage ${pagenm} ${size}`); // const pg = await pdfDoc.addPage(copied[0]); // Handle resizing the page. The `size` value is a string // to be used for indexing PageSizes which then returns // an array useful with page.setSize() // // Turns out that setSize only resizes the canvas, and to resize // the content we must use scaleContent. const toadd = copied[0]; if (typeof options.format !== 'undefined') { if (typeof options.format === 'string') { const origsz = toadd.getSize(); if (!(options.format in PageSizes)) { throw new Error(`Incorrect page size name ${util.inspect(options.format)}`); } const sz = PageSizes[options.format]; // console.log(`orig ${util.inspect(origsz)} setSize ${size} ${util.inspect(sz)}`); toadd.setSize(sz[0], sz[1]); toadd.scaleContent(sz[0] / origsz.width, sz[1] / origsz.height); // pg.setSize(sz[0], sz[1]); // await pdfDoc.addPage(copied[0]).setSize(sz[0], sz[1]); } } if (typeof options.rotate !== 'undefined') { if (typeof options.rotate === 'number') { if (options.rotate === 90) { await toadd.setRotation(degrees(90)); } else if (options.rotate === 180) { await toadd.setRotation(degrees(180)); } else if (options.rotate === 270) { await toadd.setRotation(degrees(270)); } else { throw new Error(`Incorrect rotation in ${util.inspect(options.rotate)}`); } } } // console.log(`Adding page ${util.inspect(toadd.getSize())} ${util.inspect(toadd.getRotation())}`); const pg = await pdfDoc.addPage(toadd); }; const savePDFtoFile = async (pdfDoc, outputFN) => { // console.log(`savePDFtoFile ${outputFN}`); const pdfBytes = await pdfDoc.save(); await fsp.writeFile(outputFN, pdfBytes); }; export async function showPDFinfo(inputFN: string) { // console.log(`page-count ${util.inspect(inputFN)}`); console.log(`PDF Information for ${inputFN}`); console.log(`MIME: ${mime.getType(inputFN)}`); const donor = await loadPDFfromFile(inputFN); console.log(`Title: ${donor.getTitle()}`); console.log(`Subject: ${donor.getSubject()}`); console.log(`Author: ${donor.getAuthor()}`); console.log(`Keywords: ${donor.getKeywords()}`); console.log(`Producer: ${donor.getProducer()}`); console.log(`Creator: ${donor.getCreator()}`); console.log(`Producer: ${donor.getProducer()}`); console.log(`CreationDate: ${donor.getCreationDate()}`); console.log(`ModDate: ${donor.getModificationDate()}`); console.log(`PageCount: ${donor.getPageCount()}`); } export async function showPageInformation(inputFN: string) { console.log(`PDF page information for ${inputFN}`); const pdfDoc = await loadPDFfromFile(inputFN); const totalPages = pdfDoc.getPageCount(); for (let pn = 0; pn < totalPages; pn++) { // console.log(`... COPY ${pn}`); const copied = await pdfDoc.copyPages(pdfDoc, [pn]); console.log({ pageNum: pn, artBox: copied[0].getArtBox(), bleedBox: copied[0].getBleedBox(), cropBox: copied[0].getCropBox(), mediaBox: copied[0].getMediaBox(), trimBox: copied[0].getTrimBox(), position: copied[0].getPosition(), size: copied[0].getSize(), rotation: copied[0].getRotation() }); } } export function showPageSizes() { for (const key in PageSizes) { console.log(`${key}: ${util.inspect(PageSizes[key])}`); } } export async function copyPDFMetadata(inputFN: string, donorFN: string, outputFN: string) { const input = await loadPDFfromFile(inputFN); const donor = await loadPDFfromFile(donorFN); // For each metadata value // Test donor metadata - if set, then set in input const dotitle = donor.getTitle(); if (typeof dotitle === 'string') { input.setTitle(dotitle); } const dosubject = donor.getSubject(); if (typeof dosubject === 'string') { input.setSubject(dosubject); } const doauthor = donor.getAuthor(); if (typeof doauthor === 'string') { input.setAuthor(doauthor); } const dokeywords = donor.getKeywords(); if (Array.isArray(dokeywords)) { input.setKeywords(dokeywords); } const docreator = donor.getCreator(); if (typeof docreator === 'string') { input.setCreator(docreator); } const docdate = donor.getCreationDate(); if (typeof docdate === 'object') { input.setCreationDate(docdate); } const domdate = donor.getModificationDate(); if (typeof domdate === 'object') { input.setModificationDate(domdate); } await savePDFtoFile(input, typeof outputFN === 'string' ? outputFN : inputFN); } export async function setPDFMetadata( pdfFN: string, saveToFN: string, options: { title?: string, subject?: string, author?: string, keyword?: string | Array, producer?: string, creator?: string, creationDate?: string, modificationDate?: string } ) { const donor = await loadPDFfromFile(pdfFN); if (typeof options.title === 'string') { donor.setTitle(options.title); } if (typeof options.subject === 'string') { donor.setSubject(options.subject); } if (typeof options.author === 'string') { donor.setAuthor(options.author); } if (typeof options.keyword === 'string') { donor.setKeywords([ options.keyword ]); } if (Array.isArray(options.keyword)) { donor.setKeywords(options.keyword); } if (typeof options.producer === 'string') { donor.setProducer(options.producer); } if (typeof options.creator === 'string') { donor.setCreator(options.creator); } if (typeof options.creationDate === 'string') { donor.setCreationDate(new Date(options.creationDate)); } if (typeof options.modificationDate === 'string') { donor.setModificationDate(new Date(options.modificationDate)); } await savePDFtoFile(donor, typeof saveToFN === 'string' ? saveToFN : pdfFN); } export async function reformatPDF( inputFN: string, outputFN: string, options: formatConfig & rotateConfig ) { const pdfDoc = await PDFDocument.create(); const donor = await loadPDFfromFile(inputFN); const totalPages = donor.getPageCount(); for (let pn = 0; pn < totalPages; pn++) { // console.log(`... COPY ${pn}`); await copyPageToPDF(pdfDoc, donor, pn, options); } await savePDFtoFile(pdfDoc, typeof outputFN === 'string' ? outputFN : inputFN); } export async function exportPagesFromPDF( inputFN: string, outputFN: string, pages?: Array, options?: formatConfig & rotateConfig ) { const pdfDoc = await PDFDocument.create(); const donor = await loadPDFfromFile(inputFN); // console.log(inputFN); // console.log(outputFN); // console.log(pages); // console.log(options); // console.log(options.pageFormat); for (const pnum of pages) { await copyPageToPDF( pdfDoc, donor, Number.parseInt(pnum), options); } await savePDFtoFile(pdfDoc, outputFN); } // This is test code // // export async function getFromURL( // url: string, // outputFN: string // ) { // const u = new URL(url); // let inpMime; // let inpBytes; // if (u instanceof URL) { // if (u.protocol === 'file:') { // inpMime = mime.getType(u.pathname); // inpBytes = await fsp.readFile(u); // } else if (u.protocol === 'http:' // || u.protocol === 'https:' // ) { // const promise = got.get(u, { // responseType: 'buffer', // followRedirect: true // }); // const resp = await promise; // console.log(`${util.inspect({ // ok: resp.ok, // complete: resp.complete, // statusCode: resp.statusCode, // statusMessage: resp.statusMessage, // headers: resp.headers, // contentType: resp.headers['content-type'], // url: resp.url // })}`); // inpBytes = Buffer.from((await promise.buffer()).buffer); // console.log(util.inspect(inpBytes)); // } // } // await fsp.writeFile(outputFN, inpBytes); // } export async function mergePDFsAndImages( files: Array, options: formatConfig & rotateConfig & { output?: string } ) { console.log(`mergePDFsAndImages ${util.inspect(files)} ${util.inspect(options)}`); const pdfDoc = await PDFDocument.create(); for (const filenm of files) { // test the filenm to see if it's file:// // or fails parsing as a URL. In either case, it will // perform this code here. // // If the filenm is a URL that is http:// or https:// // then it needs to be a different setup. // if file: or bad URL // readFile into a Buffer // From the file name, determine MIME type // else if http: or https: // use got to read the buffer // Does got provide a MIME type? It should // // For application/pdf // get donor from the buffer // Proceed as below // For image/jpeg or image/png // Proceed as below // let donor; let inpBytes; let inpMime; let u; try { u = new URL(filenm); } catch (err) { u = filenm; } if (typeof u === 'string' || ( u instanceof URL && u.protocol === 'file:' ) ) { inpMime = mime.getType(typeof u === 'string' ? u : u.pathname ); inpBytes = await fsp.readFile(typeof u === 'string' ? u : u.pathname ); } else if (u instanceof URL && ( u.protocol === 'http:' || u.protocol === 'https:' ) ) { const promise = got.get(u, { responseType: 'buffer', followRedirect: true }); const resp = await promise; console.log(`${util.inspect({ ok: resp.ok, complete: resp.complete, statusCode: resp.statusCode, statusMessage: resp.statusMessage, headers: resp.headers, contentType: resp.headers['content-type'], url: resp.url })}`); inpMime = resp.headers['content-type']; inpBytes = Buffer.from((await promise.buffer()).buffer); } else { throw new Error(`mergePDFsAndImages ${filenm} not a valid input file. Must be either local file, file:///, http://, or https://`); } if (inpMime === 'application/pdf') { donor = await PDFDocument.load(inpBytes); const totalPages = donor.getPageCount(); for (let pn = 0; pn < totalPages; pn++) { console.log(`... COPY ${pn}`); await copyPageToPDF(pdfDoc, donor, pn, options); } } else if (inpMime === 'image/jpeg' || inpMime === 'image/png') { let img; if (inpMime === 'image/jpeg') { img = await pdfDoc.embedJpg(inpBytes); } if (inpMime === 'image/png') { img = await pdfDoc.embedPng(inpBytes); } if (img) { const page = pdfDoc.addPage(); if (typeof options.format === 'string') { const sz = PageSizes[options.format]; page.setSize(sz[0], sz[1]); } // TBD: Handle rotation const scaled = img.scaleToFit( page.getWidth(), page.getHeight() ); page.drawImage(img, { x: page.getWidth() / 2 - scaled.width / 2, y: page.getHeight() / 2 - scaled.height / 2, width: scaled.width, height: scaled.height, }); // pdfDoc.addPage(page); } } else { throw new Error(`mergePDFsAndImages invalid MIME type ${inpMime} for ${filenm}`); } // const inpMime = mime.getType(filenm); // console.log(`input file name ${filenm} MIME ${inpMime}`); // if (inpMime === 'application/pdf') { // console.log(` // let img;Reading input PDF ${filenm} ${inpMime}`); // const donor = await loadPDFfromFile(filenm); // const totalPages = donor.getPageCount(); // for (let pn = 0; pn < totalPages; pn++) { // console.log(`... COPY ${pn}`); // await copyPageToPDF(pdfDoc, donor, pn, options); // } // } // else if (inpMime === 'image/jpeg' // || inpMime === 'image/png' // ) { // console.log(`Reading input IMAGE ${filenm} ${inpMime}`); // const bytes = await fsp.readFile(filenm); // let img; // if (inpMime === 'image/jpeg') { // img = await pdfDoc.embedJpg(bytes); // } // if (inpMime === 'image/png') { // img = await pdfDoc.embedPng(bytes); // } // if (img) { // const page = pdfDoc.addPage(); // if (typeof options.format === 'string') { // const sz = PageSizes[options.format]; // page.setSize(sz[0], sz[1]); // } // // TBD: Handle rotation // const scaled = img.scaleToFit( // page.getWidth(), // page.getHeight() // ); // page.drawImage(img, { // x: page.getWidth() / 2 - scaled.width / 2, // y: page.getHeight() / 2 - scaled.height / 2, // width: scaled.width, // height: scaled.height, // }); // // pdfDoc.addPage(page); // } // } // else { // console.log(`UNKNOWN FILE TYPE ${filenm}`); // } } console.log(`SAVING ...`); await savePDFtoFile(pdfDoc, options.output); }