/**
* Create a comment annotation
* @name comment
* @function
* @memberof Recipe
* @param {string} text - The text content
* @param {number} x - The coordinate x
* @param {number} y - The coordinate y
* @param {Object} [options] - The options
* @param {string} [options.title] - The title.
* @param {string} [options.date] - The date.
* @param {boolean} [options.open=false] - Open the annotation by default?
* @param {boolean} [options.richText] - Display with rich text format, text will be transformed automatically, or you may pass in your own rich text starts with "<?xml..."
* @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
*/
exports.comment = function comment(text = '', x, y, options = {}) {
this.annotationsToWrite.push({
subtype: 'Text',
pageNumber: this.pageNumber,
args: { text, x, y, options: Object.assign({ icon: 'Comment' }, options) }
});
return this;
};
/**
* Create an annotation
* @name annot
* @function
* @memberof Recipe
* @todo support for rich texst RC
* @todo support for opacity CA
* @param {number} x - The coordinate x
* @param {number} y - The coordinate y
* @param {string} subtype - The markup annotation type 'Text'|'FreeText'|'Line'|'Square'|'Circle'|'Polygon'|'PolyLine'|'Highlight'|'Underline'|'Squiggly'|'StrikeOut'|'Stamp'|'Caret'|'Ink'|'FileAttachment'|'Sound'
* @param {Object} [options] - The options
* @param {string} [options.title] - The title.
* @param {boolean} [options.open=false] - Open the annotation by default?
* @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
* @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon] - The icon of annotation.
* @param {number} [options.width] - Width
* @param {number} [options.height] - Height
*/
exports.annot = function annot(x, y, subtype, options = { text: '', width: 0, height: 0 }) {
const { text, width, height } = options;
this.annotationsToWrite.push({
subtype,
args: { text, x, y, width, height, options },
pageNumber: this.pageNumber
});
return this;
};
// TODO: allow non-markup annots to be associated with markup annotations
// Link, Popup, Movie, Widget, Screen, PrinterMark, TrapNet, Watermark, 3D
exports._attachNonMarkupAnnot = function _attachNonMarkupAnnot() {
};
exports._annot = function _annot(subtype, args = {}, pageNumber) {
const { x, y, width, height, text, options } = args;
this._startDictionary(pageNumber);
const { rotate } = this.metadata[pageNumber];
let { nx, ny } = this._calibrateCoordinateForAnnots(x, y, 0, 0, pageNumber);
let nWidth = width;
let nHeight = height;
if (!options.followOriginalPageRotation) {
switch (rotate) {
case 90:
nWidth = height;
nHeight = width;
nx = nx - nWidth;
break;
case 180:
nx = nx - nWidth;
ny = ny - nHeight;
break;
case 270:
nWidth = height;
nHeight = width;
ny = ny - nHeight;
break;
default:
}
}
const params = Object.assign({
title: '',
subject: '',
date: new Date(),
open: false,
flag: '' // 'readonly'
}, options);
const ex = (nWidth) ? nWidth : 0;
const ey = (nHeight) ? nHeight : 0;
const position = [nx, ny, nx + ex, ny + ey];
this.dictionaryContext
.writeKey('Type')
.writeNameValue('Annot')
.writeKey('Subtype')
.writeNameValue(subtype)
.writeKey('L')
.writeBooleanValue(true)
.writeKey('Rect')
.writeRectangleValue(position)
.writeKey('Subj')
.writeLiteralStringValue(params.subject)
.writeKey('T')
.writeLiteralStringValue(params.title || '')
.writeKey('M')
.writeLiteralStringValue(this.writer.createPDFDate(params.date).toString())
.writeKey('Open')
.writeBooleanValue(params.open)
.writeKey('F')
.writeNumberValue(getFlagBitNumberByName(params.flag));
/**
* Rich Text Strings
* 12.7.3.4
*/
if (text && options.richText) {
const richText = (text.substring(0, 4) !== '<?xml') ? contentToRC(text) : text;
const richTextContent = richText;
this.dictionaryContext
.writeKey('RC')
.writeLiteralStringValue(richTextContent);
} else
if (text) {
const textContent = text;
this.dictionaryContext
.writeKey('Contents')
.writeLiteralStringValue(textContent);
}
let { border, color } = options;
if (this._getTextMarkupAnnotationSubtype(subtype)) {
this.dictionaryContext.writeKey('QuadPoints');
const { _textHeight } = options;
const annotHeight = height;
const bx = nx;
const by = ny + ((_textHeight) ? 0 : -annotHeight);
const coordinates = [
[bx, by + annotHeight],
[bx + nWidth, by + annotHeight],
[bx, by],
[bx + nWidth, by],
];
this.objectsContext.startArray();
coordinates.forEach(coord => {
coord.forEach(point => {
this.objectsContext.writeNumber(Math.round(point));
});
});
this.objectsContext
.endArray()
.endLine();
border = border || 0;
if (!color) {
switch (subtype) {
case 'Highlight':
color = [255, 255, 0];
break;
case 'StrikeOut':
color = [255, 0, 0];
break;
case 'Underline':
color = [0, 255, 0];
break;
case 'Squiggly':
color = [0, 255, 0];
break;
default:
color = [0, 0, 0];
break;
}
}
}
if (border != void(0)) {
this.dictionaryContext.writeKey('Border');
this.objectsContext
.startArray()
.writeNumber(0)
.writeNumber(0)
.writeNumber(border)
.endArray()
.endLine();
}
if (color) {
const rgb = this._colorNumberToRGB(this._transformColor(color));
this.dictionaryContext.writeKey('C');
this.objectsContext
.startArray()
.writeNumber(rgb.r / 255)
.writeNumber(rgb.g / 255)
.writeNumber(rgb.b / 255)
.endArray()
.endLine();
}
/* Display Icon */
if (params.icon) {
this.dictionaryContext
.writeKey('Name')
.writeNameValue(params.icon);
}
this._endDictionary(pageNumber);
};
exports._writeAnnotations = function _writeAnnotations() {
this.annotationsToWrite.forEach((annot) => {
this._annot(annot.subtype, annot.args, annot.pageNumber);
});
this.annotations.forEach((pageAnnots, index) => {
this._writeAnnotation(index);
});
};
exports._writeAnnotation = function _writeAnnotation(pageIndex) {
const pdfWriter = this.writer;
const copyingContext = pdfWriter.createPDFCopyingContextForModifiedFile();
const pageID = copyingContext.getSourceDocumentParser().getPageObjectID(pageIndex);
const pageObject = copyingContext.getSourceDocumentParser().parsePage(pageIndex).getDictionary().toJSObject();
const objectsContext = pdfWriter.getObjectsContext();
objectsContext.startModifiedIndirectObject(pageID);
const modifiedPageObject = pdfWriter.getObjectsContext().startDictionary();
Object.getOwnPropertyNames(pageObject).forEach((element) => {
const ignore = ['Annots'];
if (!ignore.includes(element)) {
modifiedPageObject.writeKey(element);
copyingContext.copyDirectObjectAsIs(pageObject[element]);
}
});
modifiedPageObject.writeKey('Annots');
objectsContext.startArray();
if (pageObject['Annots'] && pageObject['Annots'].toJSArray) {
pageObject['Annots'].toJSArray().forEach((annot) => {
objectsContext.writeIndirectObjectReference(annot.getObjectID());
});
}
this.annotations[pageIndex].forEach((item) => {
objectsContext.writeIndirectObjectReference(item);
});
objectsContext
.endArray()
.endLine()
.endDictionary(modifiedPageObject)
.endIndirectObject();
};
exports._startDictionary = function _startDictionary() {
this.objectsContext = this.writer.getObjectsContext();
this.dictionaryObject = this.objectsContext.startNewIndirectObject();
this.dictionaryContext = this.objectsContext.startDictionary();
};
exports._endDictionary = function _endDictionary(pageNumber) {
this.objectsContext
.endDictionary(this.dictionaryContext)
.endIndirectObject();
const pageIndex = pageNumber - 1;
this.annotations[pageIndex] = this.annotations[pageIndex] || [];
this.annotations[pageIndex].push(this.dictionaryObject);
};
exports._getTextMarkupAnnotationSubtype = function _getTextMarkupAnnotationSubtype(subtype = '') {
const matchedSubtype = this.textMarkupAnnotations.find(item => {
return item.toLowerCase() == subtype.toLowerCase();
});
return matchedSubtype;
};
/**
* Get Flag Bit by Name
* @description 12.5.3 Annotation Flags
* @param {string} name
*/
function getFlagBitNumberByName(name) {
switch (name.toLowerCase()) {
case 'invisible':
return 1;
case 'hidden':
return 2;
case 'print':
return 4;
case 'nozoom':
return 8;
case 'norotate':
return 16;
case 'noview':
return 32;
case 'readonly':
return 64;
case 'locked':
return 128;
case 'togglenoview':
return 256;
// 1.7+
// case 'lockedcontents':
// return 512;
default:
return 0;
}
}
/**
* Text Strings to Rich Text Strings
* @todo Fix display issue for ol/ul in richText
* @param {string} content
* @description Support XHTML Elements: '<p>' | '<span>' | '<b>' | '<i>'
* @description Support CSS2 Style: 'text-align' | 'vertical-align' | 'font-size' | 'font-style' | 'font-weight' | 'font-family' | 'font' | 'color' | 'text-decoration' | 'font-stretch'
*/
function contentToRC(content) {
content = content.replace(' ', ' ');
content = content.replace(/\r?\n|\r|\t/g, '');
let richText =
'<?xml version="1.0"?>' +
'<body ' +
'xmlns="http://www.w3.org/1999/xhtml"' +
// 'xmlns:xga=\"http://www.xfa.org/schema/xfa-data/1.0/\" ' +
// 'xfa:contentType=\"text/html\" ' +
// 'xfa:APIVersion=\"Acrobat:8.0.0\" ' +
// 'xfa:spec=\"2.4\" ' +
'>' +
content +
'</body>';
richText = richText
.replace(/<li>/g, '<p> • ')
.replace(/<(\/)li>/g, '</p>')
.replace(/<(\/)p>/g, '</p><br/>');
return richText;
}