Last active
May 23, 2018 19:25
-
-
Save m1m1s1ku/2988e59d9c2b6ca42330d96796aa01ed to your computer and use it in GitHub Desktop.
flatten form fields hummus (formFiller new gen)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// tslint:disable:interface-name | |
import * as hummus from 'hummus'; | |
import * as fs from 'fs'; | |
import * as path from 'path'; | |
import logger from '../helper/logger'; | |
import * as util from 'util'; | |
const writeFile = util.promisify(fs.writeFile); | |
const unlink = util.promisify(fs.unlink); | |
export interface FillerOptions { | |
font?: string, | |
size: number; | |
colorspace?: string; | |
color?: number; | |
underline?: boolean; | |
} | |
interface Field { | |
name: string; | |
llx: number; | |
lly: number; | |
width: number; | |
height: number; | |
text: string; | |
} | |
interface HummusFontObject { | |
calculateTextDimensions(text: string): any; | |
} | |
interface HummusContentContext { | |
writeText(text: string, llx: number, lly: number, options?: any): void | |
drawImage(llx: number, lly: number, imagePath: string, options: any): void | |
} | |
/** | |
* Fill a form with data | |
* | |
* @export | |
* @param {string} template template path | |
* @param {string} output output path | |
* @param {{[key: string]: string}} data map to write { fieldName0: fieldValue0, fieldName1: fieldValue1 } | |
* @param {FillerOptions} options filler options | |
*/ | |
export async function fillForm(template: string, output: string, | |
data: {[key: string]: string}, options: FillerOptions) { | |
const tempImages = []; | |
// read our template | |
const reader = hummus.createReader(template); | |
// open writer | |
const pdfWriter = hummus.createWriter(output, {log: path.join(__dirname, 'filler.log')}); | |
const opts = { | |
font: options.font ? pdfWriter.getFontForFile(options.font) : | |
pdfWriter.getFontForFile(path.join(__dirname, 'opensanssemi.ttf')), | |
size: options.size ? options.size : 7, | |
colorspace: options.colorspace ? options.colorspace : 'gray', | |
color: options.color ? options.color : 0x00, | |
underline: options.underline ? options.underline : false | |
}; | |
const currentFont = opts.font; | |
// grab page count | |
const pageCount = reader.getPagesCount(); | |
// foreach page | |
for (let i = 0; i < pageCount; i++) { | |
// create | |
const page = pdfWriter.createPage(0, 0 , 595, 842); | |
// open context | |
const cxt = pdfWriter.startPageContentContext(page) as HummusContentContext; | |
// get form fields in this page | |
const fields = getFormFieldsPosition(reader, i); | |
// start writing flow | |
cxt.writeText('', 0, 0, opts); | |
const imageFields = []; | |
// if fields to write | |
if (fields.length > 0) { | |
for (const field of fields) { | |
// if we have data for it | |
if (data[field.name]) { | |
field.text = data[field.name]; | |
// by convention, we say that images fields should end by IMG | |
if (field.name.endsWith('IMG')) { | |
imageFields.push(field); | |
} else { | |
writeTextField(cxt, field, fields, currentFont, opts); | |
/* | |
@Tool : Debug | |
const textDim = openSans.calculateTextDimensions(field.text, opts.size); | |
logger.info("written : ", { | |
[field.text]: { | |
width: textDim.width * opts.size, | |
height: textDim.height * opts.size | |
} | |
}, | |
{ | |
[field.name]: { | |
width: field.width, | |
height: field.height | |
} | |
} | |
); | |
*/ | |
} | |
} | |
} | |
} | |
const tempFiles = await writeImageFields(imageFields, cxt); | |
// logger.info('writing images done'); | |
tempImages.push(tempFiles); | |
// Merge with source PDF | |
pdfWriter.mergePDFPagesToPage(page, template, { | |
type: hummus.eRangeTypeSpecific, | |
specificRanges: [[i, i]] | |
}); | |
// logger.info('merge done'); | |
// write page | |
pdfWriter.writePage(page); | |
// logger.info('page written'); | |
} | |
// end doc | |
pdfWriter.end(); | |
await cleanImages(tempImages); | |
// logger.info('pdf ended'); | |
} | |
/** | |
* Write a field | |
* | |
* @param {HummusContentContext} cxt contentContext | |
* @param {Field} field Field to write | |
* @param {Field[]} fields Current fields | |
* @param {HummusFontObject} currentFont hummus font object | |
* @param {FillerOptions} opts filler options | |
* | |
*/ | |
function writeTextField(cxt: HummusContentContext, | |
field: Field, fields: Field[], currentFont: HummusFontObject, opts: FillerOptions) { | |
// calc if we need to split our text | |
const chunks = splitText(field.text, currentFont, field.width, opts.size); | |
if (chunks.length > 0) { | |
let actualLine = 0; | |
// tslint:disable-next-line:forin | |
for (const chunk in chunks) { | |
const line = chunks[chunk]; | |
// doin some kind of "canFit" | |
if (actualLine === 0) { | |
// write in field (field0 in fact, the real) | |
cxt.writeText(line, field.llx, field.lly); | |
// remove from chunks to avoid duplicate | |
chunks.shift(); | |
} else { | |
// searching for a related field (field1) | |
// if we have a field that continue this field | |
const related = fields.find((formField) => { | |
if (formField.name.indexOf(field.name) !== -1 | |
&& formField.name !== field.name ) { | |
return true; | |
} | |
return false; | |
}) as any; | |
// logger.info('related', related); | |
if (related) { | |
// repeat calc for related (who can be different !) | |
writeInRelatedField(chunks, | |
currentFont, | |
related, opts.size, | |
cxt, | |
fields, | |
field, | |
actualLine); | |
} else { | |
// best effort, pushing down from field cause no related, no lines | |
logger.warn('best effort for', field.name); | |
cxt.writeText(line, field.llx, field.lly - field.height * actualLine); | |
chunks.shift(); | |
} | |
} | |
actualLine++; | |
} | |
return; | |
} | |
// write it ! | |
cxt.writeText(field.text, field.llx, field.lly); | |
} | |
/** | |
* Write in a related field | |
* | |
* @param {string[]} chunks | |
* @param {HummusFontObject} font : font object | |
* @param {Field} related : Related field | |
* @param {number} size | |
* @param {HummusContentContext} cxt : Current contentContext | |
* @param {Field[]} fields : Current fields to write | |
* @param {Field} field : Current field written | |
* @param {number} actualLine : Actual line of text written | |
*/ | |
function writeInRelatedField(chunks: string[], font: HummusFontObject, related: Field, size: number, | |
cxt: HummusContentContext, fields: Field[], field: Field, actualLine: number) { | |
const relatedChunks = splitText(chunks.join(' '), font, related.width, size); | |
if (relatedChunks.length > 0) { | |
let relatedCount = 0; | |
// tslint:disable-next-line:forin | |
for (const relatedChunk in relatedChunks) { | |
const actualChunk = relatedChunks[relatedChunk]; | |
if (relatedCount === 0) { | |
// if it fit in related field, write | |
cxt.writeText(actualChunk, related.llx, related.lly); | |
} else { | |
// searching for a next field (field2, field3...) | |
const nextLine = fields.find((formField) => { | |
if (formField.name.indexOf(field.name + | |
(actualLine + relatedCount | |
+ 1)) !== -1) { | |
return true; | |
} | |
return false; | |
}) as any; | |
if (nextLine) { | |
// write into next related field | |
cxt.writeText(actualChunk, nextLine.llx, nextLine.lly); | |
} else { | |
// best effort, pushing down from related | |
logger.warn('best effort for related', related.name); | |
cxt.writeText(actualChunk, related.llx, related.lly - related.height * relatedCount); | |
} | |
} | |
chunks.shift(); | |
relatedCount++; | |
} | |
} | |
} | |
/** | |
* Split text with specified width / font | |
* | |
* @param {string} text : text to split | |
* @param {HummusFontObject} font : font object | |
* @param {number} maxWidth : maximum line width (form field width) | |
* @param {number} fontSize : fontSize (used to measure text) | |
* | |
* @returns {string[]} splitted text | |
*/ | |
function splitText(text: string, font: HummusFontObject, maxWidth: number, fontSize: number) { | |
const words = text.split(' '); | |
let lineWidth = 0; | |
let current = ''; | |
const lines = []; | |
// For every element in the array of words | |
for (let i = 0; i < words.length; i++) { | |
const word = words[i]; | |
// Add current word to current line | |
current = current.concat(word + ' '); | |
// logger.info('line', thisLine); | |
// Get width of the entire current line | |
lineWidth = font.calculateTextDimensions(current).width * fontSize; | |
if (i !== words.length - 1) { | |
// Find out what the next upcoming word is | |
const nextWord = words[i + 1]; | |
// Check if the current line + the next word would go over width limit | |
if (lineWidth + font.calculateTextDimensions(nextWord).width * fontSize >= maxWidth) { | |
// If so, add the current line to the lines array | |
// without adding the next word | |
lines.push(current); | |
current = ''; | |
lineWidth = 0; | |
} | |
// last word | |
} else { | |
// Add this entire line to the array and return lines | |
lines.push(current); | |
current = ''; | |
lineWidth = 0; | |
} | |
} | |
return lines; | |
} | |
/** | |
* Write image fields | |
* Image needs to be base64 encoded | |
* | |
* @param {Field[]} fields | |
* @param {HummusContentContext} cxt : contentContext | |
* @returns {string[][]} tempImagesPath array | |
*/ | |
async function writeImageFields(fields: Field[], cxt: HummusContentContext) { | |
const imagesPaths = []; | |
for (const field of fields) { | |
if (field.text === '') { | |
continue; | |
} | |
const imagePath = path.join(__dirname, field.name + '.png'); | |
const base64String = field.text; | |
const base64Image = base64String.split(';base64,').pop(); | |
try { | |
await writeFile(imagePath, base64Image, 'base64'); | |
imagesPaths.push(imagePath); | |
// logger.info('written image', imagePath); | |
cxt.drawImage(field.llx, field.lly, imagePath, { | |
transformation: { width: 700 / 4, height: 350 / 4, proportional: true } | |
}); | |
} catch (err) { | |
logger.error('error while writing file'); | |
} | |
} | |
return imagesPaths; | |
} | |
/** | |
* Get fields position in page using reader | |
* | |
* @param {any} page | |
* @param {any} reader | |
* @param {number} index | |
* | |
* @returns {Field[]} fields array [{ name: 'textfield', x: 0, y: 0 }] | |
*/ | |
function getFormFieldsPosition(reader: any, index: number) { | |
// get page count | |
const pageCount = reader.getPagesCount(); | |
if (index >= pageCount) { | |
return []; | |
} | |
// parse this reader page | |
const pagePDF = reader.parsePage(index); | |
// grab object dict | |
const pagePDFDictionary = pagePDF.getDictionary(); | |
// if nothing found, break | |
if (!pagePDFDictionary.exists('Annots')) { | |
return []; | |
} | |
const fields = []; | |
const annotations = reader.queryDictionaryObject(pagePDFDictionary, 'Annots'); | |
// foreach annot | |
for (let i = 0; i < annotations.getLength(); i++) { | |
const subannot = reader.queryArrayObject(annotations , i).toJSObject(); | |
// if it's a Widget | |
// tslint:disable-next-line:triple-equals | |
if (subannot.Subtype == 'Widget' && subannot.T) { | |
// grab info | |
const name = subannot.T.value; | |
const rect = subannot.Rect; | |
// logger.info('annots:', name, subannot.Rect.toJSArray()); | |
/* | |
llx = OFFSET_LEFT (lower left x) | |
lly = OFFSET_BOTTOM; (lower left y) | |
urx = OFFSET_LEFT + WIDTH; (upper right x) | |
ury = OFFSET_BOTTOM + HEIGHT; (upper right y) | |
*/ | |
const llx = reader.queryArrayObject(rect, 0).value; | |
const lly = reader.queryArrayObject(rect, 1).value; | |
const urx = reader.queryArrayObject(rect, 2).value; | |
const ury = reader.queryArrayObject(rect, 3).value; | |
const width = urx - llx; | |
const height = ury - lly; | |
const text = ''; | |
fields.push({ | |
name, | |
llx, | |
lly, | |
width, | |
height, | |
text | |
}); | |
} | |
} | |
// all done | |
return fields; | |
} | |
/** | |
* Delete temp images written | |
* | |
* @param {string[][]} images | |
*/ | |
async function cleanImages(images: string[][]) { | |
for (const page of images) { | |
for (const image of page) { | |
await unlink(image); | |
// logger.warn('deleted temp file: ', image); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment