@digital-blueprint/annotpdf
Version:
Javascript library for creating annotations in PDF documents
517 lines • 23 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const parser_1 = require("./parser");
const util_1 = require("./util");
const writer_1 = require("./writer");
/**
* The annotation factory provides methods to create annotations. Those are stored temporary
* and than encoded into PDF code when the PDF document is outputted and for instance downloaded.
* That
* */
class AnnotationFactory {
constructor(data, userPassword = "", ownerPassword = "") {
this.data = data;
this.userPassword = userPassword;
this.ownerPassword = ownerPassword;
this.annotations = [];
this.toDelete = [];
this.data = data;
this.parser = new parser_1.PDFDocumentParser(this.data, this.userPassword, this.ownerPassword);
}
/**
* Returns the number of annotations that will be added to the PDF document
* */
getAnnotationCount() {
return this.annotations.length;
}
/**
* Load a PDF file referenced by the given 'path'
* */
static loadFile(path, userPassword = "", ownerPassword = "") {
return new Promise((resolve) => {
if (typeof window !== 'undefined') { // browser environment
fetch(path).then((r) => r.blob()).then((data) => {
let reader = new FileReader();
reader.onload = () => {
resolve(new AnnotationFactory(reader.result, userPassword, ownerPassword));
};
reader.readAsArrayBuffer(data);
});
}
else if (typeof process === 'object') { // node environment
let fs = require('fs');
let data = fs.readFileSync(path);
resolve(new AnnotationFactory(data, userPassword, ownerPassword));
}
else {
throw Error("Unsupported environment");
}
});
}
/**
* Generates a unique identifier necessary for creating the annotation
* */
generateUniqueIdentifier() {
return "pdfAnnotate-" + util_1.Util.convertDateToPDFDate(new Date()).slice(3, 17) + "-" + this.annotations.length;
}
/**
* Generates a default border
* */
createDefaultBorder() {
return {
vertical_corner_radius: 0,
horizontal_corner_radius: 0,
border_width: 1
};
}
/**
* Writes the made annotations into a bytestream
* */
write() {
if (this.annotations.length === 0 && this.toDelete.length === 0)
return this.data;
let writer = new writer_1.Writer(this.data, this.annotations, this.toDelete, this.parser);
return writer.write();
}
/**
* Checks the 'rect' parameter, whether all the entries are of type number and if the the number of entries is correct
* */
checkRect(nr, rect) {
if (rect.length !== nr)
throw Error("Rect has invalid number of entries: " + rect + " has " + rect.length + " entries, but should have " + nr + " entries");
rect.forEach((a) => {
if ('number' !== typeof a)
throw Error("Rect " + rect + " has invalid entry: " + a);
});
}
/**
* Extracts the rectangular hull from a quadPoint definition
* */
extractRectFromQuadPoints(quadPoints) {
let x_values = quadPoints.filter((element, index) => index % 2 === 0);
let y_values = quadPoints.filter((element, index) => index % 2 !== 0);
return [Math.min(...x_values), Math.min(...y_values), Math.max(...x_values), Math.max(...y_values)];
}
/**
* Checks the 'quadPoints' parameter
* */
checkQuadPoints(quadPoints) {
if (quadPoints.length % 8 !== 0)
throw Error(`Quadpoints array has length ${quadPoints.length} but must be a multiple of 8`);
quadPoints.forEach((a) => {
if ('number' !== typeof a)
throw Error("Quadpoint " + quadPoints + " has invalid entry: " + a);
});
}
/**
* Creates a base annotation that mean the raw object of annotation or those parts that are all existing
* and equally set in all types of annotations
* */
createBaseAnnotation(page, rect, contents, author) {
let annot = new parser_1.Annotation();
annot.type = "/Annot",
annot.page = page,
annot.object_id = this.parser.getFreeObjectId(),
annot.id = this.generateUniqueIdentifier(),
annot.rect = rect,
annot.author = author,
annot.pageReference = this.parser.getPage(page),
annot.updateDate = util_1.Util.convertDateToPDFDate(new Date()),
annot.contents = contents,
annot.border = this.createDefaultBorder();
return annot;
}
/**
* Creates a text annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createTextAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }) {
if (!contents)
contents = "";
if (!author)
author = "";
this.checkRect(4, rect);
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color
});
annot.type = "/Text";
this.annotations.push(annot);
}
/**
* Creates a text markup annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* subtype : the subtype of the Textmarkup annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createTextMarkupAnnotation(page, rect, contents, author, subtype, color = { r: 1, g: 1, b: 0 }, quadPoints = []) {
if (0 === quadPoints.length)
quadPoints = [rect[0], rect[3], rect[2], rect[3], rect[0], rect[1], rect[2], rect[1]];
else {
this.checkQuadPoints(quadPoints);
}
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
quadPoints: quadPoints
});
annot.type = subtype;
return annot;
}
/**
* Creates a highlight annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createHighlightAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, quadPoints = []) {
this.checkQuadPoints(quadPoints);
if (rect.length === 0 && quadPoints.length > 0) {
rect = this.extractRectFromQuadPoints(quadPoints);
}
this.checkRect(4, rect);
let annot = this.createTextMarkupAnnotation(page, rect, contents, author, "/Highlight", color, quadPoints);
this.annotations.push(annot);
}
/**
* Creates an underline annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createUnderlineAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, quadPoints = []) {
this.checkQuadPoints(quadPoints);
if (rect.length === 0 && quadPoints.length > 0) {
rect = this.extractRectFromQuadPoints(quadPoints);
}
this.checkRect(4, rect);
let annot = this.createTextMarkupAnnotation(page, rect, contents, author, "/Underline", color, quadPoints);
this.annotations.push(annot);
}
/**
* Creates a squiggle annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createSquigglyAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, quadPoints = []) {
this.checkQuadPoints(quadPoints);
if (rect.length === 0 && quadPoints.length > 0) {
rect = this.extractRectFromQuadPoints(quadPoints);
}
this.checkRect(4, rect);
let annot = this.createTextMarkupAnnotation(page, rect, contents, author, "/Squiggly", color, quadPoints);
this.annotations.push(annot);
}
/**
* Creates a strike out annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createStrikeOutAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, quadPoints = []) {
this.checkQuadPoints(quadPoints);
if (rect.length === 0 && quadPoints.length > 0) {
rect = this.extractRectFromQuadPoints(quadPoints);
}
this.checkRect(4, rect);
let annot = this.createTextMarkupAnnotation(page, rect, contents, author, "/StrikeOut", color, quadPoints);
this.annotations.push(annot);
}
/**
* Creates a free text annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createFreeTextAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 1 }) {
this.checkRect(4, rect);
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
textAlignment: "right-justified",
annotation_flag: 4,
color: color,
defaultAppearance: "/Invalid_font 9 Tf"
});
annot.type = "/FreeText";
this.annotations.push(annot);
}
createLineAnnotation() {
throw Error("No yet implemented");
}
/**
* Creates the base annotation object of a circle and square annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* fill : the filling color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createSquareCircleAnnotation(page, rect, contents, author, subtype, color = { r: 1, g: 1, b: 0 }, fill = { r: 1, g: 1, b: 0 }) {
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
fill: fill
});
annot.type = subtype;
return annot;
}
/**
* Creates a square annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* fill : the filling color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createSquareAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, fill = { r: 1, g: 1, b: 0 }) {
this.checkRect(4, rect);
let annot = this.createSquareCircleAnnotation(page, rect, contents, author, "/Square", color, fill);
this.annotations.push(annot);
}
/**
* Creates a circle annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* fill : the filling color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createCircleAnnotation(page, rect, contents, author, color = { r: 1, g: 1, b: 0 }, fill = { r: 1, g: 1, b: 0 }) {
this.checkRect(4, rect);
let annot = this.createSquareCircleAnnotation(page, rect, contents, author, "/Circle", color, fill);
this.annotations.push(annot);
}
/**
* Creates the base object of a polygon or polyline annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* vertices : the vertices defining the arrangement of the object
* subtyp: Polygon or PolyLine
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createPolygonPolyLineAnnotation(page, rect, contents, author, vertices, subtype, color = { r: 1, g: 1, b: 0 }) {
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
vertices: vertices
});
annot.type = subtype;
return annot;
}
/**
* Creates a polygon annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* vertices : the vertices defining the arrangement of the object
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createPolygonAnnotation(page, rect, contents, author, vertices, color = { r: 1, g: 1, b: 0 }) {
this.checkRect(4, rect);
let annot = this.createPolygonPolyLineAnnotation(page, rect, contents, author, vertices, "/Polygon", color);
this.annotations.push(annot);
}
/**
* Creates a polyline annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* vertices : the vertices defining the arrangement of the object
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createPolyLineAnnotation(page, rect, contents, author, vertices, color = { r: 1, g: 1, b: 0 }) {
this.checkRect(4, rect);
let annot = this.createPolygonPolyLineAnnotation(page, rect, contents, author, vertices, "/PolyLine", color);
this.annotations.push(annot);
}
/**
* Creates an ink annotation
* page : the number of the PDF document page, where the annotation must be attached
* rect : the position of the annotation on the page
* contents : the content of the annotation
* author : the author of the annotation
* inkList : a list of list containing the points for drawing the lines
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createInkAnnotation(page, rect, contents, author, inkList, color = { r: 0, g: 1, b: 0 }) {
if (inkList.length === 0)
throw Error("InkList is empty");
let _inkList = [];
if ('number' === typeof inkList[0]) {
_inkList = [inkList];
}
else {
_inkList = inkList;
}
let annot = Object.assign(this.createBaseAnnotation(page, rect, contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
inkList: _inkList
});
annot.type = "/Ink";
this.annotations.push(annot);
}
/**
* Creates a stamp annotation. There exists a number of predifined stamps that can be attached to PDF documents.
* page : the number of the PDF document page, where the annotation must be attached
* contents : the content of the annotation
* author : the author of the annotation
* stampType : the name of the used stamp type. Can be: [Approved, Experimental, NotApproved, AsIs, Expired, NotForPublicRelease, Confidential, Final, Sold, Departmental, ForComment, TopSecret, Draft, ForPublicRelease]
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createStampAnnotation(page, contents, author, stampType = "Draft", color = { r: 1, g: 1, b: 0 }) {
let annot = Object.assign(this.createBaseAnnotation(page, [50, 50, 80, 80], contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
stampType: stampType
});
annot.type = "/Stamp";
this.annotations.push(annot);
}
/**
* Creates a visual symbol that indcates the existance of text edits.
* page : the number of the PDF document page, where the annotation must be attached
* contents : the content of the annotation
* author : the author of the annotation
* caretSymbol : None or P, with P for using the paragraph symbol as caret
* color : the color of the annotation in rgb. Can be of domain 0 - 255 or 0 - 1
* */
createCaretAnnotation(page, contents, author, caretSymbol = "P", color = { r: 1, g: 1, b: 0 }) {
let annot = Object.assign(this.createBaseAnnotation(page, [], contents, author), {
opacity: 1,
initiallyOpen: false,
annotation_flag: 4,
color: color,
caretSymbol: caretSymbol
});
annot.type = "/Caret";
this.annotations.push(annot);
}
/**
* Deletes the annotation with the given id or the given reference object
* */
deleteAnnotation(id) {
return new Promise((resolve) => {
// delete if it was just created but is not in the pdf document
for (let i = 0; i < this.annotations.length; ++i) {
if ('string' === typeof id && this.annotations[i].id === id) {
this.annotations = this.annotations.slice(i, 1);
resolve(this.toDelete);
return;
}
else if (id.obj && this.annotations[i].object_id && id.obj === this.annotations[i].object_id.obj && id.generation && id.generation === this.annotations[i].object_id.generation) {
this.annotations = this.annotations.slice(i, 1);
resolve(this.toDelete);
return;
}
}
this.getAnnotations().then((annots) => {
for (let _annots of annots) {
for (let annot of _annots) {
if ('string' === typeof id && annot.id === id) {
this.toDelete.push(annot);
resolve(this.toDelete);
return;
}
else if (id.obj && annot.object_id && id.obj === annot.object_id.obj && id.generation && id.generation === annot.object_id.generation) {
this.toDelete.push(annot);
resolve(this.toDelete);
return;
}
}
}
});
});
}
/**
* Returns a promise with the resul of all annotations that are part of the document. This will
* comprise those that are already exists and those that were created using this library
* */
getAnnotations() {
return new Promise((resolve) => {
let existingAnnots = this.parser.extractAnnotations();
for (let newAnnot of this.annotations) {
existingAnnots[newAnnot.page].push(newAnnot);
}
resolve(existingAnnots);
});
}
createPopupAnnotation() {
throw Error("No yet implemented");
}
/**
* Downloads the adapted PDF document
* */
download(fileName = "output.pdf") {
let a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
let extended_pdf = this.write();
let blob = new Blob([extended_pdf], { type: "application/pdf" });
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
/**
* Saves the adapted PDF document in a nodejs environment
* */
// see https://github.com/angular/angular-cli/issues/9827
// why is it so reprehensible to create libraries for both environments (browser and nodejs)?
//
// uncomment it if you want to use it.
save(fileName = "output.pdf") {
// if (typeof process === 'undefined' && (<any>process).release.name !== 'node') {
// console.error('Use download() in a browser environment')
// return
// }
// const fs = require('fs')
// let data = this.write()
// fs.writeFile(fileName, Buffer.from(new Uint8Array(data)), (err: any) => {
// if (err) {
// return console.log(err);
// }
// console.log(`The file was written to: ${fileName}`);
// })
}
}
exports.AnnotationFactory = AnnotationFactory;
//# sourceMappingURL=annotation.js.map