Created
September 11, 2019 23:15
-
-
Save joer14/1e063753a6bae7996184b458cf12d8f8 to your computer and use it in GitHub Desktop.
I hacked pdf-form-fill to support multiline text. Only works if you explicitly specify `defaultTextOptions` when calling `fillForm`
This file contains 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
var hummus = require('hummus'), | |
_ = require('lodash'); | |
/** | |
* toText function. should get this into hummus proper sometimes | |
*/ | |
function toText(item) { | |
if(item.getType() === hummus.ePDFObjectLiteralString) { | |
return item.toPDFLiteralString().toText(); | |
} | |
else if(item.getType() === hummus.ePDFObjectHexString) { | |
return item.toPDFHexString().toText(); | |
} else { | |
return item.value; | |
} | |
} | |
/** | |
* a wonderfully reusable method to recreate a dict without all the keys that we want to change | |
* note that it starts writing a dict, but doesn't finish it. your job | |
*/ | |
function startModifiedDictionary(handles,originalDict,excludedKeys) { | |
var originalDictJs = originalDict.toJSObject(); | |
var newDict = handles.objectsContext.startDictionary(); | |
Object.getOwnPropertyNames(originalDictJs).forEach(function(element,index,array) { | |
if (!excludedKeys[element]) { | |
newDict.writeKey(element); | |
handles.copyingContext.copyDirectObjectAsIs(originalDictJs[element]); | |
} | |
}); | |
return newDict; | |
} | |
function defaultTerminalFieldWrite(handles,fieldDictionary) { | |
// default write of ending field. no reason to recurse to kids | |
handles.copyingContext | |
.copyDirectObjectAsIs(fieldDictionary) | |
.endIndirectObject(); | |
} | |
/** | |
* Update radio button value. look for the field matching the value, which should be an index. | |
* Set its ON appearance as the value, and set all radio buttons appearance to off, but the selected one which should be on | |
*/ | |
function updateOptionButtonValue(handles,fieldDictionary,value) { | |
var isWidget = fieldDictionary.exists('Subtype') && (fieldDictionary.queryObject('Subtype').toString() == 'Widget'); | |
if(isWidget || ! fieldDictionary.exists('Kids')) { | |
// this radio button has just one option and its in the widget. also means no kids | |
var modifiedDict = startModifiedDictionary(handles,fieldDictionary,{'V':-1,'AS':-1}); | |
var appearanceName; | |
if(value === null) { | |
// false is easy, just write '/Off' as the value and as the appearance stream | |
appearanceName = 'Off'; | |
} | |
else { | |
// grab the non off value. that should be the yes one | |
var apDictionary = handles.reader.queryDictionaryObject(fieldDictionary,'AP').toPDFDictionary(); | |
var nAppearances = handles.reader.queryDictionaryObject(apDictionary,'N').toPDFDictionary().toJSObject(); | |
appearanceName = _.find(Object.keys(nAppearances),function(item){return item !== 'Off'}); | |
} | |
modifiedDict | |
.writeKey('V') | |
.writeNameValue(appearanceName) | |
.writeKey('AS') | |
.writeNameValue(appearanceName); | |
handles.objectsContext | |
.endDictionary(modifiedDict) | |
.endIndirectObject(); | |
} else { | |
// Field. this would mean that there's a kid array, and there are offs and ons to set | |
var modifiedDict = startModifiedDictionary(handles,fieldDictionary,{'V':-1,'Kids':-1}); | |
var kidsArray = handles.reader.queryDictionaryObject(fieldDictionary,'Kids').toPDFArray(); | |
var appearanceName; | |
if(value === null) { | |
// false is easy, just write '/Off' as the value and as the appearance stream | |
appearanceName = 'Off'; | |
} | |
else { | |
// grab the non off value. that should be the yes one | |
var widgetDictionary = handles.reader.queryArrayObject(kidsArray,value).toPDFDictionary(); | |
var apDictionary = handles.reader.queryDictionaryObject(widgetDictionary,'AP').toPDFDictionary(); | |
var nAppearances = handles.reader.queryDictionaryObject(apDictionary,'N').toPDFDictionary().toJSObject(); | |
appearanceName = _.find(Object.keys(nAppearances),function(item){return item !== 'Off'}); | |
} | |
// set the V value on the new field dictionary | |
modifiedDict | |
.writeKey('V') | |
.writeNameValue(appearanceName); | |
// write the Kids key before we write the kids array | |
modifiedDict.writeKey('Kids') | |
// write the kids array, similar to writeFilledFields, but knowing that these are widgets and that AS needs to be set | |
var fieldsReferences = writeKidsAndEndObject(handles,modifiedDict,kidsArray); | |
// recreate widget kids, turn on or off based on their relation to the target value | |
for(var i=0;i<fieldsReferences.length;++i) { | |
var fieldReference = fieldsReferences[i]; | |
var sourceField; | |
if(fieldReference.existing) { | |
handles.objectsContext.startModifiedIndirectObject(fieldReference.id); | |
sourceField = handles.reader.parseNewObject(fieldReference.id).toPDFDictionary(); | |
} else { | |
handles.objectsContext.startNewIndirectObject(fieldReference.id); | |
sourceField = fieldReference.field.toPDFDictionary(); | |
} | |
var modifiedFieldDict = startModifiedDictionary(handles,sourceField,{'AS':-1}); | |
if(value === i) { | |
// this widget should be on | |
modifiedFieldDict | |
.writeKey('AS') | |
.writeNameValue(appearanceName); // note that we have saved it earlier | |
} | |
else { | |
// this widget should be off | |
modifiedFieldDict | |
.writeKey('AS') | |
.writeNameValue('Off'); | |
} | |
// finish | |
handles.objectsContext | |
.endDictionary(modifiedFieldDict) | |
.endIndirectObject(); | |
} | |
} | |
} | |
function getOriginalTextFieldAppearanceStreamCode(handles, fieldDictionary) { | |
// get the single appearance stream for the text, field. we'll use it to recreate the new one | |
var appearanceInField = fieldDictionary.exists('Subtype') && (fieldDictionary.queryObject('Subtype').toString() == 'Widget') || !fieldDictionary.exists('Kids'); | |
var appearanceParent = null; | |
if(appearanceInField) { | |
appearanceParent = fieldDictionary; | |
} | |
else { | |
if(fieldDictionary.exists('Kids')) { | |
var kidsArray = handles.reader.queryDictionaryObject(fieldDictionary,'Kids').toPDFArray(); | |
if(kidsArray.getLength() > 0) { | |
appearanceParent = handles.reader.queryArrayObject(0).toPDFDictionary(); | |
} | |
} | |
} | |
if(!appearanceParent) | |
return null; | |
if(!appearanceParent.exists('AP')) | |
return null; | |
var appearance = handles.reader.queryDictionaryObject(appearanceParent,'AP').toPDFDictionary(); | |
if(!appearance.exists('N')) | |
return null; | |
var appearanceXObject = handles.reader.queryDictionaryObject(appearance,'N').toPDFStream(); | |
return readStreamToString(handles,appearanceXObject); | |
} | |
function writeMultiline(text, xobjectForm, textOptions, boxWidth, boxHeight, before, after) { | |
var textDimensions = textOptions.font.calculateTextDimensions(text,textOptions.size); | |
const xPos = 0; | |
const lines = []; | |
let allWords = text.split(' ').reverse(); | |
let currentLine = ''; | |
while(allWords.length > 0) { | |
const nextWord = allWords.pop(); | |
const potentialNextLine = `${currentLine} ${nextWord}`; | |
const nextLineDiminsions = textOptions.font.calculateTextDimensions(potentialNextLine,textOptions.size); | |
if(nextLineDiminsions.width < boxWidth && allWords.length != 0) { | |
currentLine = potentialNextLine; | |
} else { | |
lines.push(currentLine); | |
currentLine = nextWord; | |
} | |
} | |
const lineHeight = textDimensions.height+2; | |
if(lines.length*lineHeight > boxHeight) { | |
const e = new Error('content over flow for multiline box'); | |
throw e; | |
} | |
lines.forEach((line,idx) => { | |
const yPos = (boxHeight-(lineHeight*(idx+1))); // vertical top | |
xobjectForm.getContentContext() | |
.writeFreeCode(before) | |
.writeFreeCode('/Tx BMC\r\n') | |
.q() | |
.writeText(line,xPos,yPos,textOptions) | |
.Q() | |
.writeFreeCode('EMC') | |
.writeFreeCode(after); | |
}) | |
} | |
function writeAppearanceXObjectForText(handles,formId,fieldsDictionary,text,inheritedProperties) { | |
var rect = handles.reader.queryDictionaryObject(fieldsDictionary,'Rect').toPDFArray().toJSArray(); | |
let da = fieldsDictionary.exists('DA') ? fieldsDictionary.queryObject('DA').toString():inheritedProperties['DA']; | |
let q = fieldsDictionary.exists('Q') ? fieldsDictionary.queryObject('Q').toNumber():inheritedProperties['Q']; | |
if(handles.options.debug) { | |
console.debug('creating new appearance with:') | |
console.debug('da =', da) | |
console.debug('q =', q) | |
console.debug('fieldsDictionary =', fieldsDictionary.toJSObject()) | |
console.debug('inheritedProperties =', inheritedProperties) | |
console.debug('text =', text) | |
} | |
var originalAppearanceContent = getOriginalTextFieldAppearanceStreamCode(handles,fieldsDictionary); | |
var before = ''; | |
var after = ''; | |
if(!!originalAppearanceContent) { | |
var pre = originalAppearanceContent.indexOf('/Tx BMC') | |
if(pre !== -1) { | |
before = originalAppearanceContent.substr(0,pre); | |
post = originalAppearanceContent.indexOf('EMC',pre + '/Tx BMC'.length) | |
if(post !== -1) { | |
after = originalAppearanceContent.substr(post + 'EMC'.length) | |
} | |
} | |
else { | |
before = originalAppearanceContent; | |
} | |
} | |
var boxWidth = rect[2].value - rect[0].value | |
var boxHeight = rect[3].value - rect[1].value; | |
var xobjectForm = handles.writer.createFormXObject( | |
0, | |
0, | |
boxWidth, | |
boxHeight, | |
formId); | |
// If default text options setup, use them to determine the text appearance. including quad support, horizontal centering etc. | |
// Otherwise, use naive method: Will use Tj with "code" encoding to write the text, assuming encoding should work (??). if it won't i need real fonts here | |
// and DA is not gonna be useful. so for now let's use as is. | |
// For the same reason i'm not support Quad, as well | |
// Should be able to parse the following from the DA, and map to system font | |
// temporarily, let user input the values | |
var textOptions = handles.options.defaultTextOptions | |
if(textOptions) { | |
// grab text dimensions for quad support and vertical centering | |
var textDimensions = textOptions.font.calculateTextDimensions(text,textOptions.size); | |
// vertical centering | |
var yPos = (boxHeight-textDimensions.height)/2 | |
// horizontal pos per quad | |
var quad = q || 0 | |
var xPos = 0 | |
switch(quad) { | |
case 0: | |
// left align | |
xPos = 0 | |
break; | |
case 1: | |
// center | |
xPos = (boxWidth-textDimensions.width)/2 | |
break; | |
case 2: | |
// right align | |
xPos = (boxWidth-textDimensions.width) | |
} | |
const useMultiLine = boxWidth-textDimensions.width < 0; | |
if(useMultiLine) { | |
writeMultiline(text, xobjectForm, textOptions, boxWidth, boxHeight, before, after); | |
} | |
else { | |
xobjectForm.getContentContext() | |
.writeFreeCode(before) | |
.writeFreeCode('/Tx BMC\r\n') | |
.q() | |
.writeText(text,xPos,yPos,textOptions) | |
.Q() | |
.writeFreeCode('EMC') | |
.writeFreeCode(after); | |
} | |
} else { | |
// Naive form, no quad support...and text may not show and may be mispositioned | |
xobjectForm.getContentContext() | |
.writeFreeCode(before) | |
.writeFreeCode('/Tx BMC\r\n') | |
.q() | |
.BT() | |
.writeFreeCode(da + '\r\n') | |
.Tj(text,{encoding:'code'}) | |
.ET() | |
.Q() | |
.writeFreeCode('EMC') | |
.writeFreeCode(after); | |
} | |
// register to copy resources from form default resources dict [would have been better to just refer to it... | |
// but alas don't have access for xobject resources dict]. | |
// Later note: well, we do need to add the fonts on occasion... | |
if(handles.acroformDict.exists('DR')) { | |
handles.writer.getEvents().once('OnResourcesWrite',function(args){ | |
// copy all but the keys that exist already | |
var dr = handles.reader.queryDictionaryObject(handles.acroformDict,'DR').toPDFDictionary().toJSObject(); | |
Object.getOwnPropertyNames(dr).forEach(function(element,index,array) { | |
if (element !== 'ProcSet' && (!textOptions || element !== 'Font')) { | |
args.pageResourcesDictionaryContext.writeKey(element); | |
handles.copyingContext.copyDirectObjectAsIs(dr[element]); | |
} | |
}); | |
}); | |
} | |
handles.writer.endFormXObject(xobjectForm); | |
} | |
var BUFFER_SIZE = 10000; | |
function readStreamToString(handles,stream) { | |
var buff = ''; | |
var readStream = handles.reader.startReadingFromStream(stream); | |
while(readStream.notEnded()) | |
{ | |
var readData = readStream.read(BUFFER_SIZE); | |
buff+= _.reduce(readData,function(acc,item){ return acc + String.fromCharCode(item)},''); | |
} | |
return buff; | |
} | |
function writeFieldWithAppearanceForText(handles,targetFieldDict,sourceFieldDictionary,appearanceInField,textToWrite,inheritedProperties) { | |
// determine how to write appearance | |
var newAppearanceFormId = handles.objectsContext.allocateNewObjectID(); | |
if(appearanceInField) { | |
// Appearance in field - so write appearance dict in field | |
targetFieldDict | |
.writeKey('AP'); | |
var apDict = handles.objectsContext.startDictionary(); | |
apDict.writeKey("N").writeObjectReferenceValue(newAppearanceFormId); | |
handles.objectsContext | |
.endDictionary(apDict) | |
.endDictionary(targetFieldDict) | |
.endIndirectObject(); | |
} | |
else { | |
// finish the field object | |
handles.objectsContext | |
.endDictionary(targetFieldDict) | |
.endIndirectObject(); | |
// write in kid (there should be just one) | |
var kidsArray = handles.reader.queryDictionaryObject(sourceFieldDictionary,'Kids').toPDFArray(); | |
var fieldsReferences = writeKidsAndEndObject(handles,targetFieldDict,kidsArray); | |
// recreate widget kid, with new stream reference | |
var fieldReference = fieldsReferences[0]; | |
if(fieldReference.existing) { | |
handles.objectsContext.startModifiedIndirectObject(fieldReference.id); | |
sourceField = handles.reader.parseNewObject(fieldReference.id).toPDFDictionary(); | |
} else { | |
handles.objectsContext.startNewIndirectObject(fieldReference.id); | |
sourceField = fieldReference.field.toPDFDictionary(); | |
} | |
var modifiedDict = startModifiedDictionary(handles,sourceField,{'AP':-1}); | |
modifiedDict | |
.writeKey('AP'); | |
var apDict = handles.objectsContext.startDictionary(); | |
apDict.writeKey("N").writeObjectReferenceValue(newAppearanceFormId); | |
handles.objectsContext | |
.endDictionary(apDict) | |
.endDictionary(modifiedDict) | |
.endIndirectObject(); | |
} | |
// write the new stream xobject | |
writeAppearanceXObjectForText(handles,newAppearanceFormId,sourceFieldDictionary,textToWrite,inheritedProperties); | |
} | |
function updateTextValue(handles,fieldDictionary,value,isRich,inheritedProperties) { | |
if(typeof(value) === 'string') { | |
value = {v:value,rv:value}; | |
} | |
var appearanceInField = fieldDictionary.exists('Subtype') && (fieldDictionary.queryObject('Subtype').toString() == 'Widget') || !fieldDictionary.exists('Kids'); | |
var fieldsToRemove = {'V':-1}; | |
if(appearanceInField) { | |
// add skipping AP if in field (and not in a child widget) | |
fieldsToRemove['AP'] = -1; | |
} | |
if(isRich) { | |
// skip RV if rich | |
fieldsToRemove['RV'] = -1; | |
} | |
var modifiedDict = startModifiedDictionary(handles,fieldDictionary,fieldsToRemove); | |
// start with value, setting both plain value and rich value | |
modifiedDict | |
.writeKey('V') | |
.writeLiteralStringValue(new hummus.PDFTextString(value['v']).toBytesArray()); | |
if(isRich) { | |
modifiedDict | |
.writeKey('RV') | |
.writeLiteralStringValue(new hummus.PDFTextString(value['rv']).toBytesArray()); | |
} | |
writeFieldWithAppearanceForText(handles,modifiedDict,fieldDictionary,appearanceInField,value['v'],inheritedProperties); | |
} | |
function updateChoiceValue(handles,fieldDictionary,value,inheritedProperties) { | |
var appearanceInField = fieldDictionary.exists('Subtype') && (fieldDictionary.queryObject('Subtype').toString() == 'Widget') || !fieldDictionary.exists('Kids'); | |
var fieldsToRemove = {'V':-1}; | |
if(appearanceInField) { | |
// add skipping AP if in field (and not in a child widget) | |
fieldsToRemove['AP'] = -1; | |
} | |
var modifiedDict = startModifiedDictionary(handles,fieldDictionary,fieldsToRemove); | |
// start with value, setting per one or multiple selection. also choose the text to write in appearance | |
var textToWrite; | |
if(typeof(value) === 'string') { | |
// one option | |
modifiedDict | |
.writeKey('V') | |
.writeLiteralStringValue(new hummus.PDFTextString(value).toBytesArray()); | |
textToWrite = value; | |
} | |
else { | |
// multiple options | |
modifiedDict | |
.writeKey('V'); | |
handles.objectsContext.startArray(); | |
value.forEach(function(singleValue) { | |
handles.objectsContext.writeLiteralString(new hummus.PDFTextString(singleValue).toBytesArray()); | |
}); | |
handles.objectsContext.endArray(); | |
textToWrite = value.length > 0 ? value[0]:''; | |
} | |
writeFieldWithAppearanceForText(handles,modifiedDict,fieldDictionary,appearanceInField,textToWrite,inheritedProperties); | |
} | |
/** | |
* Update a field. splits to per type functions | |
*/ | |
function updateFieldWithValue(handles,fieldDictionary,value,inheritedProperties) { | |
// Update a field with value. There is a logical assumption made here: | |
// This must be a terminal field. meaning it is a field, and it either has no kids, it also holding | |
// Widget data or that it has one or more kids defining its widget annotation(s). Normally it would be | |
// One but in the case of a radio button, where there's one per option. | |
var localFieldType = fieldDictionary.exists('FT') ? fieldDictionary.queryObject('FT').toString():undefined, | |
fieldType = localFieldType || inheritedProperties['FT'], | |
localFlags = fieldDictionary.exists('Ff') ? fieldDictionary.queryObject('Ff').toNumber():undefined, | |
flags = localFlags === undefined ? inheritedProperties['Ff'] : localFlags; | |
// the rest is fairly type dependent, so let's check the type | |
switch(fieldType) { | |
case 'Btn': { | |
if((flags>>16) & 1) | |
{ | |
// push button. can't write a value. forget it. | |
defaultTerminalFieldWrite(handles,fieldDictionary); | |
} | |
else | |
{ | |
// checkbox or radio button | |
updateOptionButtonValue(handles,fieldDictionary,(flags>>15) & 1 ? value : (value ? 0:null)); | |
} | |
break; | |
} | |
case 'Tx': { | |
// rich or plain text | |
updateTextValue(handles,fieldDictionary,value,(flags>>25) & 1,inheritedProperties); | |
break; | |
} | |
case 'Ch': { | |
updateChoiceValue(handles,fieldDictionary,value,inheritedProperties); | |
break; | |
} | |
case 'Sig': { | |
// signature, ain't handling that. should return or throw an error sometimes | |
defaultTerminalFieldWrite(handles,fieldDictionary); | |
break; | |
} | |
default: { | |
// in case there's a fault and there's no type, or it's irrelevant | |
defaultTerminalFieldWrite(handles,fieldDictionary); | |
} | |
} | |
} | |
function writeFieldAndKids(handles,fieldDictionary,inheritedProperties,baseFieldName) { | |
// this field or widget doesn't need value rewrite. but its kids might. so write the dictionary as is, dropping kids. | |
// write them later and recurse. | |
var modifiedFieldDict = startModifiedDictionary(handles,fieldDictionary,{'Kids':-1}); | |
// if kids exist, continue to them for extra filling! | |
var kids = fieldDictionary.exists('Kids') ? | |
handles.reader.queryDictionaryObject(fieldDictionary,'Kids').toPDFArray() : | |
null; | |
if(kids) { | |
var localEnv = {} | |
// prep some inherited values and push env | |
if(fieldDictionary.exists('FT')) | |
localEnv['FT'] = fieldDictionary.queryObject('FT').toString(); | |
if(fieldDictionary.exists('Ff')) | |
localEnv['Ff'] = fieldDictionary.queryObject('Ff').toNumber(); | |
if(fieldDictionary.exists('DA')) | |
localEnv['DA'] = fieldDictionary.queryObject('DA').toString(); | |
if(fieldDictionary.exists('Q')) | |
localEnv['Q'] = fieldDictionary.queryObject('Q').toNumber(); | |
if(fieldDictionary.exists('Opt')) | |
localEnv['Opt'] = fieldDictionary.queryObject('Opt').toPDFArray(); | |
modifiedFieldDict.writeKey('Kids'); | |
// recurse to kids. note that this will take care of ending this object | |
writeFilledFields(handles,modifiedFieldDict,kids,_.extend({},inheritedProperties,localEnv),baseFieldName + '.'); | |
} else { | |
// no kids, can finish object now | |
handles.objectsContext | |
.endDictionary(modifiedFieldDict) | |
.endIndirectObject(); | |
} | |
} | |
/** | |
* writes a single field. will fill with value if found in data. | |
* assuming that's in indirect object and having to write the dict,finish the dict, indirect object and write the kids | |
*/ | |
function writeFilledField(handles,fieldDictionary,inheritedProperties,baseFieldName) { | |
var localFieldNameT = fieldDictionary.exists('T') ? toText(fieldDictionary.queryObject('T')):undefined, | |
fullName = localFieldNameT === undefined ? baseFieldName : (baseFieldName + localFieldNameT); | |
// Based on the fullName we can now determine whether the field has a value that needs setting | |
if(handles.data[fullName]) { | |
// We got a winner! write with updated value | |
updateFieldWithValue(handles,fieldDictionary,handles.data[fullName],inheritedProperties); | |
} | |
else { | |
// Not yet. write and recurse to kids | |
writeFieldAndKids(handles,fieldDictionary,inheritedProperties,fullName); | |
} | |
} | |
/** | |
* Write kids array converting each direct kids to an indirect one | |
*/ | |
function writeKidsAndEndObject(handles,parentDict,kidsArray) { | |
var fieldsReferences = [], | |
fieldJSArray = kidsArray.toJSArray(); | |
handles.objectsContext.startArray(); | |
fieldJSArray.forEach(function(field) { | |
if(field.getType() === hummus.ePDFObjectIndirectObjectReference) { | |
// existing reference, keep as is | |
handles.copyingContext.copyDirectObjectAsIs(field); | |
fieldsReferences.push({existing:true,id:field.toPDFIndirectObjectReference().getObjectID()}); | |
} | |
else { | |
var newFieldObjectId = handles.objectsContext.allocateNewObjectID(); | |
// direct object, recreate as reference | |
fieldsReferences.push({existing:false,id:newFieldObjectId,theObject:field}); | |
handles.copyingContext.writeIndirectObjectReference(newFieldObjectId); | |
} | |
}); | |
handles.objectsContext | |
.endArray(hummus.eTokenSeparatorEndLine) | |
.endDictionary(parentDict) | |
.endIndirectObject(); | |
return fieldsReferences; | |
} | |
/** | |
* write fields/kids array of dictionary. make sure all become indirect, for the sake of simplicity, | |
* which is why it gets to take care of finishing the writing of the said dict | |
*/ | |
function writeFilledFields(handles,parentDict,fields,inheritedProperties,baseFieldName) { | |
var fieldsReferences = writeKidsAndEndObject(handles,parentDict, fields); | |
// now recreate the fields, filled this time (and down the recursion hole...) | |
fieldsReferences.forEach(function(fieldReference) { | |
if(fieldReference.existing) { | |
handles.objectsContext.startModifiedIndirectObject(fieldReference.id); | |
writeFilledField(handles,handles.reader.parseNewObject(fieldReference.id).toPDFDictionary(),inheritedProperties,baseFieldName); | |
} | |
else { | |
handles.objectsContext.startNewIndirectObject(fieldReference.id); | |
writeFilledField(handles,fieldReference.field.toPDFDictionary(),inheritedProperties,baseFieldName); | |
} | |
}); | |
} | |
/** | |
* Write a filled form dictionary, and its subordinate fields. | |
* assumes in an indirect object, so will finish it | |
*/ | |
function writeFilledForm(handles,acroformDict) { | |
var modifiedAcroFormDict = startModifiedDictionary(handles,acroformDict,{'Fields':-1}); | |
var fields = acroformDict.exists('Fields') ? | |
handles.reader.queryDictionaryObject(acroformDict,'Fields').toPDFArray() : | |
null; | |
if(fields) { | |
modifiedAcroFormDict.writeKey('Fields'); | |
writeFilledFields(handles,modifiedAcroFormDict,fields,{},''); // will also take care of finishing the dictionary and indirect object, so no need to finish after | |
} else { | |
handles | |
.objectsContext.endDictionary(modifiedAcroFormDict) | |
.objectsContext.endIndirectObject(); | |
} | |
} | |
function fillForm(writer,data, options) { | |
// setup parser | |
var reader = writer.getModifiedFileParser(); | |
// start out by finding the acrobat form | |
var catalogDict = reader.queryDictionaryObject(reader.getTrailer(),'Root').toPDFDictionary(), | |
acroformInCatalog = catalogDict.exists('AcroForm') ? catalogDict.queryObject('AcroForm'):null; | |
if(!acroformInCatalog) | |
return new Error('form not found!'); | |
// setup copying context, and keep reference to objects context as well | |
var copyingContext = writer.createPDFCopyingContextForModifiedFile(); | |
var objectsContext = writer.getObjectsContext(); | |
// parse the acroform dict | |
var acroformDict = catalogDict.exists('AcroForm') ? reader.queryDictionaryObject(catalogDict,'AcroForm'):null; | |
// lets put all the basics in a nice "handles" package, so we don't have to pass each of them all the time | |
var handles = { | |
writer:writer, | |
reader:reader, | |
copyingContext:copyingContext, | |
objectsContext:objectsContext, | |
data:data, | |
acroformDict:acroformDict, | |
options:options || {} | |
}; | |
// recreate a copy of the existing form, which we will fill with data. | |
if(acroformInCatalog.getType() === hummus.ePDFObjectIndirectObjectReference) { | |
// if the form is a referenced object, modify it | |
console.log('1111') | |
var acroformObjectId = acroformInCatalog.toPDFIndirectObjectReference().getObjectID(); | |
objectsContext.startModifiedIndirectObject(acroformObjectId); | |
writeFilledForm(handles,acroformDict); | |
} else { | |
// otherwise, recreate the form as an indirect child (this is going to be a general policy, we're making things indirect. it's simpler), and recreate the catalog | |
var catalogObjectId = reader.getTrailer().queryObject('Root').toPDFIndirectObjectReference().getObjectID(); | |
var newAcroformObjectId = objectsContext.allocateNewObjectID(); | |
// recreate the catalog with form pointing to new reference | |
objectsContext.startModifiedIndirectObject(catalogObjectId); | |
let modifiedCatalogDictionary = startModifiedDictionary(handles,catalogDict,{'AcroForm':-1}); | |
modifiedCatalogDictionary.writeKey('AcroForm'); | |
modifiedCatalogDictionary.writeObjectReferenceValue(newAcroformObjectId); | |
objectsContext | |
.endDictionary(modifiedCatalogDictionary) | |
.endIndirectObject(); | |
// now create the new form object | |
objectsContext.startNewIndirectObject(newAcroformObjectId); | |
writeFilledForm(handles,acroformDict); | |
} | |
} | |
module.exports = { | |
fillForm:fillForm | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment