@digital-blueprint/annotpdf
Version:
Javascript library for creating annotations in PDF documents
523 lines • 22.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
const object_util_1 = require("./object-util");
var generateDefaultTrailer = () => {
return { size: -1, root: { obj: -1, generation: -1 }, is_encrypted: false };
};
/**
* Holds the complete information of one update section in the Cross-Reference-Stream Object format.
*
* */
class CrossReferenceStreamObject {
constructor(data) {
this.data = data;
this.refs = [];
this.trailer = generateDefaultTrailer();
this.streamLength = -1;
this.w = [];
this.index = [];
this.start_pointer = 0;
}
/**
* Extracts a cross reference section that is a continuous definition of cross reference entries
* */
extractCrossReferenceSection(first_object_id, object_count, stream) {
let current_object_id = first_object_id;
for (let i = 0; i < object_count; ++i) {
let _type = stream.getNBytesAsNumber(this.w[0]);
let xref = undefined;
switch (_type) {
case 0:
xref = { id: current_object_id++, pointer: stream.getNBytesAsNumber(this.w[1]), generation: this.w[2] === 0 ? 0 : stream.getNBytesAsNumber(this.w[2]), free: true, update: false };
break;
case 1:
xref = { id: current_object_id++, pointer: stream.getNBytesAsNumber(this.w[1]), generation: this.w[2] === 0 ? 0 : stream.getNBytesAsNumber(this.w[2]), free: false, update: true };
break;
case 2:
// in this case the pointer becomes the stream object id that contains the compressed object and the generation represents the index of the object in the stream
xref = { id: current_object_id++, pointer: stream.getNBytesAsNumber(this.w[1]), generation: this.w[2] === 0 ? 0 : stream.getNBytesAsNumber(this.w[2]), free: true, update: false, compressed: true };
break;
}
if (xref)
this.refs.push(xref);
else
throw Error(`Invalid cross-reference-stream type ${_type}`);
}
}
/**
* Extracts the cross-reference-table from the stream
* */
extractStream(stream) {
let cross_reference_length = this.w.reduce((a, b) => a + b, 0);
// check if the data stream has a valid size
if (stream.getLength() !== cross_reference_length * this.index.filter((v, i) => i % 2 === 1).reduce((a, b) => a + b, 0))
throw Error(`Invalid stream length - is ${stream.getLength()} but should be ${cross_reference_length * this.index.filter((v, i) => i % 2 === 1).reduce((a, b) => a + b, 0)}`);
if (this.index.length % 2 === 1)
throw Error(`Invalid index flag ${this.index}`);
for (let i = 0; i < this.index.length; i += 2) {
this.extractCrossReferenceSection(this.index[i], this.index[i + 1], stream);
}
}
/**
* Parses the Cross-Reference-Stream-Object at the given index
* */
extract(xref) {
let index = xref.pointer;
this.start_pointer = index;
let crs_object = object_util_1.ObjectUtil.extractObject(this.data, xref);
let ptr_object_end = util_1.Util.locateSequence(util_1.Util.ENDOBJ, this.data, index);
this.data = this.data.slice(index, ptr_object_end);
// check type
if (crs_object.value["/Type"] !== "/XRef")
throw Error(`Invalid Cross-Reference-Stream-object type: ${crs_object.value["/Type"]}`);
// extract size
if (!crs_object.value["/Size"])
throw Error(`Invalid size value ${crs_object.value["/Size"]}`);
this.trailer.size = crs_object.value["/Size"];
// extract ROOT if it exists
if (crs_object.value["/Root"])
this.trailer.root = crs_object.value["/Root"];
// extract PREV if it exists
if (crs_object.value["/Prev"])
this.trailer.prev = crs_object.value["/Prev"];
// extract W parameter
this.w = crs_object.value["/W"];
if (!this.w || 0 === this.w.length)
throw Error("Invalid /W parameter in Cross-Reference-Stream-Object");
// extract Index parameter
this.index = crs_object.value["/Index"];
if (!this.index || 0 === this.index.length)
this.index = [0, crs_object.value["/Size"]];
if (!crs_object.stream)
throw Error("Missing stream at cross reference stream object");
let stream = crs_object.stream;
if (!stream)
throw Error("Invalid stream object");
this.streamLength = crs_object.value["/Length"];
this.extractStream(stream);
// the cross-reference-stream object is also a known reference
this.refs.push({ id: crs_object.id.obj, pointer: this.start_pointer, generation: crs_object.id.generation, free: false, update: true });
}
/**
* Returs the update section representing this CrossReferenceStreamObject
* */
getUpdateSection() {
return {
start_pointer: this.start_pointer,
size: this.trailer.size,
prev: this.trailer.prev,
root: this.trailer.root,
refs: this.refs,
is_encrypted: this.trailer.is_encrypted,
encrypt: this.trailer.encrypt,
id: this.trailer.id
};
}
}
exports.CrossReferenceStreamObject = CrossReferenceStreamObject;
/**
* Holds the complete information of one update section in the Cross-Reference-Table format. That comprises:
* - the body update
* - the crossiste reference table
* - the trailer
* */
class CrossReferenceTable {
constructor(data) {
this.data = data;
this.refs = [];
this.start_pointer = -1;
this.trailer = generateDefaultTrailer();
}
/**
* Returns the reference with the given id
* */
getReference(id) {
for (let ref of this.refs) {
if (ref.id === id)
return ref;
}
return undefined;
}
/**
* Returs the update section representing this CrossReferenceTable
* */
getUpdateSection() {
return {
start_pointer: this.start_pointer,
size: this.trailer.size,
refs: this.refs,
prev: this.trailer.prev,
root: this.trailer.root,
is_encrypted: this.trailer.is_encrypted,
encrypt: this.trailer.encrypt,
id: this.trailer.id
};
}
/**
* Extracts the header of a sub section. For instance
*
* 0 1 // <--
* ...
*
* So the obejct id 0 and the number of sub section entries 1
* */
extractSubSectionHeader(index) {
let ptr = util_1.Util.locateDelimiter(this.data, index);
let obj_id = util_1.Util.extractNumber(this.data, index, ptr).result;
ptr = util_1.Util.skipDelimiter(this.data, ptr + 1);
let ptr_ref_count = ptr;
ptr = util_1.Util.locateDelimiter(this.data, ptr);
let reference_count = util_1.Util.extractNumber(this.data, ptr_ref_count, ptr).result;
return { id: obj_id, count: reference_count, end_ptr: ptr };
}
/**
* Extracts the references of a sub section. The index points to the start of
* the first reference and count represents the number of references that are
* contained in the subsection.
*
* The first_object_id is the id provided in the sub section header
*
* By definition of the PDF standard one entry is 20 bytes long, but since the standard is rarely respected we better make it failsafe
* */
extractReferences(index, count, first_object_id) {
let _refs = [];
let res = { result: null, start_index: -1, end_index: index };
for (let i = 0; count === -1 || i < count; ++i) {
res = util_1.Util.readNextWord(this.data, res.end_index + 1);
let pointer = util_1.Util.extractNumber(res.result, 0).result;
res = util_1.Util.readNextWord(this.data, res.end_index + 1);
let generation = util_1.Util.extractNumber(res.result, 0).result;
res = util_1.Util.readNextWord(this.data, res.end_index + 1);
let ptr_flag = res.result;
let isFree = ptr_flag[0] === 102; // 102 = f
_refs.push({
id: first_object_id + i,
pointer: pointer,
generation: generation,
free: isFree,
update: !isFree
});
// if the word trailer occurs stop since we reached the end
if (this.data[util_1.Util.skipSpaces(this.data, res.end_index + 1)] === 116) {
break;
}
}
return { refs: _refs, end_index: res.end_index };
}
/**
* Extracts the trailer of the subsection that means the part stating with the 'trailer' keyword and
* in particular the trailer dictionary
* */
extractTrailer(index) {
// run forward to the dictionary start
index = util_1.Util.locateSequence(util_1.Util.DICT_START, this.data, index) + 2;
let obj = {};
object_util_1.ObjectUtil.extractDictKeyRec(this.data, index, obj);
return {
size: obj["/Size"],
root: obj["/Root"],
prev: obj["/Prev"] ? obj["/Prev"] : undefined,
is_encrypted: obj["/Encrypt"] ? true : false,
encrypt: obj["/Encrypt"] ? obj["/Encrypt"] : undefined,
id: obj["/ID"] ? obj["/ID"] : undefined
};
}
/**
* Parses the Cross Reference Table at the given index
* */
extract(index, skipXREFString = false) {
this.start_pointer = index;
let start_ptr = index;
if (!skipXREFString)
start_ptr += 5; // + length(xref) + blank
start_ptr = util_1.Util.skipDelimiter(this.data, start_ptr);
// check if there actually is a subsection header
// if the line finishes with an 'f' we know that it starts with the first cross reference entry
let tmp_ptr = start_ptr;
while (tmp_ptr < this.data.length && this.data[tmp_ptr] !== 102 && this.data[tmp_ptr] !== 13 && this.data[tmp_ptr] !== 10)
tmp_ptr++;
let first_header = { id: 0, count: -1, end_ptr: start_ptr - 1 };
if (this.data[tmp_ptr] === 10 || this.data[tmp_ptr] === 13)
first_header = this.extractSubSectionHeader(start_ptr);
let ref_start = util_1.Util.skipDelimiter(this.data, first_header.end_ptr + 1);
// extract first reference
let reference_result = this.extractReferences(ref_start, first_header.count, first_header.id);
this.refs = this.refs.concat(reference_result.refs);
// extract remaining references
start_ptr = util_1.Util.skipSpaces(this.data, reference_result.end_index + 1);
while (first_header.count > 0 && this.data[start_ptr] !== 116) { // 116 = 't' start of the word trailer that concludes the crosssite reference section
let header = this.extractSubSectionHeader(start_ptr);
ref_start = util_1.Util.skipDelimiter(this.data, header.end_ptr + 1);
let references = this.extractReferences(ref_start, header.count, header.id);
this.refs = this.refs.concat(references.refs);
start_ptr = util_1.Util.skipSpaces(this.data, references.end_index + 1);
}
this.trailer = this.extractTrailer(start_ptr);
}
}
exports.CrossReferenceTable = CrossReferenceTable;
/**
* Represents the complete PDF document history and therefore holds the
* updated body parts, the crosssite references and the document trailers
* */
class DocumentHistory {
constructor(data) {
this.data = data;
this.updates = [];
this.trailerSize = -1;
/**
* Holds object ids that were formerly freed and are now 'already' reused.
* This is used to prevent a freed object a second time */
this.already_reused_ids = [];
this.data = new Uint8Array(data);
}
/**
* Extracts the cross reference table starting at the given index
* */
extractCrossReferenceTable(index, skipXREFString = false) {
let crt = new CrossReferenceTable(this.data);
crt.extract(index, skipXREFString);
return crt;
}
/**
* Extracts the cross reference stream object starting at the given index
* */
extractCrossReferenceStreamObject(xref) {
let crs = new CrossReferenceStreamObject(this.data);
crs.extract(xref);
return crs;
}
/**
* Extracts the last update section of a document (that means
* the most recent update locating at the end of the file)
*
* Handles missing or wrong pointers
* and also decides, whether the cross reference table is provided as stream object or regular
* */
extractDocumentEntry() {
let ptr = this.data.length - 1;
let ptr_startxref = util_1.Util.locateSequenceReversed(util_1.Util.STARTXREF, this.data, ptr, true) + 9;
// identify cross reference section type
let section_type = "UNKNOWN";
let preceding_word_index = util_1.Util.skipSpacesReverse(this.data, ptr_startxref - 10);
if (util_1.Util.areArraysEqual(this.data.slice(preceding_word_index - 5, preceding_word_index + 1), util_1.Util.ENDOBJ)) {
section_type = "stream";
}
else {
section_type = "trailer";
}
// try to locate cross reference table manually
let locateXREFStartManually = () => {
let new_ptr = util_1.Util.locateSequenceReversed(util_1.Util.XREF, this.data, this.data.length);
section_type = "trailer";
while (new_ptr > 0 && this.data[new_ptr - 1] === 116) { // 116 = 't' -> we are looking for 'xref' not 'startxref'
new_ptr = util_1.Util.locateSequenceReversed(util_1.Util.XREF, this.data, new_ptr - 1);
}
if (new_ptr === -1) { // than we try to identify the word 'trailer' and run backwards as long as we find a symbol that is not a number or 'f' or 'n' - what could possibly go wrong
section_type = "trailer_without_xref_start";
new_ptr = util_1.Util.locateSequenceReversed(util_1.Util.TRAILER, this.data, this.data.length);
if (new_ptr > 0) {
new_ptr--;
while (new_ptr > 0 && (util_1.Util.isSpace(this.data[new_ptr]) || util_1.Util.isNumber(this.data[new_ptr]) || this.data[new_ptr] === 110 || //110 = 'n' 102 = 'f'
this.data[new_ptr] === 102))
--new_ptr;
new_ptr = util_1.Util.skipSpaces(this.data, new_ptr + 1);
}
}
return { pointer: new_ptr, sectionType: section_type };
};
try {
ptr = util_1.Util.extractNumber(this.data, ptr_startxref).result;
}
catch (err) {
return locateXREFStartManually();
}
if (ptr > this.data.length) {
return locateXREFStartManually();
}
// start section with XREF?
if (section_type !== "stream" && !(this.data[ptr] === util_1.Util.XREF[0] && this.data[ptr + 1] === util_1.Util.XREF[1] && this.data[ptr + 2] === util_1.Util.XREF[2] && this.data[ptr + 3] === util_1.Util.XREF[3])) {
return locateXREFStartManually();
}
return { pointer: ptr, sectionType: section_type };
}
/**
* Extracts the entire update sections
*
* Needs to adapt depending whether the document uses a cross-reference table or a cross-reference stream object
* */
extractDocumentHistory() {
let document_entry = this.extractDocumentEntry();
let ptr = document_entry.pointer;
if (ptr === -1) {
throw Error("Could not locate document entry");
}
let xref = {
id: -1,
pointer: ptr,
generation: 0,
free: false,
update: true
};
this.extractCrossReferenceTables(document_entry, xref);
// adapt pointer in case there is junk before the header
let pdf_header_start = util_1.Util.locateSequence(util_1.Util.VERSION, this.data, 0);
if (pdf_header_start !== 0 && pdf_header_start !== -1) {
for (let updateSection of this.updates) {
for (let ref of updateSection.refs) {
ref.pointer += pdf_header_start;
}
}
}
}
/**
* Extracts the cross reference tables of the entire document
* */
extractCrossReferenceTables(document_entry, xref) {
let ptr = document_entry.pointer;
// Handle cross reference table
if (document_entry.sectionType === "trailer") {
let crt = this.extractCrossReferenceTable(ptr);
this.updates.push(crt.getUpdateSection());
let us = this.updates[0];
while (us.prev) {
crt = this.extractCrossReferenceTable(us.prev);
this.updates.push(crt.getUpdateSection());
us = this.updates[this.updates.length - 1];
}
}
else if (document_entry.sectionType === "stream") { // handle cross reference stream object
let crs = this.extractCrossReferenceStreamObject(xref);
this.updates.push(crs.getUpdateSection());
let us = this.updates[0];
while (us.prev) {
let _xref = {
id: -1,
pointer: us.prev,
generation: 0,
free: false,
update: true
};
crs = this.extractCrossReferenceStreamObject(_xref);
this.updates.push(crs.getUpdateSection());
us = this.updates[this.updates.length - 1];
}
}
else if (document_entry.sectionType === "trailer_without_xref_start") {
let crt = this.extractCrossReferenceTable(ptr, true);
this.updates.push(crt.getUpdateSection());
let us = this.updates[0];
while (us.prev) {
crt = this.extractCrossReferenceTable(us.prev);
this.updates.push(crt.getUpdateSection());
us = this.updates[this.updates.length - 1];
}
}
else {
throw Error("Could not part cross reference table");
}
this.trailerSize = this.extractReferenceNumberCount();
}
/**
* Counts the number of specified objects
* */
extractReferenceNumberCount() {
let visited = [];
let count = 0;
for (let update of this.updates) {
for (let ref of update.refs) {
if (!visited.includes(ref.id)) {
count++;
visited.push(ref.id);
}
}
}
return count;
}
/**
* Primarily for clarification. The first element is the most recent. We parsed backwards.
* */
getRecentUpdate() {
return this.updates[0];
}
/**
* Indicates whether the PDF document is encrypted
* */
isEncrypted() {
return this.getRecentUpdate().is_encrypted;
}
/**
* By running through the PDf history we can for every object id determine the pointer address to the most recent version, and
* whether the object id is still in used.
*
* So the object lookup table has an entry for every existing object id, a pointer to the the most recent object definition, as long
* as the object exists, or an according indication otherwise.
* */
createObjectLookupTable() {
let objTable = {};
let update = this.getRecentUpdate();
let i = 1;
while (Object.keys(objTable).length < this.extractReferenceNumberCount()) {
let refs = update.refs;
for (let ref of refs) {
if (!objTable.hasOwnProperty(ref.id)) {
objTable[ref.id] = ref;
}
}
update = this.updates[i];
++i;
}
return objTable;
}
/**
* Returns the new object id. It primarily tries to reuse an existing id, but if no such exists it will return a
* new one
* */
getFreeObjectId() {
let objectLookupTable = this.createObjectLookupTable();
let free_header = objectLookupTable[0];
if (!free_header)
throw Error("Crosssite reference has no header for the linked list of free objects");
// if the pointer of object 0 points to 0 there is no freed object that can be reused
if (0 === free_header.pointer) {
if (-1 === this.trailerSize)
throw Error("Trailer size not set");
return { obj: this.trailerSize++, generation: 0, reused: false };
}
// get list head
let ptr = free_header.pointer;
let freedHeaderList = [];
while (ptr !== 0) {
freedHeaderList.push(free_header);
free_header = objectLookupTable[ptr];
if (!free_header.free) {
// handle the case of an incosistent xref
return { obj: this.trailerSize++, generation: 0, reused: false };
}
ptr = free_header.pointer;
}
let getFreeHeader = (freeHeaderList) => {
for (let p of freeHeaderList.reverse()) {
if (p.generation < 65535 && // max generation number
-1 === this.already_reused_ids.indexOf(p)) { // not already reused
return p;
}
}
return undefined;
};
let reused_free_header = getFreeHeader(freedHeaderList);
if (reused_free_header) {
free_header = reused_free_header;
// store used id to make sure it will not be selected again
this.already_reused_ids.push(free_header);
}
else {
// handle the case that all freed object ids are already reused
return { obj: this.trailerSize++, generation: 0, reused: false };
}
return { obj: free_header.pointer, generation: objectLookupTable[free_header.id].generation, reused: true };
}
}
exports.DocumentHistory = DocumentHistory;
//# sourceMappingURL=document-history.js.map