Skip to content

Instantly share code, notes, and snippets.

@m1m1s1ku
Last active May 23, 2018 19:25
Show Gist options
  • Save m1m1s1ku/2988e59d9c2b6ca42330d96796aa01ed to your computer and use it in GitHub Desktop.
Save m1m1s1ku/2988e59d9c2b6ca42330d96796aa01ed to your computer and use it in GitHub Desktop.
flatten form fields hummus (formFiller new gen)
// 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