Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)

FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 12:15:39 +00:00
commit 8d688b459d
10003 changed files with 1928395 additions and 0 deletions

299
frontend/node_modules/pdf-lib/src/core/PDFContext.ts generated vendored Normal file
View file

@ -0,0 +1,299 @@
import pako from 'pako';
import PDFHeader from 'src/core/document/PDFHeader';
import { UnexpectedObjectTypeError } from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFOperator from 'src/core/operators/PDFOperator';
import Ops from 'src/core/operators/PDFOperatorNames';
import PDFContentStream from 'src/core/structures/PDFContentStream';
import { typedArrayFor } from 'src/utils';
import { SimpleRNG } from 'src/utils/rng';
type LookupKey = PDFRef | PDFObject | undefined;
interface LiteralObject {
[name: string]: Literal | PDFObject;
}
interface LiteralArray {
[index: number]: Literal | PDFObject;
}
type Literal =
| LiteralObject
| LiteralArray
| string
| number
| boolean
| null
| undefined;
const byAscendingObjectNumber = (
[a]: [PDFRef, PDFObject],
[b]: [PDFRef, PDFObject],
) => a.objectNumber - b.objectNumber;
class PDFContext {
static create = () => new PDFContext();
largestObjectNumber: number;
header: PDFHeader;
trailerInfo: {
Root?: PDFObject;
Encrypt?: PDFObject;
Info?: PDFObject;
ID?: PDFObject;
};
rng: SimpleRNG;
private readonly indirectObjects: Map<PDFRef, PDFObject>;
private pushGraphicsStateContentStreamRef?: PDFRef;
private popGraphicsStateContentStreamRef?: PDFRef;
private constructor() {
this.largestObjectNumber = 0;
this.header = PDFHeader.forVersion(1, 7);
this.trailerInfo = {};
this.indirectObjects = new Map();
this.rng = SimpleRNG.withSeed(1);
}
assign(ref: PDFRef, object: PDFObject): void {
this.indirectObjects.set(ref, object);
if (ref.objectNumber > this.largestObjectNumber) {
this.largestObjectNumber = ref.objectNumber;
}
}
nextRef(): PDFRef {
this.largestObjectNumber += 1;
return PDFRef.of(this.largestObjectNumber);
}
register(object: PDFObject): PDFRef {
const ref = this.nextRef();
this.assign(ref, object);
return ref;
}
delete(ref: PDFRef): boolean {
return this.indirectObjects.delete(ref);
}
lookupMaybe(ref: LookupKey, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
ref: LookupKey,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFName): PDFName | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
ref: LookupKey,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(ref: LookupKey, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const result = ref instanceof PDFRef ? this.indirectObjects.get(ref) : ref;
if (!result || (result === PDFNull && !preservePDFNull)) return undefined;
for (let idx = 0, len = types.length; idx < len; idx++) {
const type = types[idx];
if (type === PDFNull) {
if (result === PDFNull) return result;
} else {
if (result instanceof type) return result;
}
}
throw new UnexpectedObjectTypeError(types, result);
}
lookup(ref: LookupKey): PDFObject | undefined;
lookup(ref: LookupKey, type: typeof PDFArray): PDFArray;
lookup(ref: LookupKey, type: typeof PDFBool): PDFBool;
lookup(ref: LookupKey, type: typeof PDFDict): PDFDict;
lookup(ref: LookupKey, type: typeof PDFHexString): PDFHexString;
lookup(ref: LookupKey, type: typeof PDFName): PDFName;
lookup(ref: LookupKey, type: typeof PDFNull): typeof PDFNull;
lookup(ref: LookupKey, type: typeof PDFNumber): PDFNumber;
lookup(ref: LookupKey, type: typeof PDFStream): PDFStream;
lookup(ref: LookupKey, type: typeof PDFRef): PDFRef;
lookup(ref: LookupKey, type: typeof PDFString): PDFString;
lookup(
ref: LookupKey,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(ref: LookupKey, ...types: any[]) {
const result = ref instanceof PDFRef ? this.indirectObjects.get(ref) : ref;
if (types.length === 0) return result;
for (let idx = 0, len = types.length; idx < len; idx++) {
const type = types[idx];
if (type === PDFNull) {
if (result === PDFNull) return result;
} else {
if (result instanceof type) return result;
}
}
throw new UnexpectedObjectTypeError(types, result);
}
getObjectRef(pdfObject: PDFObject): PDFRef | undefined {
const entries = Array.from(this.indirectObjects.entries());
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [ref, object] = entries[idx];
if (object === pdfObject) {
return ref;
}
}
return undefined;
}
enumerateIndirectObjects(): [PDFRef, PDFObject][] {
return Array.from(this.indirectObjects.entries()).sort(
byAscendingObjectNumber,
);
}
obj(literal: null | undefined): typeof PDFNull;
obj(literal: string): PDFName;
obj(literal: number): PDFNumber;
obj(literal: boolean): PDFBool;
obj(literal: LiteralObject): PDFDict;
obj(literal: LiteralArray): PDFArray;
obj(literal: Literal) {
if (literal instanceof PDFObject) {
return literal;
} else if (literal === null || literal === undefined) {
return PDFNull;
} else if (typeof literal === 'string') {
return PDFName.of(literal);
} else if (typeof literal === 'number') {
return PDFNumber.of(literal);
} else if (typeof literal === 'boolean') {
return literal ? PDFBool.True : PDFBool.False;
} else if (Array.isArray(literal)) {
const array = PDFArray.withContext(this);
for (let idx = 0, len = literal.length; idx < len; idx++) {
array.push(this.obj(literal[idx]));
}
return array;
} else {
const dict = PDFDict.withContext(this);
const keys = Object.keys(literal);
for (let idx = 0, len = keys.length; idx < len; idx++) {
const key = keys[idx];
const value = (literal as LiteralObject)[key] as any;
if (value !== undefined) dict.set(PDFName.of(key), this.obj(value));
}
return dict;
}
}
stream(
contents: string | Uint8Array,
dict: LiteralObject = {},
): PDFRawStream {
return PDFRawStream.of(this.obj(dict), typedArrayFor(contents));
}
flateStream(
contents: string | Uint8Array,
dict: LiteralObject = {},
): PDFRawStream {
return this.stream(pako.deflate(typedArrayFor(contents)), {
...dict,
Filter: 'FlateDecode',
});
}
contentStream(
operators: PDFOperator[],
dict: LiteralObject = {},
): PDFContentStream {
return PDFContentStream.of(this.obj(dict), operators);
}
formXObject(
operators: PDFOperator[],
dict: LiteralObject = {},
): PDFContentStream {
return this.contentStream(operators, {
BBox: this.obj([0, 0, 0, 0]),
Matrix: this.obj([1, 0, 0, 1, 0, 0]),
...dict,
Type: 'XObject',
Subtype: 'Form',
});
}
/*
* Reference to PDFContentStream that contains a single PDFOperator: `q`.
* Used by [[PDFPageLeaf]] instances to ensure that when content streams are
* added to a modified PDF, they start in the default, unchanged graphics
* state.
*/
getPushGraphicsStateContentStream(): PDFRef {
if (this.pushGraphicsStateContentStreamRef) {
return this.pushGraphicsStateContentStreamRef;
}
const dict = this.obj({});
const op = PDFOperator.of(Ops.PushGraphicsState);
const stream = PDFContentStream.of(dict, [op]);
this.pushGraphicsStateContentStreamRef = this.register(stream);
return this.pushGraphicsStateContentStreamRef;
}
/*
* Reference to PDFContentStream that contains a single PDFOperator: `Q`.
* Used by [[PDFPageLeaf]] instances to ensure that when content streams are
* added to a modified PDF, they start in the default, unchanged graphics
* state.
*/
getPopGraphicsStateContentStream(): PDFRef {
if (this.popGraphicsStateContentStreamRef) {
return this.popGraphicsStateContentStreamRef;
}
const dict = this.obj({});
const op = PDFOperator.of(Ops.PopGraphicsState);
const stream = PDFContentStream.of(dict, [op]);
this.popGraphicsStateContentStreamRef = this.register(stream);
return this.popGraphicsStateContentStreamRef;
}
addRandomSuffix(prefix: string, suffixLength = 4): string {
return `${prefix}-${Math.floor(this.rng.nextInt() * 10 ** suffixLength)}`;
}
}
export default PDFContext;

View file

@ -0,0 +1,143 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
/**
* PDFObjectCopier copies PDFObjects from a src context to a dest context.
* The primary use case for this is to copy pages between PDFs.
*
* _Copying_ an object with a PDFObjectCopier is different from _cloning_ an
* object with its [[PDFObject.clone]] method:
*
* ```
* const src: PDFContext = ...
* const dest: PDFContext = ...
* const originalObject: PDFObject = ...
* const copiedObject = PDFObjectCopier.for(src, dest).copy(originalObject);
* const clonedObject = originalObject.clone();
* ```
*
* Copying an object is equivalent to cloning it and then copying over any other
* objects that it references. Note that only dictionaries, arrays, and streams
* (or structures build from them) can contain indirect references to other
* objects. Copying a PDFObject that is not a dictionary, array, or stream is
* supported, but is equivalent to cloning it.
*/
class PDFObjectCopier {
static for = (src: PDFContext, dest: PDFContext) =>
new PDFObjectCopier(src, dest);
private readonly src: PDFContext;
private readonly dest: PDFContext;
private readonly traversedObjects = new Map<PDFObject, PDFObject>();
private constructor(src: PDFContext, dest: PDFContext) {
this.src = src;
this.dest = dest;
}
// prettier-ignore
copy = <T extends PDFObject>(object: T): T => (
object instanceof PDFPageLeaf ? this.copyPDFPage(object)
: object instanceof PDFDict ? this.copyPDFDict(object)
: object instanceof PDFArray ? this.copyPDFArray(object)
: object instanceof PDFStream ? this.copyPDFStream(object)
: object instanceof PDFRef ? this.copyPDFIndirectObject(object)
: object.clone()
) as T;
private copyPDFPage = (originalPage: PDFPageLeaf): PDFPageLeaf => {
const clonedPage = originalPage.clone();
// Move any entries that the originalPage is inheriting from its parent
// tree nodes directly into originalPage so they are preserved during
// the copy.
const { InheritableEntries } = PDFPageLeaf;
for (let idx = 0, len = InheritableEntries.length; idx < len; idx++) {
const key = PDFName.of(InheritableEntries[idx]);
const value = clonedPage.getInheritableAttribute(key)!;
if (!clonedPage.get(key) && value) clonedPage.set(key, value);
}
// Remove the parent reference to prevent the whole donor document's page
// tree from being copied when we only need a single page.
clonedPage.delete(PDFName.of('Parent'));
return this.copyPDFDict(clonedPage) as PDFPageLeaf;
};
private copyPDFDict = (originalDict: PDFDict): PDFDict => {
if (this.traversedObjects.has(originalDict)) {
return this.traversedObjects.get(originalDict) as PDFDict;
}
const clonedDict = originalDict.clone(this.dest);
this.traversedObjects.set(originalDict, clonedDict);
const entries = originalDict.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clonedDict.set(key, this.copy(value));
}
return clonedDict;
};
private copyPDFArray = (originalArray: PDFArray): PDFArray => {
if (this.traversedObjects.has(originalArray)) {
return this.traversedObjects.get(originalArray) as PDFArray;
}
const clonedArray = originalArray.clone(this.dest);
this.traversedObjects.set(originalArray, clonedArray);
for (let idx = 0, len = originalArray.size(); idx < len; idx++) {
const value = originalArray.get(idx);
clonedArray.set(idx, this.copy(value));
}
return clonedArray;
};
private copyPDFStream = (originalStream: PDFStream): PDFStream => {
if (this.traversedObjects.has(originalStream)) {
return this.traversedObjects.get(originalStream) as PDFStream;
}
const clonedStream = originalStream.clone(this.dest);
this.traversedObjects.set(originalStream, clonedStream);
const entries = originalStream.dict.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clonedStream.dict.set(key, this.copy(value));
}
return clonedStream;
};
private copyPDFIndirectObject = (ref: PDFRef): PDFRef => {
const alreadyMapped = this.traversedObjects.has(ref);
if (!alreadyMapped) {
const newRef = this.dest.nextRef();
this.traversedObjects.set(ref, newRef);
const dereferencedValue = this.src.lookup(ref);
if (dereferencedValue) {
const cloned = this.copy(dereferencedValue);
this.dest.assign(newRef, cloned);
}
}
return this.traversedObjects.get(ref) as PDFRef;
};
}
export default PDFObjectCopier;

View file

@ -0,0 +1,114 @@
import PDFObject from 'src/core/objects/PDFObject';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import { IndexOutOfBoundsError } from 'src/core/errors';
class PDFAcroButton extends PDFAcroTerminal {
Opt(): PDFString | PDFHexString | PDFArray | undefined {
return this.dict.lookupMaybe(
PDFName.of('Opt'),
PDFString,
PDFHexString,
PDFArray,
);
}
setOpt(opt: PDFObject[]) {
this.dict.set(PDFName.of('Opt'), this.dict.context.obj(opt));
}
getExportValues(): (PDFString | PDFHexString)[] | undefined {
const opt = this.Opt();
if (!opt) return undefined;
if (opt instanceof PDFString || opt instanceof PDFHexString) {
return [opt];
}
const values: (PDFString | PDFHexString)[] = [];
for (let idx = 0, len = opt.size(); idx < len; idx++) {
const value = opt.lookup(idx);
if (value instanceof PDFString || value instanceof PDFHexString) {
values.push(value);
}
}
return values;
}
removeExportValue(idx: number) {
const opt = this.Opt();
if (!opt) return;
if (opt instanceof PDFString || opt instanceof PDFHexString) {
if (idx !== 0) throw new IndexOutOfBoundsError(idx, 0, 0);
this.setOpt([]);
} else {
if (idx < 0 || idx > opt.size()) {
throw new IndexOutOfBoundsError(idx, 0, opt.size());
}
opt.remove(idx);
}
}
// Enforce use use of /Opt even if it isn't strictly necessary
normalizeExportValues() {
const exportValues = this.getExportValues() ?? [];
const Opt: (PDFString | PDFHexString)[] = [];
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const exportVal =
exportValues[idx] ??
PDFHexString.fromText(widget.getOnValue()?.decodeText() ?? '');
Opt.push(exportVal);
}
this.setOpt(Opt);
}
/**
* Reuses existing opt if one exists with the same value (assuming
* `useExistingIdx` is `true`). Returns index of existing (or new) opt.
*/
addOpt(opt: PDFHexString | PDFString, useExistingOptIdx: boolean): number {
this.normalizeExportValues();
const optText = opt.decodeText();
let existingIdx: number | undefined;
if (useExistingOptIdx) {
const exportValues = this.getExportValues() ?? [];
for (let idx = 0, len = exportValues.length; idx < len; idx++) {
const exportVal = exportValues[idx];
if (exportVal.decodeText() === optText) existingIdx = idx;
}
}
const Opt = this.Opt() as PDFArray;
Opt.push(opt);
return existingIdx ?? Opt.size() - 1;
}
addWidgetWithOpt(
widget: PDFRef,
opt: PDFHexString | PDFString,
useExistingOptIdx: boolean,
) {
const optIdx = this.addOpt(opt, useExistingOptIdx);
const apStateValue = PDFName.of(String(optIdx));
this.addWidget(widget);
return apStateValue;
}
}
export default PDFAcroButton;

View file

@ -0,0 +1,49 @@
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import { InvalidAcroFieldValueError } from 'src/core/errors';
class PDFAcroCheckBox extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroCheckBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroCheckBox(dict, ref);
};
setValue(value: PDFName) {
const onValue = this.getOnValue() ?? PDFName.of('Yes');
if (value !== onValue && value !== PDFName.of('Off')) {
throw new InvalidAcroFieldValueError();
}
this.dict.set(PDFName.of('V'), value);
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getOnValue() === value ? value : PDFName.of('Off');
widget.setAppearanceState(state);
}
}
getValue(): PDFName {
const v = this.V();
if (v instanceof PDFName) return v;
return PDFName.of('Off');
}
getOnValue(): PDFName | undefined {
const [widget] = this.getWidgets();
return widget?.getOnValue();
}
}
export default PDFAcroCheckBox;

View file

@ -0,0 +1,153 @@
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFString from 'src/core/objects/PDFString';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import { AcroChoiceFlags } from 'src/core/acroform/flags';
import {
InvalidAcroFieldValueError,
MultiSelectValueError,
} from 'src/core/errors';
class PDFAcroChoice extends PDFAcroTerminal {
setValues(values: (PDFString | PDFHexString)[]) {
if (
this.hasFlag(AcroChoiceFlags.Combo) &&
!this.hasFlag(AcroChoiceFlags.Edit) &&
!this.valuesAreValid(values)
) {
throw new InvalidAcroFieldValueError();
}
if (values.length === 0) {
this.dict.delete(PDFName.of('V'));
}
if (values.length === 1) {
this.dict.set(PDFName.of('V'), values[0]);
}
if (values.length > 1) {
if (!this.hasFlag(AcroChoiceFlags.MultiSelect)) {
throw new MultiSelectValueError();
}
this.dict.set(PDFName.of('V'), this.dict.context.obj(values));
}
this.updateSelectedIndices(values);
}
valuesAreValid(values: (PDFString | PDFHexString)[]): boolean {
const options = this.getOptions();
for (let idx = 0, len = values.length; idx < len; idx++) {
const val = values[idx].decodeText();
if (!options.find((o) => val === (o.display || o.value).decodeText())) {
return false;
}
}
return true;
}
updateSelectedIndices(values: (PDFString | PDFHexString)[]) {
if (values.length > 1) {
const indices = new Array<number>(values.length);
const options = this.getOptions();
for (let idx = 0, len = values.length; idx < len; idx++) {
const val = values[idx].decodeText();
indices[idx] = options.findIndex(
(o) => val === (o.display || o.value).decodeText(),
);
}
this.dict.set(PDFName.of('I'), this.dict.context.obj(indices.sort()));
} else {
this.dict.delete(PDFName.of('I'));
}
}
getValues(): (PDFString | PDFHexString)[] {
const v = this.V();
if (v instanceof PDFString || v instanceof PDFHexString) return [v];
if (v instanceof PDFArray) {
const values: (PDFString | PDFHexString)[] = [];
for (let idx = 0, len = v.size(); idx < len; idx++) {
const value = v.lookup(idx);
if (value instanceof PDFString || value instanceof PDFHexString) {
values.push(value);
}
}
return values;
}
return [];
}
Opt(): PDFArray | PDFString | PDFHexString | undefined {
return this.dict.lookupMaybe(
PDFName.of('Opt'),
PDFString,
PDFHexString,
PDFArray,
);
}
setOptions(
options: {
value: PDFString | PDFHexString;
display?: PDFString | PDFHexString;
}[],
) {
const newOpt = new Array<PDFArray>(options.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
const { value, display } = options[idx];
newOpt[idx] = this.dict.context.obj([value, display || value]);
}
this.dict.set(PDFName.of('Opt'), this.dict.context.obj(newOpt));
}
getOptions(): {
value: PDFString | PDFHexString;
display: PDFString | PDFHexString;
}[] {
const Opt = this.Opt();
// Not supposed to happen - Opt _should_ always be `PDFArray | undefined`
if (Opt instanceof PDFString || Opt instanceof PDFHexString) {
return [{ value: Opt, display: Opt }];
}
if (Opt instanceof PDFArray) {
const res: {
value: PDFString | PDFHexString;
display: PDFString | PDFHexString;
}[] = [];
for (let idx = 0, len = Opt.size(); idx < len; idx++) {
const item = Opt.lookup(idx);
// If `item` is a string, use that as both the export and text value
if (item instanceof PDFString || item instanceof PDFHexString) {
res.push({ value: item, display: item });
}
// If `item` is an array of one, treat it the same as just a string,
// if it's an array of two then `item[0]` is the export value and
// `item[1]` is the text value
if (item instanceof PDFArray) {
if (item.size() > 0) {
const first = item.lookup(0, PDFString, PDFHexString);
const second = item.lookupMaybe(1, PDFString, PDFHexString);
res.push({ value: first, display: second || first });
}
}
}
return res;
}
return [];
}
}
export default PDFAcroChoice;

View file

@ -0,0 +1,22 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import { AcroChoiceFlags } from 'src/core/acroform/flags';
class PDFAcroComboBox extends PDFAcroChoice {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroComboBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Ch',
Ff: AcroChoiceFlags.Combo,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroComboBox(dict, ref);
};
}
export default PDFAcroComboBox;

View file

@ -0,0 +1,167 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import { findLastMatch } from 'src/utils';
import { MissingDAEntryError, MissingTfOperatorError } from 'src/core/errors';
// Examples:
// `/Helv 12 Tf` -> ['Helv', '12']
// `/HeBo 8.00 Tf` -> ['HeBo', '8.00']
// `/HeBo Tf` -> ['HeBo', undefined]
const tfRegex = /\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+Tf/;
class PDFAcroField {
readonly dict: PDFDict;
readonly ref: PDFRef;
protected constructor(dict: PDFDict, ref: PDFRef) {
this.dict = dict;
this.ref = ref;
}
T(): PDFString | PDFHexString | undefined {
return this.dict.lookupMaybe(PDFName.of('T'), PDFString, PDFHexString);
}
Ff(): PDFNumber | undefined {
const numberOrRef = this.getInheritableAttribute(PDFName.of('Ff'));
return this.dict.context.lookupMaybe(numberOrRef, PDFNumber);
}
V(): PDFObject | undefined {
const valueOrRef = this.getInheritableAttribute(PDFName.of('V'));
return this.dict.context.lookup(valueOrRef);
}
Kids(): PDFArray | undefined {
return this.dict.lookupMaybe(PDFName.of('Kids'), PDFArray);
}
// Parent(): PDFDict | undefined {
// return this.dict.lookupMaybe(PDFName.of('Parent'), PDFDict);
// }
DA(): PDFString | PDFHexString | undefined {
const da = this.dict.lookup(PDFName.of('DA'));
if (da instanceof PDFString || da instanceof PDFHexString) return da;
return undefined;
}
setKids(kids: PDFObject[]) {
this.dict.set(PDFName.of('Kids'), this.dict.context.obj(kids));
}
getParent(): PDFAcroField | undefined {
// const parent = this.Parent();
// if (!parent) return undefined;
// return new PDFAcroField(parent);
const parentRef = this.dict.get(PDFName.of('Parent'));
if (parentRef instanceof PDFRef) {
const parent = this.dict.lookup(PDFName.of('Parent'), PDFDict);
return new PDFAcroField(parent, parentRef);
}
return undefined;
}
setParent(parent: PDFRef | undefined) {
if (!parent) this.dict.delete(PDFName.of('Parent'));
else this.dict.set(PDFName.of('Parent'), parent);
}
getFullyQualifiedName(): string | undefined {
const parent = this.getParent();
if (!parent) return this.getPartialName();
return `${parent.getFullyQualifiedName()}.${this.getPartialName()}`;
}
getPartialName(): string | undefined {
return this.T()?.decodeText();
}
setPartialName(partialName: string | undefined) {
if (!partialName) this.dict.delete(PDFName.of('T'));
else this.dict.set(PDFName.of('T'), PDFHexString.fromText(partialName));
}
setDefaultAppearance(appearance: string) {
this.dict.set(PDFName.of('DA'), PDFString.of(appearance));
}
getDefaultAppearance(): string | undefined {
const DA = this.DA();
if (DA instanceof PDFHexString) {
return DA.decodeText();
}
return DA?.asString();
}
setFontSize(fontSize: number) {
const name = this.getFullyQualifiedName() ?? '';
const da = this.getDefaultAppearance();
if (!da) throw new MissingDAEntryError(name);
const daMatch = findLastMatch(da, tfRegex);
if (!daMatch.match) throw new MissingTfOperatorError(name);
const daStart = da.slice(0, daMatch.pos - daMatch.match[0].length);
const daEnd = daMatch.pos <= da.length ? da.slice(daMatch.pos) : '';
const fontName = daMatch.match[1];
const modifiedDa = `${daStart} /${fontName} ${fontSize} Tf ${daEnd}`;
this.setDefaultAppearance(modifiedDa);
}
getFlags(): number {
return this.Ff()?.asNumber() ?? 0;
}
setFlags(flags: number) {
this.dict.set(PDFName.of('Ff'), PDFNumber.of(flags));
}
hasFlag(flag: number): boolean {
const flags = this.getFlags();
return (flags & flag) !== 0;
}
setFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags | flag);
}
clearFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags & ~flag);
}
setFlagTo(flag: number, enable: boolean) {
if (enable) this.setFlag(flag);
else this.clearFlag(flag);
}
getInheritableAttribute(name: PDFName): PDFObject | undefined {
let attribute: PDFObject | undefined;
this.ascend((node) => {
if (!attribute) attribute = node.dict.get(name);
});
return attribute;
}
ascend(visitor: (node: PDFAcroField) => any): void {
visitor(this);
const parent = this.getParent();
if (parent) parent.ascend(visitor);
}
}
export default PDFAcroField;

View file

@ -0,0 +1,102 @@
import PDFContext from 'src/core/PDFContext';
import PDFDict from 'src/core/objects/PDFDict';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFAcroNonTerminal from 'src/core/acroform/PDFAcroNonTerminal';
import {
createPDFAcroField,
createPDFAcroFields,
} from 'src/core/acroform/utils';
class PDFAcroForm {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict) => new PDFAcroForm(dict);
static create = (context: PDFContext) => {
const dict = context.obj({ Fields: [] });
return new PDFAcroForm(dict);
};
private constructor(dict: PDFDict) {
this.dict = dict;
}
Fields(): PDFArray | undefined {
const fields = this.dict.lookup(PDFName.of('Fields'));
if (fields instanceof PDFArray) return fields;
return undefined;
}
getFields(): [PDFAcroField, PDFRef][] {
const { Fields } = this.normalizedEntries();
const fields = new Array(Fields.size());
for (let idx = 0, len = Fields.size(); idx < len; idx++) {
const ref = Fields.get(idx) as PDFRef;
const dict = Fields.lookup(idx, PDFDict);
fields[idx] = [createPDFAcroField(dict, ref), ref];
}
return fields;
}
getAllFields(): [PDFAcroField, PDFRef][] {
const allFields: [PDFAcroField, PDFRef][] = [];
const pushFields = (fields?: [PDFAcroField, PDFRef][]) => {
if (!fields) return;
for (let idx = 0, len = fields.length; idx < len; idx++) {
const field = fields[idx];
allFields.push(field);
const [fieldModel] = field;
if (fieldModel instanceof PDFAcroNonTerminal) {
pushFields(createPDFAcroFields(fieldModel.Kids()));
}
}
};
pushFields(this.getFields());
return allFields;
}
addField(field: PDFRef) {
const { Fields } = this.normalizedEntries();
Fields?.push(field);
}
removeField(field: PDFAcroField): void {
const parent = field.getParent();
const fields =
parent === undefined ? this.normalizedEntries().Fields : parent.Kids();
const index = fields?.indexOf(field.ref);
if (fields === undefined || index === undefined) {
throw new Error(
`Tried to remove inexistent field ${field.getFullyQualifiedName()}`,
);
}
fields.remove(index);
if (parent !== undefined && fields.size() === 0) {
this.removeField(parent);
}
}
normalizedEntries() {
let Fields = this.Fields();
if (!Fields) {
Fields = this.dict.context.obj([]);
this.dict.set(PDFName.of('Fields'), Fields);
}
return { Fields };
}
}
export default PDFAcroForm;

View file

@ -0,0 +1,20 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
class PDFAcroListBox extends PDFAcroChoice {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroListBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Ch',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroListBox(dict, ref);
};
}
export default PDFAcroListBox;

View file

@ -0,0 +1,34 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFRef from 'src/core/objects/PDFRef';
import PDFName from 'src/core/objects/PDFName';
import PDFContext from 'src/core/PDFContext';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
class PDFAcroNonTerminal extends PDFAcroField {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroNonTerminal(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({});
const ref = context.register(dict);
return new PDFAcroNonTerminal(dict, ref);
};
addField(field: PDFRef) {
const { Kids } = this.normalizedEntries();
Kids?.push(field);
}
normalizedEntries() {
let Kids = this.Kids();
if (!Kids) {
Kids = this.dict.context.obj([]);
this.dict.set(PDFName.of('Kids'), Kids);
}
return { Kids };
}
}
export default PDFAcroNonTerminal;

View file

@ -0,0 +1,22 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import { AcroButtonFlags } from 'src/core/acroform/flags';
class PDFAcroPushButton extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroPushButton(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Ff: AcroButtonFlags.PushButton,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroPushButton(dict, ref);
};
}
export default PDFAcroPushButton;

View file

@ -0,0 +1,58 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFContext from 'src/core/PDFContext';
import { AcroButtonFlags } from 'src/core/acroform/flags';
import { InvalidAcroFieldValueError } from 'src/core/errors';
class PDFAcroRadioButton extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroRadioButton(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Ff: AcroButtonFlags.Radio,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroRadioButton(dict, ref);
};
setValue(value: PDFName) {
const onValues = this.getOnValues();
if (!onValues.includes(value) && value !== PDFName.of('Off')) {
throw new InvalidAcroFieldValueError();
}
this.dict.set(PDFName.of('V'), value);
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getOnValue() === value ? value : PDFName.of('Off');
widget.setAppearanceState(state);
}
}
getValue(): PDFName {
const v = this.V();
if (v instanceof PDFName) return v;
return PDFName.of('Off');
}
getOnValues(): PDFName[] {
const widgets = this.getWidgets();
const onValues: PDFName[] = [];
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const onValue = widgets[idx].getOnValue();
if (onValue) onValues.push(onValue);
}
return onValues;
}
}
export default PDFAcroRadioButton;

View file

@ -0,0 +1,10 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
class PDFAcroSignature extends PDFAcroTerminal {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroSignature(dict, ref);
}
export default PDFAcroSignature;

View file

@ -0,0 +1,71 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFWidgetAnnotation from 'src/core/annotation/PDFWidgetAnnotation';
import { IndexOutOfBoundsError } from 'src/core/errors';
class PDFAcroTerminal extends PDFAcroField {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroTerminal(dict, ref);
FT(): PDFName {
const nameOrRef = this.getInheritableAttribute(PDFName.of('FT'));
return this.dict.context.lookup(nameOrRef, PDFName);
}
getWidgets(): PDFWidgetAnnotation[] {
const kidDicts = this.Kids();
// This field is itself a widget
if (!kidDicts) return [PDFWidgetAnnotation.fromDict(this.dict)];
// This field's kids are its widgets
const widgets = new Array<PDFWidgetAnnotation>(kidDicts.size());
for (let idx = 0, len = kidDicts.size(); idx < len; idx++) {
const dict = kidDicts.lookup(idx, PDFDict);
widgets[idx] = PDFWidgetAnnotation.fromDict(dict);
}
return widgets;
}
addWidget(ref: PDFRef) {
const { Kids } = this.normalizedEntries();
Kids.push(ref);
}
removeWidget(idx: number) {
const kidDicts = this.Kids();
if (!kidDicts) {
// This field is itself a widget
if (idx !== 0) throw new IndexOutOfBoundsError(idx, 0, 0);
this.setKids([]);
} else {
// This field's kids are its widgets
if (idx < 0 || idx > kidDicts.size()) {
throw new IndexOutOfBoundsError(idx, 0, kidDicts.size());
}
kidDicts.remove(idx);
}
}
normalizedEntries() {
let Kids = this.Kids();
// If this field is itself a widget (because it was only rendered once in
// the document, so the field and widget properties were merged) then we
// add itself to the `Kids` array. The alternative would be to try
// splitting apart the widget properties and creating a separate object
// for them.
if (!Kids) {
Kids = this.dict.context.obj([this.ref]);
this.dict.set(PDFName.of('Kids'), Kids);
}
return { Kids };
}
}
export default PDFAcroTerminal;

View file

@ -0,0 +1,76 @@
import PDFContext from 'src/core/PDFContext';
import PDFDict from 'src/core/objects/PDFDict';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
class PDFAcroText extends PDFAcroTerminal {
static fromDict = (dict: PDFDict, ref: PDFRef) => new PDFAcroText(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Tx',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroText(dict, ref);
};
MaxLen(): PDFNumber | undefined {
const maxLen = this.dict.lookup(PDFName.of('MaxLen'));
if (maxLen instanceof PDFNumber) return maxLen;
return undefined;
}
Q(): PDFNumber | undefined {
const q = this.dict.lookup(PDFName.of('Q'));
if (q instanceof PDFNumber) return q;
return undefined;
}
setMaxLength(maxLength: number) {
this.dict.set(PDFName.of('MaxLen'), PDFNumber.of(maxLength));
}
removeMaxLength() {
this.dict.delete(PDFName.of('MaxLen'));
}
getMaxLength(): number | undefined {
return this.MaxLen()?.asNumber();
}
setQuadding(quadding: 0 | 1 | 2) {
this.dict.set(PDFName.of('Q'), PDFNumber.of(quadding));
}
getQuadding(): number | undefined {
return this.Q()?.asNumber();
}
setValue(value: PDFHexString | PDFString) {
this.dict.set(PDFName.of('V'), value);
// const widgets = this.getWidgets();
// for (let idx = 0, len = widgets.length; idx < len; idx++) {
// const widget = widgets[idx];
// const state = widget.getOnValue() === value ? value : PDFName.of('Off');
// widget.setAppearanceState(state);
// }
}
removeValue() {
this.dict.delete(PDFName.of('V'));
}
getValue(): PDFString | PDFHexString | undefined {
const v = this.V();
if (v instanceof PDFString || v instanceof PDFHexString) return v;
return undefined;
}
}
export default PDFAcroText;

View file

@ -0,0 +1,162 @@
const flag = (bitIndex: number) => 1 << bitIndex;
/** From PDF spec table 221 */
export enum AcroFieldFlags {
/**
* If set, the user may not change the value of the field. Any associated
* widget annotations will not interact with the user; that is, they will not
* respond to mouse clicks or change their appearance in response to mouse
* motions. This flag is useful for fields whose values are computed or
* imported from a database.
*/
ReadOnly = flag(1 - 1),
/**
* If set, the field shall have a value at the time it is exported by a
* submit-form action (see 12.7.5.2, "Submit-Form Action").
*/
Required = flag(2 - 1),
/**
* If set, the field shall not be exported by a submit-form action
* (see 12.7.5.2, "Submit-Form Action").
*/
NoExport = flag(3 - 1),
}
/** From PDF spec table 226 */
export enum AcroButtonFlags {
/**
* (Radio buttons only) If set, exactly one radio button shall be selected at
* all times; selecting the currently selected button has no effect. If clear,
* clicking the selected button deselects it, leaving no button selected.
*/
NoToggleToOff = flag(15 - 1),
/**
* If set, the field is a set of radio buttons; if clear, the field is a check
* box. This flag may be set only if the Pushbutton flag is clear.
*/
Radio = flag(16 - 1),
/**
* If set, the field is a pushbutton that does not retain a permanent value.
*/
PushButton = flag(17 - 1),
/**
* If set, a group of radio buttons within a radio button field that use the
* same value for the on state will turn on and off in unison; that is if one
* is checked, they are all checked. If clear, the buttons are mutually
* exclusive (the same behavior as HTML radio buttons).
*/
RadiosInUnison = flag(26 - 1),
}
/** From PDF spec table 228 */
export enum AcroTextFlags {
/**
* If set, the field may contain multiple lines of text; if clear, the field's
* text shall be restricted to a single line.
*/
Multiline = flag(13 - 1),
/**
* If set, the field is intended for entering a secure password that should
* not be echoed visibly to the screen. Characters typed from the keyboard
* shall instead be echoed in some unreadable form, such as asterisks or
* bullet characters.
* > NOTE To protect password confidentiality, readers should never store
* > the value of the text field in the PDF file if this flag is set.
*/
Password = flag(14 - 1),
/**
* If set, the text entered in the field represents the pathname of a file
* whose contents shall be submitted as the value of the field.
*/
FileSelect = flag(21 - 1),
/**
* If set, text entered in the field shall not be spell-checked.
*/
DoNotSpellCheck = flag(23 - 1),
/**
* If set, the field shall not scroll (horizontally for single-line fields,
* vertically for multiple-line fields) to accommodate more text than fits
* within its annotation rectangle. Once the field is full, no further text
* shall be accepted for interactive form filling; for non-interactive form
* filling, the filler should take care not to add more character than will
* visibly fit in the defined area.
*/
DoNotScroll = flag(24 - 1),
/**
* May be set only if the MaxLen entry is present in the text field dictionary
* (see Table 229) and if the Multiline, Password, and FileSelect flags are
* clear. If set, the field shall be automatically divided into as many
* equally spaced positions, or combs, as the value of MaxLen, and the text
* is laid out into those combs.
*/
Comb = flag(25 - 1),
/**
* If set, the value of this field shall be a rich text string
* (see 12.7.3.4, "Rich Text Strings"). If the field has a value, the RV
* entry of the field dictionary (Table 222) shall specify the rich text
* string.
*/
RichText = flag(26 - 1),
}
/** From PDF spec table 230 */
export enum AcroChoiceFlags {
/**
* If set, the field is a combo box; if clear, the field is a list box.
*/
Combo = flag(18 - 1),
/**
* If set, the combo box shall include an editable text box as well as a
* drop-down list; if clear, it shall include only a drop-down list. This
* flag shall be used only if the Combo flag is set.
*/
Edit = flag(19 - 1),
/**
* If set, the field's option items shall be sorted alphabetically. This flag
* is intended for use by writers, not by readers. Conforming readers shall
* display the options in the order in which they occur in the Opt array
* (see Table 231).
*/
Sort = flag(20 - 1),
/**
* If set, more than one of the field's option items may be selected
* simultaneously; if clear, at most one item shall be selected.
*/
MultiSelect = flag(22 - 1),
/**
* If set, text entered in the field shall not be spell-checked. This flag
* shall not be used unless the Combo and Edit flags are both set.
*/
DoNotSpellCheck = flag(23 - 1),
/**
* If set, the new value shall be committed as soon as a selection is made
* (commonly with the pointing device). In this case, supplying a value for
* a field involves three actions: selecting the field for fill-in,
* selecting a choice for the fill-in value, and leaving that field, which
* finalizes or "commits" the data choice and triggers any actions associated
* with the entry or changing of this data. If this flag is on, then
* processing does not wait for leaving the field action to occur, but
* immediately proceeds to the third step.
*
* This option enables applications to perform an action once a selection is
* made, without requiring the user to exit the field. If clear, the new
* value is not committed until the user exits the field.
*/
CommitOnSelChange = flag(27 - 1),
}

View file

@ -0,0 +1,15 @@
export { default as PDFAcroButton } from 'src/core/acroform/PDFAcroButton';
export { default as PDFAcroCheckBox } from 'src/core/acroform/PDFAcroCheckBox';
export { default as PDFAcroChoice } from 'src/core/acroform/PDFAcroChoice';
export { default as PDFAcroComboBox } from 'src/core/acroform/PDFAcroComboBox';
export { default as PDFAcroField } from 'src/core/acroform/PDFAcroField';
export { default as PDFAcroForm } from 'src/core/acroform/PDFAcroForm';
export { default as PDFAcroListBox } from 'src/core/acroform/PDFAcroListBox';
export { default as PDFAcroNonTerminal } from 'src/core/acroform/PDFAcroNonTerminal';
export { default as PDFAcroPushButton } from 'src/core/acroform/PDFAcroPushButton';
export { default as PDFAcroRadioButton } from 'src/core/acroform/PDFAcroRadioButton';
export { default as PDFAcroSignature } from 'src/core/acroform/PDFAcroSignature';
export { default as PDFAcroTerminal } from 'src/core/acroform/PDFAcroTerminal';
export { default as PDFAcroText } from 'src/core/acroform/PDFAcroText';
export * from 'src/core/acroform/flags';
export * from 'src/core/acroform/utils';

View file

@ -0,0 +1,135 @@
import PDFObject from 'src/core/objects/PDFObject';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import PDFAcroNonTerminal from 'src/core/acroform/PDFAcroNonTerminal';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFAcroSignature from 'src/core/acroform/PDFAcroSignature';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFAcroText from 'src/core/acroform/PDFAcroText';
import PDFAcroPushButton from 'src/core/acroform/PDFAcroPushButton';
import PDFAcroRadioButton from 'src/core/acroform/PDFAcroRadioButton';
import PDFAcroCheckBox from 'src/core/acroform/PDFAcroCheckBox';
import PDFAcroComboBox from 'src/core/acroform/PDFAcroComboBox';
import PDFAcroListBox from 'src/core/acroform/PDFAcroListBox';
import { AcroButtonFlags, AcroChoiceFlags } from 'src/core/acroform/flags';
export const createPDFAcroFields = (
kidDicts?: PDFArray,
): [PDFAcroField, PDFRef][] => {
if (!kidDicts) return [];
const kids: [PDFAcroField, PDFRef][] = [];
for (let idx = 0, len = kidDicts.size(); idx < len; idx++) {
const ref = kidDicts.get(idx);
const dict = kidDicts.lookup(idx);
// if (dict instanceof PDFDict) kids.push(PDFAcroField.fromDict(dict));
if (ref instanceof PDFRef && dict instanceof PDFDict) {
kids.push([createPDFAcroField(dict, ref), ref]);
}
}
return kids;
};
export const createPDFAcroField = (
dict: PDFDict,
ref: PDFRef,
): PDFAcroField => {
const isNonTerminal = isNonTerminalAcroField(dict);
if (isNonTerminal) return PDFAcroNonTerminal.fromDict(dict, ref);
return createPDFAcroTerminal(dict, ref);
};
// TODO: Maybe just check if the dict is *not* a widget? That might be better.
// According to the PDF spec:
//
// > A field's children in the hierarchy may also include widget annotations
// > that define its appearance on the page. A field that has children that
// > are fields is called a non-terminal field. A field that does not have
// > children that are fields is called a terminal field.
//
// The spec is not entirely clear about how to determine whether a given
// dictionary represents an acrofield or a widget annotation. So we will assume
// that a dictionary is an acrofield if it is a member of the `/Kids` array
// and it contains a `/T` entry (widgets do not have `/T` entries). This isn't
// a bullet proof solution, because the `/T` entry is technically defined as
// optional for acrofields by the PDF spec. But in practice all acrofields seem
// to have a `/T` entry defined.
const isNonTerminalAcroField = (dict: PDFDict): boolean => {
const kids = dict.lookup(PDFName.of('Kids'));
if (kids instanceof PDFArray) {
for (let idx = 0, len = kids.size(); idx < len; idx++) {
const kid = kids.lookup(idx);
const kidIsField = kid instanceof PDFDict && kid.has(PDFName.of('T'));
if (kidIsField) return true;
}
}
return false;
};
const createPDFAcroTerminal = (dict: PDFDict, ref: PDFRef): PDFAcroTerminal => {
const ftNameOrRef = getInheritableAttribute(dict, PDFName.of('FT'));
const type = dict.context.lookup(ftNameOrRef, PDFName);
if (type === PDFName.of('Btn')) return createPDFAcroButton(dict, ref);
if (type === PDFName.of('Ch')) return createPDFAcroChoice(dict, ref);
if (type === PDFName.of('Tx')) return PDFAcroText.fromDict(dict, ref);
if (type === PDFName.of('Sig')) return PDFAcroSignature.fromDict(dict, ref);
// We should never reach this line. But there are a lot of weird PDFs out
// there. So, just to be safe, we'll try to handle things gracefully instead
// of throwing an error.
return PDFAcroTerminal.fromDict(dict, ref);
};
const createPDFAcroButton = (dict: PDFDict, ref: PDFRef): PDFAcroButton => {
const ffNumberOrRef = getInheritableAttribute(dict, PDFName.of('Ff'));
const ffNumber = dict.context.lookupMaybe(ffNumberOrRef, PDFNumber);
const flags = ffNumber?.asNumber() ?? 0;
if (flagIsSet(flags, AcroButtonFlags.PushButton)) {
return PDFAcroPushButton.fromDict(dict, ref);
} else if (flagIsSet(flags, AcroButtonFlags.Radio)) {
return PDFAcroRadioButton.fromDict(dict, ref);
} else {
return PDFAcroCheckBox.fromDict(dict, ref);
}
};
const createPDFAcroChoice = (dict: PDFDict, ref: PDFRef): PDFAcroChoice => {
const ffNumberOrRef = getInheritableAttribute(dict, PDFName.of('Ff'));
const ffNumber = dict.context.lookupMaybe(ffNumberOrRef, PDFNumber);
const flags = ffNumber?.asNumber() ?? 0;
if (flagIsSet(flags, AcroChoiceFlags.Combo)) {
return PDFAcroComboBox.fromDict(dict, ref);
} else {
return PDFAcroListBox.fromDict(dict, ref);
}
};
const flagIsSet = (flags: number, flag: number): boolean =>
(flags & flag) !== 0;
const getInheritableAttribute = (startNode: PDFDict, name: PDFName) => {
let attribute: PDFObject | undefined;
ascend(startNode, (node) => {
if (!attribute) attribute = node.get(name);
});
return attribute;
};
const ascend = (startNode: PDFDict, visitor: (node: PDFDict) => any) => {
visitor(startNode);
const Parent = startNode.lookupMaybe(PDFName.of('Parent'), PDFDict);
if (Parent) ascend(Parent, visitor);
};

View file

@ -0,0 +1,133 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFArray from 'src/core/objects/PDFArray';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFString from 'src/core/objects/PDFString';
class AppearanceCharacteristics {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): AppearanceCharacteristics =>
new AppearanceCharacteristics(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
R(): PDFNumber | undefined {
const R = this.dict.lookup(PDFName.of('R'));
if (R instanceof PDFNumber) return R;
return undefined;
}
BC(): PDFArray | undefined {
const BC = this.dict.lookup(PDFName.of('BC'));
if (BC instanceof PDFArray) return BC;
return undefined;
}
BG(): PDFArray | undefined {
const BG = this.dict.lookup(PDFName.of('BG'));
if (BG instanceof PDFArray) return BG;
return undefined;
}
CA(): PDFHexString | PDFString | undefined {
const CA = this.dict.lookup(PDFName.of('CA'));
if (CA instanceof PDFHexString || CA instanceof PDFString) return CA;
return undefined;
}
RC(): PDFHexString | PDFString | undefined {
const RC = this.dict.lookup(PDFName.of('RC'));
if (RC instanceof PDFHexString || RC instanceof PDFString) return RC;
return undefined;
}
AC(): PDFHexString | PDFString | undefined {
const AC = this.dict.lookup(PDFName.of('AC'));
if (AC instanceof PDFHexString || AC instanceof PDFString) return AC;
return undefined;
}
getRotation(): number | undefined {
return this.R()?.asNumber();
}
getBorderColor(): number[] | undefined {
const BC = this.BC();
if (!BC) return undefined;
const components: number[] = [];
for (let idx = 0, len = BC?.size(); idx < len; idx++) {
const component = BC.get(idx);
if (component instanceof PDFNumber) components.push(component.asNumber());
}
return components;
}
getBackgroundColor(): number[] | undefined {
const BG = this.BG();
if (!BG) return undefined;
const components: number[] = [];
for (let idx = 0, len = BG?.size(); idx < len; idx++) {
const component = BG.get(idx);
if (component instanceof PDFNumber) components.push(component.asNumber());
}
return components;
}
getCaptions(): { normal?: string; rollover?: string; down?: string } {
const CA = this.CA();
const RC = this.RC();
const AC = this.AC();
return {
normal: CA?.decodeText(),
rollover: RC?.decodeText(),
down: AC?.decodeText(),
};
}
setRotation(rotation: number) {
const R = this.dict.context.obj(rotation);
this.dict.set(PDFName.of('R'), R);
}
setBorderColor(color: number[]) {
const BC = this.dict.context.obj(color);
this.dict.set(PDFName.of('BC'), BC);
}
setBackgroundColor(color: number[]) {
const BG = this.dict.context.obj(color);
this.dict.set(PDFName.of('BG'), BG);
}
setCaptions(captions: { normal: string; rollover?: string; down?: string }) {
const CA = PDFHexString.fromText(captions.normal);
this.dict.set(PDFName.of('CA'), CA);
if (captions.rollover) {
const RC = PDFHexString.fromText(captions.rollover);
this.dict.set(PDFName.of('RC'), RC);
} else {
this.dict.delete(PDFName.of('RC'));
}
if (captions.down) {
const AC = PDFHexString.fromText(captions.down);
this.dict.set(PDFName.of('AC'), AC);
} else {
this.dict.delete(PDFName.of('AC'));
}
}
}
export default AppearanceCharacteristics;

View file

@ -0,0 +1,31 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
// TODO: Also handle the `/S` and `/D` entries
class BorderStyle {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): BorderStyle => new BorderStyle(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
W(): PDFNumber | undefined {
const W = this.dict.lookup(PDFName.of('W'));
if (W instanceof PDFNumber) return W;
return undefined;
}
getWidth(): number | undefined {
return this.W()?.asNumber() ?? 1;
}
setWidth(width: number) {
const W = this.dict.context.obj(width);
this.dict.set(PDFName.of('W'), W);
}
}
export default BorderStyle;

View file

@ -0,0 +1,148 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFStream from 'src/core/objects/PDFStream';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import PDFNumber from 'src/core/objects/PDFNumber';
class PDFAnnotation {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): PDFAnnotation => new PDFAnnotation(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
// This is technically required by the PDF spec
Rect(): PDFArray | undefined {
return this.dict.lookup(PDFName.of('Rect'), PDFArray);
}
AP(): PDFDict | undefined {
return this.dict.lookupMaybe(PDFName.of('AP'), PDFDict);
}
F(): PDFNumber | undefined {
const numberOrRef = this.dict.lookup(PDFName.of('F'));
return this.dict.context.lookupMaybe(numberOrRef, PDFNumber);
}
getRectangle(): { x: number; y: number; width: number; height: number } {
const Rect = this.Rect();
return Rect?.asRectangle() ?? { x: 0, y: 0, width: 0, height: 0 };
}
setRectangle(rect: { x: number; y: number; width: number; height: number }) {
const { x, y, width, height } = rect;
const Rect = this.dict.context.obj([x, y, x + width, y + height]);
this.dict.set(PDFName.of('Rect'), Rect);
}
getAppearanceState(): PDFName | undefined {
const AS = this.dict.lookup(PDFName.of('AS'));
if (AS instanceof PDFName) return AS;
return undefined;
}
setAppearanceState(state: PDFName) {
this.dict.set(PDFName.of('AS'), state);
}
setAppearances(appearances: PDFDict) {
this.dict.set(PDFName.of('AP'), appearances);
}
ensureAP(): PDFDict {
let AP = this.AP();
if (!AP) {
AP = this.dict.context.obj({});
this.dict.set(PDFName.of('AP'), AP);
}
return AP;
}
getNormalAppearance(): PDFRef | PDFDict {
const AP = this.ensureAP();
const N = AP.get(PDFName.of('N'));
if (N instanceof PDFRef || N instanceof PDFDict) return N;
throw new Error(`Unexpected N type: ${N?.constructor.name}`);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setNormalAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('N'), appearance);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setRolloverAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('R'), appearance);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setDownAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('D'), appearance);
}
removeRolloverAppearance() {
const AP = this.AP();
AP?.delete(PDFName.of('R'));
}
removeDownAppearance() {
const AP = this.AP();
AP?.delete(PDFName.of('D'));
}
getAppearances():
| {
normal: PDFStream | PDFDict;
rollover?: PDFStream | PDFDict;
down?: PDFStream | PDFDict;
}
| undefined {
const AP = this.AP();
if (!AP) return undefined;
const N = AP.lookup(PDFName.of('N'), PDFDict, PDFStream);
const R = AP.lookupMaybe(PDFName.of('R'), PDFDict, PDFStream);
const D = AP.lookupMaybe(PDFName.of('D'), PDFDict, PDFStream);
return { normal: N, rollover: R, down: D };
}
getFlags(): number {
return this.F()?.asNumber() ?? 0;
}
setFlags(flags: number) {
this.dict.set(PDFName.of('F'), PDFNumber.of(flags));
}
hasFlag(flag: number): boolean {
const flags = this.getFlags();
return (flags & flag) !== 0;
}
setFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags | flag);
}
clearFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags & ~flag);
}
setFlagTo(flag: number, enable: boolean) {
if (enable) this.setFlag(flag);
else this.clearFlag(flag);
}
}
export default PDFAnnotation;

View file

@ -0,0 +1,112 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import BorderStyle from 'src/core/annotation/BorderStyle';
import PDFAnnotation from 'src/core/annotation/PDFAnnotation';
import AppearanceCharacteristics from 'src/core/annotation/AppearanceCharacteristics';
class PDFWidgetAnnotation extends PDFAnnotation {
static fromDict = (dict: PDFDict): PDFWidgetAnnotation =>
new PDFWidgetAnnotation(dict);
static create = (context: PDFContext, parent: PDFRef) => {
const dict = context.obj({
Type: 'Annot',
Subtype: 'Widget',
Rect: [0, 0, 0, 0],
Parent: parent,
});
return new PDFWidgetAnnotation(dict);
};
MK(): PDFDict | undefined {
const MK = this.dict.lookup(PDFName.of('MK'));
if (MK instanceof PDFDict) return MK;
return undefined;
}
BS(): PDFDict | undefined {
const BS = this.dict.lookup(PDFName.of('BS'));
if (BS instanceof PDFDict) return BS;
return undefined;
}
DA(): PDFString | PDFHexString | undefined {
const da = this.dict.lookup(PDFName.of('DA'));
if (da instanceof PDFString || da instanceof PDFHexString) return da;
return undefined;
}
P(): PDFRef | undefined {
const P = this.dict.get(PDFName.of('P'));
if (P instanceof PDFRef) return P;
return undefined;
}
setP(page: PDFRef) {
this.dict.set(PDFName.of('P'), page);
}
setDefaultAppearance(appearance: string) {
this.dict.set(PDFName.of('DA'), PDFString.of(appearance));
}
getDefaultAppearance(): string | undefined {
const DA = this.DA();
if (DA instanceof PDFHexString) {
return DA.decodeText();
}
return DA?.asString();
}
getAppearanceCharacteristics(): AppearanceCharacteristics | undefined {
const MK = this.MK();
if (MK) return AppearanceCharacteristics.fromDict(MK);
return undefined;
}
getOrCreateAppearanceCharacteristics(): AppearanceCharacteristics {
const MK = this.MK();
if (MK) return AppearanceCharacteristics.fromDict(MK);
const ac = AppearanceCharacteristics.fromDict(this.dict.context.obj({}));
this.dict.set(PDFName.of('MK'), ac.dict);
return ac;
}
getBorderStyle(): BorderStyle | undefined {
const BS = this.BS();
if (BS) return BorderStyle.fromDict(BS);
return undefined;
}
getOrCreateBorderStyle(): BorderStyle {
const BS = this.BS();
if (BS) return BorderStyle.fromDict(BS);
const bs = BorderStyle.fromDict(this.dict.context.obj({}));
this.dict.set(PDFName.of('BS'), bs.dict);
return bs;
}
getOnValue(): PDFName | undefined {
const normal = this.getAppearances()?.normal;
if (normal instanceof PDFDict) {
const keys = normal.keys();
for (let idx = 0, len = keys.length; idx < len; idx++) {
const key = keys[idx];
if (key !== PDFName.of('Off')) return key;
}
}
return undefined;
}
}
export default PDFWidgetAnnotation;

View file

@ -0,0 +1,90 @@
const flag = (bitIndex: number) => 1 << bitIndex;
/** From PDF spec table 165 */
export enum AnnotationFlags {
/**
* If set, do not display the annotation if it does not belong to one of the
* standard annotation types and no annotation handler is available. If clear,
* display such an unknown annotation using an appearance stream specified by
* its appearance dictionary, if any.
*/
Invisible = flag(1 - 1),
/**
* If set, do not display or print the annotation or allow it to interact with
* the user, regardless of its annotation type or whether an annotation
* handler is available.
*
* In cases where screen space is limited, the ability to hide and show
* annotations selectively can be used in combination with appearance streams
* to display auxiliary pop-up information similar in function to online help
* systems.
*/
Hidden = flag(2 - 1),
/**
* If set, print the annotation when the page is printed. If clear, never
* print the annotation, regardless of whether it is displayed on the screen.
*
* This can be useful for annotations representing interactive pushbuttons,
* which would serve no meaningful purpose on the printed page.
*/
Print = flag(3 - 1),
/**
* If set, do not scale the annotations appearance to match the magnification
* of the page. The location of the annotation on the page (defined by the
* upper-left corner of its annotation rectangle) shall remain fixed,
* regardless of the page magnification.
*/
NoZoom = flag(4 - 1),
/**
* If set, do not rotate the annotations appearance to match the rotation of
* the page. The upper-left corner of the annotation rectangle shall remain in
* a fixed location on the page, regardless of the page rotation.
*/
NoRotate = flag(5 - 1),
/**
* If set, do not display the annotation on the screen or allow it to interact
* with the user. The annotation may be printed (depending on the setting of
* the Print flag) but should be considered hidden for purposes of on-screen
* display and user interaction.
*/
NoView = flag(6 - 1),
/**
* If set, do not allow the annotation to interact with the user. The
* annotation may be displayed or printed (depending on the settings of the
* NoView and Print flags) but should not respond to mouse clicks or change
* its appearance in response to mouse motions.
*
* This flag shall be ignored for widget annotations; its function is
* subsumed by the ReadOnly flag of the associated form field.
*/
ReadOnly = flag(7 - 1),
/**
* If set, do not allow the annotation to be deleted or its properties
* (including position and size) to be modified by the user. However, this
* flag does not restrict changes to the annotations contents, such as the
* value of a form field.
*/
Locked = flag(8 - 1),
/**
* If set, invert the interpretation of the NoView flag for certain events.
*
* A typical use is to have an annotation that appears only when a mouse
* cursor is held over it.
*/
ToggleNoView = flag(9 - 1),
/**
* If set, do not allow the contents of the annotation to be modified by the
* user. This flag does not restrict deletion of the annotation or changes to
* other annotation properties, such as position and size.
*/
LockedContents = flag(10 - 1),
}

View file

@ -0,0 +1,4 @@
export { default as PDFAnnotation } from 'src/core/annotation/PDFAnnotation';
export { default as PDFWidgetAnnotation } from 'src/core/annotation/PDFWidgetAnnotation';
export { default as AppearanceCharacteristics } from 'src/core/annotation/AppearanceCharacteristics';
export * from 'src/core/annotation/flags';

View file

@ -0,0 +1,173 @@
import PDFRef from 'src/core/objects/PDFRef';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer, padStart } from 'src/utils';
export interface Entry {
ref: PDFRef;
offset: number;
deleted: boolean;
}
/**
* Entries should be added using the [[addEntry]] and [[addDeletedEntry]]
* methods **in order of ascending object number**.
*/
class PDFCrossRefSection {
static create = () =>
new PDFCrossRefSection({
ref: PDFRef.of(0, 65535),
offset: 0,
deleted: true,
});
static createEmpty = () => new PDFCrossRefSection();
private subsections: Entry[][];
private chunkIdx: number;
private chunkLength: number;
private constructor(firstEntry: Entry | void) {
this.subsections = firstEntry ? [[firstEntry]] : [];
this.chunkIdx = 0;
this.chunkLength = firstEntry ? 1 : 0;
}
addEntry(ref: PDFRef, offset: number): void {
this.append({ ref, offset, deleted: false });
}
addDeletedEntry(ref: PDFRef, nextFreeObjectNumber: number): void {
this.append({ ref, offset: nextFreeObjectNumber, deleted: true });
}
toString(): string {
let section = `xref\n`;
for (
let rangeIdx = 0, rangeLen = this.subsections.length;
rangeIdx < rangeLen;
rangeIdx++
) {
const range = this.subsections[rangeIdx];
section += `${range[0].ref.objectNumber} ${range.length}\n`;
for (
let entryIdx = 0, entryLen = range.length;
entryIdx < entryLen;
entryIdx++
) {
const entry = range[entryIdx];
section += padStart(String(entry.offset), 10, '0');
section += ' ';
section += padStart(String(entry.ref.generationNumber), 5, '0');
section += ' ';
section += entry.deleted ? 'f' : 'n';
section += ' \n';
}
}
return section;
}
sizeInBytes(): number {
let size = 5;
for (let idx = 0, len = this.subsections.length; idx < len; idx++) {
const subsection = this.subsections[idx];
const subsectionLength = subsection.length;
const [firstEntry] = subsection;
size += 2;
size += String(firstEntry.ref.objectNumber).length;
size += String(subsectionLength).length;
size += 20 * subsectionLength;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.x;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.Newline;
offset += this.copySubsectionsIntoBuffer(this.subsections, buffer, offset);
return offset - initialOffset;
}
private copySubsectionsIntoBuffer(
subsections: Entry[][],
buffer: Uint8Array,
offset: number,
): number {
const initialOffset = offset;
const length = subsections.length;
for (let idx = 0; idx < length; idx++) {
const subsection = this.subsections[idx];
const firstObjectNumber = String(subsection[0].ref.objectNumber);
offset += copyStringIntoBuffer(firstObjectNumber, buffer, offset);
buffer[offset++] = CharCodes.Space;
const rangeLength = String(subsection.length);
offset += copyStringIntoBuffer(rangeLength, buffer, offset);
buffer[offset++] = CharCodes.Newline;
offset += this.copyEntriesIntoBuffer(subsection, buffer, offset);
}
return offset - initialOffset;
}
private copyEntriesIntoBuffer(
entries: Entry[],
buffer: Uint8Array,
offset: number,
): number {
const length = entries.length;
for (let idx = 0; idx < length; idx++) {
const entry = entries[idx];
const entryOffset = padStart(String(entry.offset), 10, '0');
offset += copyStringIntoBuffer(entryOffset, buffer, offset);
buffer[offset++] = CharCodes.Space;
const entryGen = padStart(String(entry.ref.generationNumber), 5, '0');
offset += copyStringIntoBuffer(entryGen, buffer, offset);
buffer[offset++] = CharCodes.Space;
buffer[offset++] = entry.deleted ? CharCodes.f : CharCodes.n;
buffer[offset++] = CharCodes.Space;
buffer[offset++] = CharCodes.Newline;
}
return 20 * length;
}
private append(currEntry: Entry): void {
if (this.chunkLength === 0) {
this.subsections.push([currEntry]);
this.chunkIdx = 0;
this.chunkLength = 1;
return;
}
const chunk = this.subsections[this.chunkIdx];
const prevEntry = chunk[this.chunkLength - 1];
if (currEntry.ref.objectNumber - prevEntry.ref.objectNumber > 1) {
this.subsections.push([currEntry]);
this.chunkIdx += 1;
this.chunkLength = 1;
} else {
chunk.push(currEntry);
this.chunkLength += 1;
}
}
}
export default PDFCrossRefSection;

View file

@ -0,0 +1,49 @@
import CharCodes from 'src/core/syntax/CharCodes';
import { charFromCode, copyStringIntoBuffer } from 'src/utils';
class PDFHeader {
static forVersion = (major: number, minor: number) =>
new PDFHeader(major, minor);
private readonly major: string;
private readonly minor: string;
private constructor(major: number, minor: number) {
this.major = String(major);
this.minor = String(minor);
}
toString(): string {
const bc = charFromCode(129);
return `%PDF-${this.major}.${this.minor}\n%${bc}${bc}${bc}${bc}`;
}
sizeInBytes(): number {
return 12 + this.major.length + this.minor.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.P;
buffer[offset++] = CharCodes.D;
buffer[offset++] = CharCodes.F;
buffer[offset++] = CharCodes.Dash;
offset += copyStringIntoBuffer(this.major, buffer, offset);
buffer[offset++] = CharCodes.Period;
offset += copyStringIntoBuffer(this.minor, buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = 129;
buffer[offset++] = 129;
buffer[offset++] = 129;
buffer[offset++] = 129;
return offset - initialOffset;
}
}
export default PDFHeader;

View file

@ -0,0 +1,49 @@
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer } from 'src/utils';
class PDFTrailer {
static forLastCrossRefSectionOffset = (offset: number) =>
new PDFTrailer(offset);
private readonly lastXRefOffset: string;
private constructor(lastXRefOffset: number) {
this.lastXRefOffset = String(lastXRefOffset);
}
toString(): string {
return `startxref\n${this.lastXRefOffset}\n%%EOF`;
}
sizeInBytes(): number {
return 16 + this.lastXRefOffset.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.x;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.Newline;
offset += copyStringIntoBuffer(this.lastXRefOffset, buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.E;
buffer[offset++] = CharCodes.O;
buffer[offset++] = CharCodes.F;
return offset - initialOffset;
}
}
export default PDFTrailer;

View file

@ -0,0 +1,39 @@
import PDFDict from 'src/core/objects/PDFDict';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFTrailerDict {
static of = (dict: PDFDict) => new PDFTrailerDict(dict);
readonly dict: PDFDict;
private constructor(dict: PDFDict) {
this.dict = dict;
}
toString(): string {
return `trailer\n${this.dict.toString()}`;
}
sizeInBytes(): number {
return 8 + this.dict.sizeInBytes();
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.i;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.Newline;
offset += this.dict.copyBytesInto(buffer, offset);
return offset - initialOffset;
}
}
export default PDFTrailerDict;

View file

@ -0,0 +1,70 @@
import { Glyph } from 'src/types/fontkit';
import { toHexString, toHexStringOfMinLength } from 'src/utils';
import {
hasSurrogates,
highSurrogate,
isWithinBMP,
lowSurrogate,
} from 'src/utils/unicode';
/** [fontId, codePoint] */
type BfChar = [string, string];
/** `glyphs` should be an array of unique glyphs */
export const createCmap = (glyphs: Glyph[], glyphId: (g?: Glyph) => number) => {
const bfChars: BfChar[] = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const glyph = glyphs[idx];
const id = cmapHexFormat(cmapHexString(glyphId(glyph)));
const unicode = cmapHexFormat(...glyph.codePoints.map(cmapCodePointFormat));
bfChars[idx] = [id, unicode];
}
return fillCmapTemplate(bfChars);
};
/* =============================== Templates ================================ */
const fillCmapTemplate = (bfChars: BfChar[]) => `\
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (UCS)
/Supplement 0
>> def
/CMapName /Adobe-Identity-UCS def
/CMapType 2 def
1 begincodespacerange
<0000><ffff>
endcodespacerange
${bfChars.length} beginbfchar
${bfChars.map(([glyphId, codePoint]) => `${glyphId} ${codePoint}`).join('\n')}
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end\
`;
/* =============================== Utilities ================================ */
const cmapHexFormat = (...values: string[]) => `<${values.join('')}>`;
const cmapHexString = (value: number) => toHexStringOfMinLength(value, 4);
const cmapCodePointFormat = (codePoint: number) => {
if (isWithinBMP(codePoint)) return cmapHexString(codePoint);
if (hasSurrogates(codePoint)) {
const hs = highSurrogate(codePoint);
const ls = lowSurrogate(codePoint);
return `${cmapHexString(hs)}${cmapHexString(ls)}`;
}
const hex = toHexString(codePoint);
const msg = `0x${hex} is not a valid UTF-8 or UTF-16 codepoint.`;
throw new Error(msg);
};

View file

@ -0,0 +1,249 @@
import { Font, Fontkit, Glyph, TypeFeatures } from 'src/types/fontkit';
import { createCmap } from 'src/core/embedders/CMap';
import { deriveFontFlags } from 'src/core/embedders/FontFlags';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFRef from 'src/core/objects/PDFRef';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import {
byAscendingId,
Cache,
sortedUniq,
toHexStringOfMinLength,
} from 'src/utils';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/jpeg.coffee
*/
class CustomFontEmbedder {
static async for(
fontkit: Fontkit,
fontData: Uint8Array,
customName?: string,
fontFeatures?: TypeFeatures,
) {
const font = await fontkit.create(fontData);
return new CustomFontEmbedder(font, fontData, customName, fontFeatures);
}
readonly font: Font;
readonly scale: number;
readonly fontData: Uint8Array;
readonly fontName: string;
readonly customName: string | undefined;
readonly fontFeatures: TypeFeatures | undefined;
protected baseFontName: string;
protected glyphCache: Cache<Glyph[]>;
protected constructor(
font: Font,
fontData: Uint8Array,
customName?: string,
fontFeatures?: TypeFeatures,
) {
this.font = font;
this.scale = 1000 / this.font.unitsPerEm;
this.fontData = fontData;
this.fontName = this.font.postscriptName || 'Font';
this.customName = customName;
this.fontFeatures = fontFeatures;
this.baseFontName = '';
this.glyphCache = Cache.populatedBy(this.allGlyphsInFontSortedById);
}
/**
* Encode the JavaScript string into this font. (JavaScript encodes strings in
* Unicode, but embedded fonts use their own custom encodings)
*/
encodeText(text: string): PDFHexString {
const { glyphs } = this.font.layout(text, this.fontFeatures);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
hexCodes[idx] = toHexStringOfMinLength(glyphs[idx].id, 4);
}
return PDFHexString.of(hexCodes.join(''));
}
// The advanceWidth takes into account kerning automatically, so we don't
// have to do that manually like we do for the standard fonts.
widthOfTextAtSize(text: string, size: number): number {
const { glyphs } = this.font.layout(text, this.fontFeatures);
let totalWidth = 0;
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
totalWidth += glyphs[idx].advanceWidth * this.scale;
}
const scale = size / 1000;
return totalWidth * scale;
}
heightOfFontAtSize(
size: number,
options: { descender?: boolean } = {},
): number {
const { descender = true } = options;
const { ascent, descent, bbox } = this.font;
const yTop = (ascent || bbox.maxY) * this.scale;
const yBottom = (descent || bbox.minY) * this.scale;
let height = yTop - yBottom;
if (!descender) height -= Math.abs(descent) || 0;
return (height / 1000) * size;
}
sizeOfFontAtHeight(height: number): number {
const { ascent, descent, bbox } = this.font;
const yTop = (ascent || bbox.maxY) * this.scale;
const yBottom = (descent || bbox.minY) * this.scale;
return (1000 * height) / (yTop - yBottom);
}
embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
this.baseFontName =
this.customName || context.addRandomSuffix(this.fontName);
return this.embedFontDict(context, ref);
}
protected async embedFontDict(
context: PDFContext,
ref?: PDFRef,
): Promise<PDFRef> {
const cidFontDictRef = await this.embedCIDFontDict(context);
const unicodeCMapRef = this.embedUnicodeCmap(context);
const fontDict = context.obj({
Type: 'Font',
Subtype: 'Type0',
BaseFont: this.baseFontName,
Encoding: 'Identity-H',
DescendantFonts: [cidFontDictRef],
ToUnicode: unicodeCMapRef,
});
if (ref) {
context.assign(ref, fontDict);
return ref;
} else {
return context.register(fontDict);
}
}
protected isCFF(): boolean {
return this.font.cff;
}
protected async embedCIDFontDict(context: PDFContext): Promise<PDFRef> {
const fontDescriptorRef = await this.embedFontDescriptor(context);
const cidFontDict = context.obj({
Type: 'Font',
Subtype: this.isCFF() ? 'CIDFontType0' : 'CIDFontType2',
CIDToGIDMap: 'Identity',
BaseFont: this.baseFontName,
CIDSystemInfo: {
Registry: PDFString.of('Adobe'),
Ordering: PDFString.of('Identity'),
Supplement: 0,
},
FontDescriptor: fontDescriptorRef,
W: this.computeWidths(),
});
return context.register(cidFontDict);
}
protected async embedFontDescriptor(context: PDFContext): Promise<PDFRef> {
const fontStreamRef = await this.embedFontStream(context);
const { scale } = this;
const { italicAngle, ascent, descent, capHeight, xHeight } = this.font;
const { minX, minY, maxX, maxY } = this.font.bbox;
const fontDescriptor = context.obj({
Type: 'FontDescriptor',
FontName: this.baseFontName,
Flags: deriveFontFlags(this.font),
FontBBox: [minX * scale, minY * scale, maxX * scale, maxY * scale],
ItalicAngle: italicAngle,
Ascent: ascent * scale,
Descent: descent * scale,
CapHeight: (capHeight || ascent) * scale,
XHeight: (xHeight || 0) * scale,
// Not sure how to compute/find this, nor is anybody else really:
// https://stackoverflow.com/questions/35485179/stemv-value-of-the-truetype-font
StemV: 0,
[this.isCFF() ? 'FontFile3' : 'FontFile2']: fontStreamRef,
});
return context.register(fontDescriptor);
}
protected async serializeFont(): Promise<Uint8Array> {
return this.fontData;
}
protected async embedFontStream(context: PDFContext): Promise<PDFRef> {
const fontStream = context.flateStream(await this.serializeFont(), {
Subtype: this.isCFF() ? 'CIDFontType0C' : undefined,
});
return context.register(fontStream);
}
protected embedUnicodeCmap(context: PDFContext): PDFRef {
const cmap = createCmap(this.glyphCache.access(), this.glyphId.bind(this));
const cmapStream = context.flateStream(cmap);
return context.register(cmapStream);
}
protected glyphId(glyph?: Glyph): number {
return glyph ? glyph.id : -1;
}
protected computeWidths(): (number | number[])[] {
const glyphs = this.glyphCache.access();
const widths: (number | number[])[] = [];
let currSection: number[] = [];
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const currGlyph = glyphs[idx];
const prevGlyph = glyphs[idx - 1];
const currGlyphId = this.glyphId(currGlyph);
const prevGlyphId = this.glyphId(prevGlyph);
if (idx === 0) {
widths.push(currGlyphId);
} else if (currGlyphId - prevGlyphId !== 1) {
widths.push(currSection);
widths.push(currGlyphId);
currSection = [];
}
currSection.push(currGlyph.advanceWidth * this.scale);
}
widths.push(currSection);
return widths;
}
private allGlyphsInFontSortedById = (): Glyph[] => {
const glyphs: Glyph[] = new Array(this.font.characterSet.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const codePoint = this.font.characterSet[idx];
glyphs[idx] = this.font.glyphForCodePoint(codePoint);
}
return sortedUniq(glyphs.sort(byAscendingId), (g) => g.id);
};
}
export default CustomFontEmbedder;

View file

@ -0,0 +1,84 @@
import { Font, Fontkit, Glyph, Subset, TypeFeatures } from 'src/types/fontkit';
import CustomFontEmbedder from 'src/core/embedders/CustomFontEmbedder';
import PDFHexString from 'src/core/objects/PDFHexString';
import { Cache, mergeUint8Arrays, toHexStringOfMinLength } from 'src/utils';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/jpeg.coffee
*/
class CustomFontSubsetEmbedder extends CustomFontEmbedder {
static async for(
fontkit: Fontkit,
fontData: Uint8Array,
customFontName?: string,
fontFeatures?: TypeFeatures,
) {
const font = await fontkit.create(fontData);
return new CustomFontSubsetEmbedder(
font,
fontData,
customFontName,
fontFeatures,
);
}
private readonly subset: Subset;
private readonly glyphs: Glyph[];
private readonly glyphIdMap: Map<number, number>;
private constructor(
font: Font,
fontData: Uint8Array,
customFontName?: string,
fontFeatures?: TypeFeatures,
) {
super(font, fontData, customFontName, fontFeatures);
this.subset = this.font.createSubset();
this.glyphs = [];
this.glyphCache = Cache.populatedBy(() => this.glyphs);
this.glyphIdMap = new Map();
}
encodeText(text: string): PDFHexString {
const { glyphs } = this.font.layout(text, this.fontFeatures);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const glyph = glyphs[idx];
const subsetGlyphId = this.subset.includeGlyph(glyph);
this.glyphs[subsetGlyphId - 1] = glyph;
this.glyphIdMap.set(glyph.id, subsetGlyphId);
hexCodes[idx] = toHexStringOfMinLength(subsetGlyphId, 4);
}
this.glyphCache.invalidate();
return PDFHexString.of(hexCodes.join(''));
}
protected isCFF(): boolean {
return (this.subset as any).cff;
}
protected glyphId(glyph?: Glyph): number {
return glyph ? this.glyphIdMap.get(glyph.id)! : -1;
}
protected serializeFont(): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const parts: Uint8Array[] = [];
this.subset
.encodeStream()
.on('data', (bytes) => parts.push(bytes))
.on('end', () => resolve(mergeUint8Arrays(parts)))
.on('error' as any, (err) => reject(err));
});
}
}
export default CustomFontSubsetEmbedder;

View file

@ -0,0 +1,95 @@
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
/**
* From the PDF-A3 specification, section **3.1. Requirements - General**.
* See:
* * https://www.pdfa.org/wp-content/uploads/2018/10/PDF20_AN002-AF.pdf
*/
export enum AFRelationship {
Source = 'Source',
Data = 'Data',
Alternative = 'Alternative',
Supplement = 'Supplement',
EncryptedPayload = 'EncryptedPayload',
FormData = 'EncryptedPayload',
Schema = 'Schema',
Unspecified = 'Unspecified',
}
export interface EmbeddedFileOptions {
mimeType?: string;
description?: string;
creationDate?: Date;
modificationDate?: Date;
afRelationship?: AFRelationship;
}
class FileEmbedder {
static for(
bytes: Uint8Array,
fileName: string,
options: EmbeddedFileOptions = {},
) {
return new FileEmbedder(bytes, fileName, options);
}
private readonly fileData: Uint8Array;
readonly fileName: string;
readonly options: EmbeddedFileOptions;
private constructor(
fileData: Uint8Array,
fileName: string,
options: EmbeddedFileOptions = {},
) {
this.fileData = fileData;
this.fileName = fileName;
this.options = options;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const {
mimeType,
description,
creationDate,
modificationDate,
afRelationship,
} = this.options;
const embeddedFileStream = context.flateStream(this.fileData, {
Type: 'EmbeddedFile',
Subtype: mimeType ?? undefined,
Params: {
Size: this.fileData.length,
CreationDate: creationDate
? PDFString.fromDate(creationDate)
: undefined,
ModDate: modificationDate
? PDFString.fromDate(modificationDate)
: undefined,
},
});
const embeddedFileStreamRef = context.register(embeddedFileStream);
const fileSpecDict = context.obj({
Type: 'Filespec',
F: PDFString.of(this.fileName), // TODO: Assert that this is plain ASCII
UF: PDFHexString.fromText(this.fileName),
EF: { F: embeddedFileStreamRef },
Desc: description ? PDFHexString.fromText(description) : undefined,
AFRelationship: afRelationship ?? undefined,
});
if (ref) {
context.assign(ref, fileSpecDict);
return ref;
} else {
return context.register(fileSpecDict);
}
}
}
export default FileEmbedder;

View file

@ -0,0 +1,45 @@
import { Font } from 'src/types/fontkit';
export interface FontFlagOptions {
fixedPitch?: boolean;
serif?: boolean;
symbolic?: boolean;
script?: boolean;
nonsymbolic?: boolean;
italic?: boolean;
allCap?: boolean;
smallCap?: boolean;
forceBold?: boolean;
}
// prettier-ignore
const makeFontFlags = (options: FontFlagOptions) => {
let flags = 0;
const flipBit = (bit: number) => { flags |= (1 << (bit - 1)); };
if (options.fixedPitch) flipBit(1);
if (options.serif) flipBit(2);
if (options.symbolic) flipBit(3);
if (options.script) flipBit(4);
if (options.nonsymbolic) flipBit(6);
if (options.italic) flipBit(7);
if (options.allCap) flipBit(17);
if (options.smallCap) flipBit(18);
if (options.forceBold) flipBit(19);
return flags;
};
// From: https://github.com/foliojs/pdfkit/blob/83f5f7243172a017adcf6a7faa5547c55982c57b/lib/font/embedded.js#L123-L129
export const deriveFontFlags = (font: Font): number => {
const familyClass = font['OS/2'] ? font['OS/2'].sFamilyClass : 0;
const flags = makeFontFlags({
fixedPitch: font.post.isFixedPitch,
serif: 1 <= familyClass && familyClass <= 7,
symbolic: true, // Assume the font uses non-latin characters
script: familyClass === 10,
italic: font.head.macStyle.italic,
});
return flags;
};

View file

@ -0,0 +1,34 @@
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
class JavaScriptEmbedder {
static for(script: string, scriptName: string) {
return new JavaScriptEmbedder(script, scriptName);
}
private readonly script: string;
readonly scriptName: string;
private constructor(script: string, scriptName: string) {
this.script = script;
this.scriptName = scriptName;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const jsActionDict = context.obj({
Type: 'Action',
S: 'JavaScript',
JS: PDFHexString.fromText(this.script),
});
if (ref) {
context.assign(ref, jsActionDict);
return ref;
} else {
return context.register(jsActionDict);
}
}
}
export default JavaScriptEmbedder;

View file

@ -0,0 +1,127 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
// prettier-ignore
const MARKERS = [
0xffc0, 0xffc1, 0xffc2,
0xffc3, 0xffc5, 0xffc6,
0xffc7, 0xffc8, 0xffc9,
0xffca, 0xffcb, 0xffcc,
0xffcd, 0xffce, 0xffcf,
];
enum ColorSpace {
DeviceGray = 'DeviceGray',
DeviceRGB = 'DeviceRGB',
DeviceCMYK = 'DeviceCMYK',
}
const ChannelToColorSpace: { [idx: number]: ColorSpace | undefined } = {
1: ColorSpace.DeviceGray,
3: ColorSpace.DeviceRGB,
4: ColorSpace.DeviceCMYK,
};
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/foliojs/pdfkit/blob/a6af76467ce06bd6a2af4aa7271ccac9ff152a7d/lib/image/jpeg.js
*/
class JpegEmbedder {
static async for(imageData: Uint8Array) {
const dataView = new DataView(imageData.buffer);
const soi = dataView.getUint16(0);
if (soi !== 0xffd8) throw new Error('SOI not found in JPEG');
let pos = 2;
let marker: number;
while (pos < dataView.byteLength) {
marker = dataView.getUint16(pos);
pos += 2;
if (MARKERS.includes(marker)) break;
pos += dataView.getUint16(pos);
}
if (!MARKERS.includes(marker!)) throw new Error('Invalid JPEG');
pos += 2;
const bitsPerComponent = dataView.getUint8(pos++);
const height = dataView.getUint16(pos);
pos += 2;
const width = dataView.getUint16(pos);
pos += 2;
const channelByte = dataView.getUint8(pos++);
const channelName = ChannelToColorSpace[channelByte];
if (!channelName) throw new Error('Unknown JPEG channel.');
const colorSpace = channelName;
return new JpegEmbedder(
imageData,
bitsPerComponent,
width,
height,
colorSpace,
);
}
readonly bitsPerComponent: number;
readonly height: number;
readonly width: number;
readonly colorSpace: ColorSpace;
private readonly imageData: Uint8Array;
private constructor(
imageData: Uint8Array,
bitsPerComponent: number,
width: number,
height: number,
colorSpace: ColorSpace,
) {
this.imageData = imageData;
this.bitsPerComponent = bitsPerComponent;
this.width = width;
this.height = height;
this.colorSpace = colorSpace;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const xObject = context.stream(this.imageData, {
Type: 'XObject',
Subtype: 'Image',
BitsPerComponent: this.bitsPerComponent,
Width: this.width,
Height: this.height,
ColorSpace: this.colorSpace,
Filter: 'DCTDecode',
// CMYK JPEG streams in PDF are typically stored complemented,
// with 1 as 'off' and 0 as 'on' (PDF 32000-1:2008, 8.6.4.4).
//
// Standalone CMYK JPEG (usually exported by Photoshop) are
// stored inverse, with 0 as 'off' and 1 as 'on', like RGB.
//
// Applying a swap here as a hedge that most bytes passing
// through this method will benefit from it.
Decode:
this.colorSpace === ColorSpace.DeviceCMYK
? [1, 0, 1, 0, 1, 0, 1, 0]
: undefined,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
}
export default JpegEmbedder;

View file

@ -0,0 +1,141 @@
import {
MissingPageContentsEmbeddingError,
UnrecognizedStreamTypeError,
} from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import { decodePDFRawStream } from 'src/core/streams/decode';
import PDFContentStream from 'src/core/structures/PDFContentStream';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
import CharCodes from 'src/core/syntax/CharCodes';
import { TransformationMatrix } from 'src/types/matrix';
import { mergeIntoTypedArray } from 'src/utils';
/**
* Represents a page bounding box.
* Usually `left` and `bottom` are 0 and right, top are equal
* to width, height if you want to clip to the whole page.
*
* y
* ^
* | +--------+ (width,height)
* | | |
* | | Page |
* | | |
* | | |
* (0,0) | +--------+
* +----------> x
*/
export interface PageBoundingBox {
left: number /** The left of the bounding box */;
bottom: number /** The bottom of the bounding box */;
right: number /** The right of the bounding box */;
top: number /** The top of the bounding box */;
}
const fullPageBoundingBox = (page: PDFPageLeaf) => {
const mediaBox = page.MediaBox();
const width =
mediaBox.lookup(2, PDFNumber).asNumber() -
mediaBox.lookup(0, PDFNumber).asNumber();
const height =
mediaBox.lookup(3, PDFNumber).asNumber() -
mediaBox.lookup(1, PDFNumber).asNumber();
return { left: 0, bottom: 0, right: width, top: height };
};
// Returns the identity matrix, modified to position the content of the given
// bounding box at (0, 0).
const boundingBoxAdjustedMatrix = (
bb: PageBoundingBox,
): TransformationMatrix => [1, 0, 0, 1, -bb.left, -bb.bottom];
class PDFPageEmbedder {
static async for(
page: PDFPageLeaf,
boundingBox?: PageBoundingBox,
transformationMatrix?: TransformationMatrix,
) {
return new PDFPageEmbedder(page, boundingBox, transformationMatrix);
}
readonly width: number;
readonly height: number;
readonly boundingBox: PageBoundingBox;
readonly transformationMatrix: TransformationMatrix;
private readonly page: PDFPageLeaf;
private constructor(
page: PDFPageLeaf,
boundingBox?: PageBoundingBox,
transformationMatrix?: TransformationMatrix,
) {
this.page = page;
const bb = boundingBox ?? fullPageBoundingBox(page);
this.width = bb.right - bb.left;
this.height = bb.top - bb.bottom;
this.boundingBox = bb;
this.transformationMatrix =
transformationMatrix ?? boundingBoxAdjustedMatrix(bb);
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const { Contents, Resources } = this.page.normalizedEntries();
if (!Contents) throw new MissingPageContentsEmbeddingError();
const decodedContents = this.decodeContents(Contents);
const { left, bottom, right, top } = this.boundingBox;
const xObject = context.flateStream(decodedContents, {
Type: 'XObject',
Subtype: 'Form',
FormType: 1,
BBox: [left, bottom, right, top],
Matrix: this.transformationMatrix,
Resources,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
// `contents` is an array of streams which are merged to include them in the XObject.
// This methods extracts each stream and joins them with a newline character.
private decodeContents(contents: PDFArray) {
const newline = Uint8Array.of(CharCodes.Newline);
const decodedContents: Uint8Array[] = [];
for (let idx = 0, len = contents.size(); idx < len; idx++) {
const stream = contents.lookup(idx, PDFStream);
let content: Uint8Array;
if (stream instanceof PDFRawStream) {
content = decodePDFRawStream(stream).decode();
} else if (stream instanceof PDFContentStream) {
content = stream.getUnencodedContents();
} else {
throw new UnrecognizedStreamTypeError(stream);
}
decodedContents.push(content, newline);
}
return mergeIntoTypedArray(...decodedContents);
}
}
export default PDFPageEmbedder;

View file

@ -0,0 +1,69 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import { PNG } from 'src/utils/png';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/png.coffee
*/
class PngEmbedder {
static async for(imageData: Uint8Array) {
const png = PNG.load(imageData);
return new PngEmbedder(png);
}
readonly bitsPerComponent: number;
readonly height: number;
readonly width: number;
readonly colorSpace: 'DeviceRGB';
private readonly image: PNG;
private constructor(png: PNG) {
this.image = png;
this.bitsPerComponent = png.bitsPerComponent;
this.width = png.width;
this.height = png.height;
this.colorSpace = 'DeviceRGB';
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const SMask = this.embedAlphaChannel(context);
const xObject = context.flateStream(this.image.rgbChannel, {
Type: 'XObject',
Subtype: 'Image',
BitsPerComponent: this.image.bitsPerComponent,
Width: this.image.width,
Height: this.image.height,
ColorSpace: this.colorSpace,
SMask,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
private embedAlphaChannel(context: PDFContext): PDFRef | undefined {
if (!this.image.alphaChannel) return undefined;
const xObject = context.flateStream(this.image.alphaChannel, {
Type: 'XObject',
Subtype: 'Image',
Height: this.image.height,
Width: this.image.width,
BitsPerComponent: this.image.bitsPerComponent,
ColorSpace: 'DeviceGray',
Decode: [0, 1],
});
return context.register(xObject);
}
}
export default PngEmbedder;

View file

@ -0,0 +1,130 @@
import {
Encodings,
Font,
FontNames,
EncodingType,
} from '@pdf-lib/standard-fonts';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import { toCodePoint, toHexString } from 'src/utils';
export interface Glyph {
code: number;
name: string;
}
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/foliojs/pdfkit/blob/f91bdd61c164a72ea06be1a43dc0a412afc3925f/lib/font/afm.coffee
*/
class StandardFontEmbedder {
static for = (fontName: FontNames, customName?: string) =>
new StandardFontEmbedder(fontName, customName);
readonly font: Font;
readonly encoding: EncodingType;
readonly fontName: string;
readonly customName: string | undefined;
private constructor(fontName: FontNames, customName?: string) {
// prettier-ignore
this.encoding = (
fontName === FontNames.ZapfDingbats ? Encodings.ZapfDingbats
: fontName === FontNames.Symbol ? Encodings.Symbol
: Encodings.WinAnsi
);
this.font = Font.load(fontName);
this.fontName = this.font.FontName;
this.customName = customName;
}
/**
* Encode the JavaScript string into this font. (JavaScript encodes strings in
* Unicode, but standard fonts use either WinAnsi, ZapfDingbats, or Symbol
* encodings)
*/
encodeText(text: string): PDFHexString {
const glyphs = this.encodeTextAsGlyphs(text);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
hexCodes[idx] = toHexString(glyphs[idx].code);
}
return PDFHexString.of(hexCodes.join(''));
}
widthOfTextAtSize(text: string, size: number): number {
const glyphs = this.encodeTextAsGlyphs(text);
let totalWidth = 0;
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const left = glyphs[idx].name;
const right = (glyphs[idx + 1] || {}).name;
const kernAmount = this.font.getXAxisKerningForPair(left, right) || 0;
totalWidth += this.widthOfGlyph(left) + kernAmount;
}
const scale = size / 1000;
return totalWidth * scale;
}
heightOfFontAtSize(
size: number,
options: { descender?: boolean } = {},
): number {
const { descender = true } = options;
const { Ascender, Descender, FontBBox } = this.font;
const yTop = Ascender || FontBBox[3];
const yBottom = Descender || FontBBox[1];
let height = yTop - yBottom;
if (!descender) height += Descender || 0;
return (height / 1000) * size;
}
sizeOfFontAtHeight(height: number): number {
const { Ascender, Descender, FontBBox } = this.font;
const yTop = Ascender || FontBBox[3];
const yBottom = Descender || FontBBox[1];
return (1000 * height) / (yTop - yBottom);
}
embedIntoContext(context: PDFContext, ref?: PDFRef): PDFRef {
const fontDict = context.obj({
Type: 'Font',
Subtype: 'Type1',
BaseFont: this.customName || this.fontName,
Encoding:
this.encoding === Encodings.WinAnsi ? 'WinAnsiEncoding' : undefined,
});
if (ref) {
context.assign(ref, fontDict);
return ref;
} else {
return context.register(fontDict);
}
}
private widthOfGlyph(glyphName: string): number {
// Default to 250 if font doesn't specify a width
return this.font.getWidthOfGlyph(glyphName) || 250;
}
private encodeTextAsGlyphs(text: string): Glyph[] {
const codePoints = Array.from(text);
const glyphs: Glyph[] = new Array(codePoints.length);
for (let idx = 0, len = codePoints.length; idx < len; idx++) {
const codePoint = toCodePoint(codePoints[idx])!;
glyphs[idx] = this.encoding.encodeUnicodeCodePoint(codePoint);
}
return glyphs;
}
}
export default StandardFontEmbedder;

221
frontend/node_modules/pdf-lib/src/core/errors.ts generated vendored Normal file
View file

@ -0,0 +1,221 @@
// tslint:disable: max-classes-per-file
import PDFObject from 'src/core/objects/PDFObject';
import { arrayAsString } from 'src/utils';
export class MethodNotImplementedError extends Error {
constructor(className: string, methodName: string) {
const msg = `Method ${className}.${methodName}() not implemented`;
super(msg);
}
}
export class PrivateConstructorError extends Error {
constructor(className: string) {
const msg = `Cannot construct ${className} - it has a private constructor`;
super(msg);
}
}
export class UnexpectedObjectTypeError extends Error {
constructor(expected: any | any[], actual: any) {
const name = (t: any) => t?.name ?? t?.constructor?.name;
const expectedTypes = Array.isArray(expected)
? expected.map(name)
: [name(expected)];
const msg =
`Expected instance of ${expectedTypes.join(' or ')}, ` +
`but got instance of ${actual ? name(actual) : actual}`;
super(msg);
}
}
export class UnsupportedEncodingError extends Error {
constructor(encoding: string) {
const msg = `${encoding} stream encoding not supported`;
super(msg);
}
}
export class ReparseError extends Error {
constructor(className: string, methodName: string) {
const msg = `Cannot call ${className}.${methodName}() more than once`;
super(msg);
}
}
export class MissingCatalogError extends Error {
constructor(ref?: PDFObject) {
const msg = `Missing catalog (ref=${ref})`;
super(msg);
}
}
export class MissingPageContentsEmbeddingError extends Error {
constructor() {
const msg = `Can't embed page with missing Contents`;
super(msg);
}
}
export class UnrecognizedStreamTypeError extends Error {
constructor(stream: any) {
const streamType = stream?.contructor?.name ?? stream?.name ?? stream;
const msg = `Unrecognized stream type: ${streamType}`;
super(msg);
}
}
export class PageEmbeddingMismatchedContextError extends Error {
constructor() {
const msg = `Found mismatched contexts while embedding pages. All pages in the array passed to \`PDFDocument.embedPages()\` must be from the same document.`;
super(msg);
}
}
export class PDFArrayIsNotRectangleError extends Error {
constructor(size: number) {
const msg = `Attempted to convert PDFArray with ${size} elements to rectangle, but must have exactly 4 elements.`;
super(msg);
}
}
export class InvalidPDFDateStringError extends Error {
constructor(value: string) {
const msg = `Attempted to convert "${value}" to a date, but it does not match the PDF date string format.`;
super(msg);
}
}
export class InvalidTargetIndexError extends Error {
constructor(targetIndex: number, Count: number) {
const msg = `Invalid targetIndex specified: targetIndex=${targetIndex} must be less than Count=${Count}`;
super(msg);
}
}
export class CorruptPageTreeError extends Error {
constructor(targetIndex: number, operation: string) {
const msg = `Failed to ${operation} at targetIndex=${targetIndex} due to corrupt page tree: It is likely that one or more 'Count' entries are invalid`;
super(msg);
}
}
export class IndexOutOfBoundsError extends Error {
constructor(index: number, min: number, max: number) {
const msg = `index should be at least ${min} and at most ${max}, but was actually ${index}`;
super(msg);
}
}
export class InvalidAcroFieldValueError extends Error {
constructor() {
const msg = `Attempted to set invalid field value`;
super(msg);
}
}
export class MultiSelectValueError extends Error {
constructor() {
const msg = `Attempted to select multiple values for single-select field`;
super(msg);
}
}
export class MissingDAEntryError extends Error {
constructor(fieldName: string) {
const msg = `No /DA (default appearance) entry found for field: ${fieldName}`;
super(msg);
}
}
export class MissingTfOperatorError extends Error {
constructor(fieldName: string) {
const msg = `No Tf operator found for DA of field: ${fieldName}`;
super(msg);
}
}
/***** Parser Errors ******/
export interface Position {
line: number;
column: number;
offset: number;
}
export class NumberParsingError extends Error {
constructor(pos: Position, value: string) {
const msg =
`Failed to parse number ` +
`(line:${pos.line} col:${pos.column} offset=${pos.offset}): "${value}"`;
super(msg);
}
}
export class PDFParsingError extends Error {
constructor(pos: Position, details: string) {
const msg =
`Failed to parse PDF document ` +
`(line:${pos.line} col:${pos.column} offset=${pos.offset}): ${details}`;
super(msg);
}
}
export class NextByteAssertionError extends PDFParsingError {
constructor(pos: Position, expectedByte: number, actualByte: number) {
const msg = `Expected next byte to be ${expectedByte} but it was actually ${actualByte}`;
super(pos, msg);
}
}
export class PDFObjectParsingError extends PDFParsingError {
constructor(pos: Position, byte: number) {
const msg = `Failed to parse PDF object starting with the following byte: ${byte}`;
super(pos, msg);
}
}
export class PDFInvalidObjectParsingError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse invalid PDF object`;
super(pos, msg);
}
}
export class PDFStreamParsingError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse PDF stream`;
super(pos, msg);
}
}
export class UnbalancedParenthesisError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse PDF literal string due to unbalanced parenthesis`;
super(pos, msg);
}
}
export class StalledParserError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Parser stalled`;
super(pos, msg);
}
}
export class MissingPDFHeaderError extends PDFParsingError {
constructor(pos: Position) {
const msg = `No PDF header found`;
super(pos, msg);
}
}
export class MissingKeywordError extends PDFParsingError {
constructor(pos: Position, keyword: number[]) {
const msg = `Did not find expected keyword '${arrayAsString(keyword)}'`;
super(pos, msg);
}
}

69
frontend/node_modules/pdf-lib/src/core/index.ts generated vendored Normal file
View file

@ -0,0 +1,69 @@
export * from 'src/core/errors';
export { default as CharCodes } from 'src/core/syntax/CharCodes';
export { default as PDFContext } from 'src/core/PDFContext';
export { default as PDFObjectCopier } from 'src/core/PDFObjectCopier';
export { default as PDFWriter } from 'src/core/writers/PDFWriter';
export { default as PDFStreamWriter } from 'src/core/writers/PDFStreamWriter';
export { default as PDFHeader } from 'src/core/document/PDFHeader';
export { default as PDFTrailer } from 'src/core/document/PDFTrailer';
export { default as PDFTrailerDict } from 'src/core/document/PDFTrailerDict';
export { default as PDFCrossRefSection } from 'src/core/document/PDFCrossRefSection';
export { default as StandardFontEmbedder } from 'src/core/embedders/StandardFontEmbedder';
export { default as CustomFontEmbedder } from 'src/core/embedders/CustomFontEmbedder';
export { default as CustomFontSubsetEmbedder } from 'src/core/embedders/CustomFontSubsetEmbedder';
export {
default as FileEmbedder,
AFRelationship,
} from 'src/core/embedders/FileEmbedder';
export { default as JpegEmbedder } from 'src/core/embedders/JpegEmbedder';
export { default as PngEmbedder } from 'src/core/embedders/PngEmbedder';
export {
default as PDFPageEmbedder,
PageBoundingBox,
} from 'src/core/embedders/PDFPageEmbedder';
export {
default as ViewerPreferences,
NonFullScreenPageMode,
ReadingDirection,
PrintScaling,
Duplex,
} from 'src/core/interactive/ViewerPreferences';
export { default as PDFObject } from 'src/core/objects/PDFObject';
export { default as PDFBool } from 'src/core/objects/PDFBool';
export { default as PDFNumber } from 'src/core/objects/PDFNumber';
export { default as PDFString } from 'src/core/objects/PDFString';
export { default as PDFHexString } from 'src/core/objects/PDFHexString';
export { default as PDFName } from 'src/core/objects/PDFName';
export { default as PDFNull } from 'src/core/objects/PDFNull';
export { default as PDFArray } from 'src/core/objects/PDFArray';
export { default as PDFDict } from 'src/core/objects/PDFDict';
export { default as PDFRef } from 'src/core/objects/PDFRef';
export { default as PDFInvalidObject } from 'src/core/objects/PDFInvalidObject';
export { default as PDFStream } from 'src/core/objects/PDFStream';
export { default as PDFRawStream } from 'src/core/objects/PDFRawStream';
export { default as PDFCatalog } from 'src/core/structures/PDFCatalog';
export { default as PDFContentStream } from 'src/core/structures/PDFContentStream';
export { default as PDFCrossRefStream } from 'src/core/structures/PDFCrossRefStream';
export { default as PDFObjectStream } from 'src/core/structures/PDFObjectStream';
export { default as PDFPageTree } from 'src/core/structures/PDFPageTree';
export { default as PDFPageLeaf } from 'src/core/structures/PDFPageLeaf';
export { default as PDFFlateStream } from 'src/core/structures/PDFFlateStream';
export { default as PDFOperator } from 'src/core/operators/PDFOperator';
export { default as PDFOperatorNames } from 'src/core/operators/PDFOperatorNames';
export { default as PDFObjectParser } from 'src/core/parser/PDFObjectParser';
export { default as PDFObjectStreamParser } from 'src/core/parser/PDFObjectStreamParser';
export { default as PDFParser } from 'src/core/parser/PDFParser';
export { default as PDFXRefStreamParser } from 'src/core/parser/PDFXRefStreamParser';
export { decodePDFRawStream } from 'src/core/streams/decode';
export * from 'src/core/annotation';
export * from 'src/core/acroform';

View file

@ -0,0 +1,579 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFContext from 'src/core/PDFContext';
import {
assertEachIs,
assertInteger,
assertIsOneOf,
assertRange,
} from 'src/utils';
const asEnum = <T extends string | number, U extends { [key: string]: T }>(
rawValue: T | undefined,
enumType: U,
): U[keyof U] | undefined => {
if (rawValue === undefined) return undefined;
return enumType[rawValue];
};
export enum NonFullScreenPageMode {
/**
* After exiting FullScreen mode, neither the document outline nor thumbnail
* images should be visible.
*/
UseNone = 'UseNone',
/** After exiting FullScreen mode, the document outline should be visible. */
UseOutlines = 'UseOutlines',
/** After exiting FullScreen mode, thumbnail images should be visible. */
UseThumbs = 'UseThumbs',
/**
* After exiting FullScreen mode, the optional content group panel should be
* visible.
*/
UseOC = 'UseOC',
}
export enum ReadingDirection {
/** The predominant reading order is Left to Right. */
L2R = 'L2R',
/**
* The predominant reading order is Right to left (including vertical writing
* systems, such as Chinese, Japanese and Korean).
*/
R2L = 'R2L',
}
export enum PrintScaling {
/** No page scaling. */
None = 'None',
/* Use the PDF reader's default print scaling. */
AppDefault = 'AppDefault',
}
export enum Duplex {
/** The PDF reader should print single-sided. */
Simplex = 'Simplex',
/**
* The PDF reader should print double sided and flip on the short edge of the
* sheet.
*/
DuplexFlipShortEdge = 'DuplexFlipShortEdge',
/**
* The PDF reader should print double sided and flip on the long edge of the
* sheet.
*/
DuplexFlipLongEdge = 'DuplexFlipLongEdge',
}
type BoolViewerPrefKey =
| 'HideToolbar'
| 'HideMenubar'
| 'HideWindowUI'
| 'FitWindow'
| 'CenterWindow'
| 'DisplayDocTitle'
| 'PickTrayByPDFSize';
type NameViewerPrefKey =
| 'NonFullScreenPageMode'
| 'Direction'
| 'PrintScaling'
| 'Duplex';
interface PageRange {
start: number;
end: number;
}
class ViewerPreferences {
/** @ignore */
readonly dict: PDFDict;
/** @ignore */
static fromDict = (dict: PDFDict): ViewerPreferences =>
new ViewerPreferences(dict);
/** @ignore */
static create = (context: PDFContext) => {
const dict = context.obj({});
return new ViewerPreferences(dict);
};
/** @ignore */
protected constructor(dict: PDFDict) {
this.dict = dict;
}
protected lookupBool(key: BoolViewerPrefKey): PDFBool | undefined {
const returnObj = this.dict.lookup(PDFName.of(key));
if (returnObj instanceof PDFBool) return returnObj;
return undefined;
}
protected lookupName(key: NameViewerPrefKey): PDFName | undefined {
const returnObj = this.dict.lookup(PDFName.of(key));
if (returnObj instanceof PDFName) return returnObj;
return undefined;
}
/** @ignore */
HideToolbar(): PDFBool | undefined {
return this.lookupBool('HideToolbar');
}
/** @ignore */
HideMenubar(): PDFBool | undefined {
return this.lookupBool('HideMenubar');
}
/** @ignore */
HideWindowUI(): PDFBool | undefined {
return this.lookupBool('HideWindowUI');
}
/** @ignore */
FitWindow(): PDFBool | undefined {
return this.lookupBool('FitWindow');
}
/** @ignore */
CenterWindow(): PDFBool | undefined {
return this.lookupBool('CenterWindow');
}
/** @ignore */
DisplayDocTitle(): PDFBool | undefined {
return this.lookupBool('DisplayDocTitle');
}
/** @ignore */
NonFullScreenPageMode(): PDFName | undefined {
return this.lookupName('NonFullScreenPageMode');
}
/** @ignore */
Direction(): PDFName | undefined {
return this.lookupName('Direction');
}
/** @ignore */
PrintScaling(): PDFName | undefined {
return this.lookupName('PrintScaling');
}
/** @ignore */
Duplex(): PDFName | undefined {
return this.lookupName('Duplex');
}
/** @ignore */
PickTrayByPDFSize(): PDFBool | undefined {
return this.lookupBool('PickTrayByPDFSize');
}
/** @ignore */
PrintPageRange(): PDFArray | undefined {
const PrintPageRange = this.dict.lookup(PDFName.of('PrintPageRange'));
if (PrintPageRange instanceof PDFArray) return PrintPageRange;
return undefined;
}
/** @ignore */
NumCopies(): PDFNumber | undefined {
const NumCopies = this.dict.lookup(PDFName.of('NumCopies'));
if (NumCopies instanceof PDFNumber) return NumCopies;
return undefined;
}
/**
* Returns `true` if PDF readers should hide the toolbar menus when displaying
* this document.
* @returns Whether or not toolbars should be hidden.
*/
getHideToolbar(): boolean {
return this.HideToolbar()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should hide the menu bar when displaying this
* document.
* @returns Whether or not the menu bar should be hidden.
*/
getHideMenubar(): boolean {
return this.HideMenubar()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should hide the user interface elements in
* the document's window (such as scroll bars and navigation controls),
* leaving only the document's contents displayed.
* @returns Whether or not user interface elements should be hidden.
*/
getHideWindowUI(): boolean {
return this.HideWindowUI()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should resize the document's window to fit
* the size of the first displayed page.
* @returns Whether or not the window should be resized to fit.
*/
getFitWindow(): boolean {
return this.FitWindow()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should position the document's window in the
* center of the screen.
* @returns Whether or not to center the document window.
*/
getCenterWindow(): boolean {
return this.CenterWindow()?.asBoolean() ?? false;
}
/**
* Returns `true` if the window's title bar should display the document
* `Title`, taken from the document metadata (see [[PDFDocument.getTitle]]).
* Returns `false` if the title bar should instead display the filename of the
* PDF file.
* @returns Whether to display the document title.
*/
getDisplayDocTitle(): boolean {
return this.DisplayDocTitle()?.asBoolean() ?? false;
}
/**
* Returns the page mode, which tells the PDF reader how to display the
* document after exiting full-screen mode.
* @returns The page mode after exiting full-screen mode.
*/
getNonFullScreenPageMode(): NonFullScreenPageMode {
const mode = this.NonFullScreenPageMode()?.decodeText();
return asEnum(mode, NonFullScreenPageMode) ?? NonFullScreenPageMode.UseNone;
}
/**
* Returns the predominant reading order for text.
* @returns The text reading order.
*/
getReadingDirection(): ReadingDirection {
const direction = this.Direction()?.decodeText();
return asEnum(direction, ReadingDirection) ?? ReadingDirection.L2R;
}
/**
* Returns the page scaling option that the PDF reader should select when the
* print dialog is displayed.
* @returns The page scaling option.
*/
getPrintScaling(): PrintScaling {
const scaling = this.PrintScaling()?.decodeText();
return asEnum(scaling, PrintScaling) ?? PrintScaling.AppDefault;
}
/**
* Returns the paper handling option that should be used when printing the
* file from the print dialog.
* @returns The paper handling option.
*/
getDuplex(): Duplex | undefined {
const duplex = this.Duplex()?.decodeText();
return asEnum(duplex, Duplex);
}
/**
* Returns `true` if the PDF page size should be used to select the input
* paper tray.
* @returns Whether or not the PDF page size should be used to select the
* input paper tray.
*/
getPickTrayByPDFSize(): boolean | undefined {
return this.PickTrayByPDFSize()?.asBoolean();
}
/**
* Returns an array of page number ranges, which are the values used to
* initialize the print dialog box when the file is printed. Each range
* specifies the first (`start`) and last (`end`) pages in a sub-range of
* pages to be printed. The first page of the PDF file is denoted by 0.
* For example:
* ```js
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* const includesPage3 = viewerPrefs
* .getPrintRanges()
* .some(pr => pr.start =< 2 && pr.end >= 2)
* if (includesPage3) console.log('printRange includes page 3')
* ```
* @returns An array of objects, each with the properties `start` and `end`,
* denoting page indices. If not, specified an empty array is
* returned.
*/
getPrintPageRange(): PageRange[] {
const rng = this.PrintPageRange();
if (!rng) return [];
const pageRanges: PageRange[] = [];
for (let i = 0; i < rng.size(); i += 2) {
// Despite the spec clearly stating that "The first page of the PDF file
// shall be donoted by 1", several test PDFs (spec 1.7) created in
// Acrobat XI 11.0 and also read with Reader DC 2020.013 indicate this is
// actually a 0 based index.
const start = rng.lookup(i, PDFNumber).asNumber();
const end = rng.lookup(i + 1, PDFNumber).asNumber();
pageRanges.push({ start, end });
}
return pageRanges;
}
/**
* Returns the number of copies to be printed when the print dialog is opened
* for this document.
* @returns The default number of copies to be printed.
*/
getNumCopies(): number {
return this.NumCopies()?.asNumber() ?? 1;
}
/**
* Choose whether the PDF reader's toolbars should be hidden while the
* document is active.
* @param hideToolbar `true` if the toolbar should be hidden.
*/
setHideToolbar(hideToolbar: boolean) {
const HideToolbar = this.dict.context.obj(hideToolbar);
this.dict.set(PDFName.of('HideToolbar'), HideToolbar);
}
/**
* Choose whether the PDF reader's menu bar should be hidden while the
* document is active.
* @param hideMenubar `true` if the menu bar should be hidden.
*/
setHideMenubar(hideMenubar: boolean) {
const HideMenubar = this.dict.context.obj(hideMenubar);
this.dict.set(PDFName.of('HideMenubar'), HideMenubar);
}
/**
* Choose whether the PDF reader should hide user interface elements in the
* document's window (such as scroll bars and navigation controls), leaving
* only the document's contents displayed.
* @param hideWindowUI `true` if the user interface elements should be hidden.
*/
setHideWindowUI(hideWindowUI: boolean) {
const HideWindowUI = this.dict.context.obj(hideWindowUI);
this.dict.set(PDFName.of('HideWindowUI'), HideWindowUI);
}
/**
* Choose whether the PDF reader should resize the document's window to fit
* the size of the first displayed page.
* @param fitWindow `true` if the window should be resized.
*/
setFitWindow(fitWindow: boolean) {
const FitWindow = this.dict.context.obj(fitWindow);
this.dict.set(PDFName.of('FitWindow'), FitWindow);
}
/**
* Choose whether the PDF reader should position the document's window in the
* center of the screen.
* @param centerWindow `true` if the window should be centered.
*/
setCenterWindow(centerWindow: boolean) {
const CenterWindow = this.dict.context.obj(centerWindow);
this.dict.set(PDFName.of('CenterWindow'), CenterWindow);
}
/**
* Choose whether the window's title bar should display the document `Title`
* taken from the document metadata (see [[PDFDocument.setTitle]]). If
* `false`, the title bar should instead display the PDF filename.
* @param displayTitle `true` if the document title should be displayed.
*/
setDisplayDocTitle(displayTitle: boolean) {
const DisplayDocTitle = this.dict.context.obj(displayTitle);
this.dict.set(PDFName.of('DisplayDocTitle'), DisplayDocTitle);
}
/**
* Choose how the PDF reader should display the document upon exiting
* full-screen mode. This entry is meaningful only if the value of the
* `PageMode` entry in the document's [[PDFCatalog]] is `FullScreen`.
*
* For example:
* ```js
* import { PDFDocument, NonFullScreenPageMode, PDFName } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
*
* // Set the PageMode
* pdfDoc.catalog.set(PDFName.of('PageMode'),PDFName.of('FullScreen'))
*
* // Set what happens when full-screen is closed
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setNonFullScreenPageMode(NonFullScreenPageMode.UseOutlines)
* ```
*
* @param nonFullScreenPageMode How the document should be displayed upon
* exiting full screen mode.
*/
setNonFullScreenPageMode(nonFullScreenPageMode: NonFullScreenPageMode) {
assertIsOneOf(
nonFullScreenPageMode,
'nonFullScreenPageMode',
NonFullScreenPageMode,
);
const mode = PDFName.of(nonFullScreenPageMode);
this.dict.set(PDFName.of('NonFullScreenPageMode'), mode);
}
/**
* Choose the predominant reading order for text.
*
* This entry has no direct effect on the document's contents or page
* numbering, but may be used to determine the relative positioning of pages
* when displayed side by side or printed n-up.
*
* For example:
* ```js
* import { PDFDocument, ReadingDirection } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setReadingDirection(ReadingDirection.R2L)
* ```
*
* @param readingDirection The reading order for text.
*/
setReadingDirection(readingDirection: ReadingDirection) {
assertIsOneOf(readingDirection, 'readingDirection', ReadingDirection);
const direction = PDFName.of(readingDirection);
this.dict.set(PDFName.of('Direction'), direction);
}
/**
* Choose the page scaling option that should be selected when a print dialog
* is displayed for this document.
*
* For example:
* ```js
* import { PDFDocument, PrintScaling } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setPrintScaling(PrintScaling.None)
* ```
*
* @param printScaling The print scaling option.
*/
setPrintScaling(printScaling: PrintScaling) {
assertIsOneOf(printScaling, 'printScaling', PrintScaling);
const scaling = PDFName.of(printScaling);
this.dict.set(PDFName.of('PrintScaling'), scaling);
}
/**
* Choose the paper handling option that should be selected by default in the
* print dialog.
*
* For example:
* ```js
* import { PDFDocument, Duplex } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setDuplex(Duplex.DuplexFlipShortEdge)
* ```
*
* @param duplex The double or single sided printing option.
*/
setDuplex(duplex: Duplex) {
assertIsOneOf(duplex, 'duplex', Duplex);
const dup = PDFName.of(duplex);
this.dict.set(PDFName.of('Duplex'), dup);
}
/**
* Choose whether the PDF document's page size should be used to select the
* input paper tray when printing. This setting influences only the preset
* values used to populate the print dialog presented by a PDF reader.
*
* If PickTrayByPDFSize is true, the check box in the print dialog associated
* with input paper tray should be checked. This setting has no effect on
* operating systems that do not provide the ability to pick the input tray
* by size.
*
* @param pickTrayByPDFSize `true` if the document's page size should be used
* to select the input paper tray.
*/
setPickTrayByPDFSize(pickTrayByPDFSize: boolean) {
const PickTrayByPDFSize = this.dict.context.obj(pickTrayByPDFSize);
this.dict.set(PDFName.of('PickTrayByPDFSize'), PickTrayByPDFSize);
}
/**
* Choose the page numbers used to initialize the print dialog box when the
* file is printed. The first page of the PDF file is denoted by 0.
*
* For example:
* ```js
* import { PDFDocument } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
*
* // We can set the default print range to only the first page
* viewerPrefs.setPrintPageRange({ start: 0, end: 0 })
*
* // Or we can supply noncontiguous ranges (e.g. pages 1, 3, and 5-7)
* viewerPrefs.setPrintPageRange([
* { start: 0, end: 0 },
* { start: 2, end: 2 },
* { start: 4, end: 6 },
* ])
* ```
*
* @param printPageRange An object or array of objects, each with the
* properties `start` and `end`, denoting a range of
* page indices.
*/
setPrintPageRange(printPageRange: PageRange[] | PageRange) {
if (!Array.isArray(printPageRange)) printPageRange = [printPageRange];
const flatRange: number[] = [];
for (let idx = 0, len = printPageRange.length; idx < len; idx++) {
flatRange.push(printPageRange[idx].start);
flatRange.push(printPageRange[idx].end);
}
assertEachIs(flatRange, 'printPageRange', ['number']);
const pageRanges = this.dict.context.obj(flatRange);
this.dict.set(PDFName.of('PrintPageRange'), pageRanges);
}
/**
* Choose the default number of copies to be printed when the print dialog is
* opened for this file.
* @param numCopies The default number of copies.
*/
setNumCopies(numCopies: number) {
assertRange(numCopies, 'numCopies', 1, Number.MAX_VALUE);
assertInteger(numCopies, 'numCopies');
const NumCopies = this.dict.context.obj(numCopies);
this.dict.set(PDFName.of('NumCopies'), NumCopies);
}
}
export default ViewerPreferences;

View file

@ -0,0 +1,185 @@
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { PDFArrayIsNotRectangleError } from 'src/core/errors';
import PDFRawStream from 'src/core/objects/PDFRawStream';
class PDFArray extends PDFObject {
static withContext = (context: PDFContext) => new PDFArray(context);
private readonly array: PDFObject[];
private readonly context: PDFContext;
private constructor(context: PDFContext) {
super();
this.array = [];
this.context = context;
}
size(): number {
return this.array.length;
}
push(object: PDFObject): void {
this.array.push(object);
}
insert(index: number, object: PDFObject): void {
this.array.splice(index, 0, object);
}
indexOf(object: PDFObject): number | undefined {
const index = this.array.indexOf(object);
return index === -1 ? undefined : index;
}
remove(index: number): void {
this.array.splice(index, 1);
}
set(idx: number, object: PDFObject): void {
this.array[idx] = object;
}
get(index: number): PDFObject {
return this.array[index];
}
lookupMaybe(index: number, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(index: number, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(index: number, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
index: number,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(index: number, type: typeof PDFName): PDFName | undefined;
lookupMaybe(index: number, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(index: number, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(index: number, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(
index: number,
type: typeof PDFRawStream,
): PDFRawStream | undefined;
lookupMaybe(index: number, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(index: number, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
index: number,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(index: number, ...types: any[]) {
return this.context.lookupMaybe(
this.get(index),
// @ts-ignore
...types,
) as any;
}
lookup(index: number): PDFObject | undefined;
lookup(index: number, type: typeof PDFArray): PDFArray;
lookup(index: number, type: typeof PDFBool): PDFBool;
lookup(index: number, type: typeof PDFDict): PDFDict;
lookup(index: number, type: typeof PDFHexString): PDFHexString;
lookup(index: number, type: typeof PDFName): PDFName;
lookup(index: number, type: typeof PDFNull): typeof PDFNull;
lookup(index: number, type: typeof PDFNumber): PDFNumber;
lookup(index: number, type: typeof PDFStream): PDFStream;
lookup(index: number, type: typeof PDFRawStream): PDFRawStream;
lookup(index: number, type: typeof PDFRef): PDFRef;
lookup(index: number, type: typeof PDFString): PDFString;
lookup(
index: number,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(index: number, ...types: any[]) {
return this.context.lookup(
this.get(index),
// @ts-ignore
...types,
) as any;
}
asRectangle(): { x: number; y: number; width: number; height: number } {
if (this.size() !== 4) throw new PDFArrayIsNotRectangleError(this.size());
const lowerLeftX = this.lookup(0, PDFNumber).asNumber();
const lowerLeftY = this.lookup(1, PDFNumber).asNumber();
const upperRightX = this.lookup(2, PDFNumber).asNumber();
const upperRightY = this.lookup(3, PDFNumber).asNumber();
const x = lowerLeftX;
const y = lowerLeftY;
const width = upperRightX - lowerLeftX;
const height = upperRightY - lowerLeftY;
return { x, y, width, height };
}
asArray(): PDFObject[] {
return this.array.slice();
}
clone(context?: PDFContext): PDFArray {
const clone = PDFArray.withContext(context || this.context);
for (let idx = 0, len = this.size(); idx < len; idx++) {
clone.push(this.array[idx]);
}
return clone;
}
toString(): string {
let arrayString = '[ ';
for (let idx = 0, len = this.size(); idx < len; idx++) {
arrayString += this.get(idx).toString();
arrayString += ' ';
}
arrayString += ']';
return arrayString;
}
sizeInBytes(): number {
let size = 3;
for (let idx = 0, len = this.size(); idx < len; idx++) {
size += this.get(idx).sizeInBytes() + 1;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.LeftSquareBracket;
buffer[offset++] = CharCodes.Space;
for (let idx = 0, len = this.size(); idx < len; idx++) {
offset += this.get(idx).copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Space;
}
buffer[offset++] = CharCodes.RightSquareBracket;
return offset - initialOffset;
}
scalePDFNumbers(x: number, y: number): void {
for (let idx = 0, len = this.size(); idx < len; idx++) {
const el = this.lookup(idx);
if (el instanceof PDFNumber) {
const factor = idx % 2 === 0 ? x : y;
this.set(idx, PDFNumber.of(el.asNumber() * factor));
}
}
}
}
export default PDFArray;

View file

@ -0,0 +1,53 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
const ENFORCER = {};
class PDFBool extends PDFObject {
static readonly True = new PDFBool(ENFORCER, true);
static readonly False = new PDFBool(ENFORCER, false);
private readonly value: boolean;
private constructor(enforcer: any, value: boolean) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFBool');
super();
this.value = value;
}
asBoolean(): boolean {
return this.value;
}
clone(): PDFBool {
return this;
}
toString(): string {
return String(this.value);
}
sizeInBytes(): number {
return this.value ? 4 : 5;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
if (this.value) {
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.u;
buffer[offset++] = CharCodes.e;
return 4;
} else {
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.e;
return 5;
}
}
}
export default PDFBool;

View file

@ -0,0 +1,226 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
export type DictMap = Map<PDFName, PDFObject>;
class PDFDict extends PDFObject {
static withContext = (context: PDFContext) => new PDFDict(new Map(), context);
static fromMapWithContext = (map: DictMap, context: PDFContext) =>
new PDFDict(map, context);
readonly context: PDFContext;
private readonly dict: DictMap;
protected constructor(map: DictMap, context: PDFContext) {
super();
this.dict = map;
this.context = context;
}
keys(): PDFName[] {
return Array.from(this.dict.keys());
}
values(): PDFObject[] {
return Array.from(this.dict.values());
}
entries(): [PDFName, PDFObject][] {
return Array.from(this.dict.entries());
}
set(key: PDFName, value: PDFObject): void {
this.dict.set(key, value);
}
get(
key: PDFName,
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
preservePDFNull = false,
): PDFObject | undefined {
const value = this.dict.get(key);
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
has(key: PDFName): boolean {
const value = this.dict.get(key);
return value !== undefined && value !== PDFNull;
}
lookupMaybe(key: PDFName, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(key: PDFName, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(key: PDFName, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
key: PDFName,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(key: PDFName, type: typeof PDFName): PDFName | undefined;
lookupMaybe(key: PDFName, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(key: PDFName, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(key: PDFName, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(key: PDFName, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(key: PDFName, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFDict,
type2: typeof PDFStream,
): PDFDict | PDFStream | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
type3: typeof PDFArray,
): PDFString | PDFHexString | PDFArray | undefined;
lookupMaybe(key: PDFName, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const value = this.context.lookupMaybe(
this.get(key, preservePDFNull),
// @ts-ignore
...types,
) as any;
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
lookup(key: PDFName): PDFObject | undefined;
lookup(key: PDFName, type: typeof PDFArray): PDFArray;
lookup(key: PDFName, type: typeof PDFBool): PDFBool;
lookup(key: PDFName, type: typeof PDFDict): PDFDict;
lookup(key: PDFName, type: typeof PDFHexString): PDFHexString;
lookup(key: PDFName, type: typeof PDFName): PDFName;
lookup(key: PDFName, type: typeof PDFNull): typeof PDFNull;
lookup(key: PDFName, type: typeof PDFNumber): PDFNumber;
lookup(key: PDFName, type: typeof PDFStream): PDFStream;
lookup(key: PDFName, type: typeof PDFRef): PDFRef;
lookup(key: PDFName, type: typeof PDFString): PDFString;
lookup(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(
ref: PDFName,
type1: typeof PDFDict,
type2: typeof PDFStream,
): PDFDict | PDFStream;
lookup(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
type3: typeof PDFArray,
): PDFString | PDFHexString | PDFArray;
lookup(key: PDFName, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const value = this.context.lookup(
this.get(key, preservePDFNull),
// @ts-ignore
...types,
) as any;
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
delete(key: PDFName): boolean {
return this.dict.delete(key);
}
asMap(): Map<PDFName, PDFObject> {
return new Map(this.dict);
}
/** Generate a random key that doesn't exist in current key set */
uniqueKey(tag = ''): PDFName {
const existingKeys = this.keys();
let key = PDFName.of(this.context.addRandomSuffix(tag, 10));
while (existingKeys.includes(key)) {
key = PDFName.of(this.context.addRandomSuffix(tag, 10));
}
return key;
}
clone(context?: PDFContext): PDFDict {
const clone = PDFDict.withContext(context || this.context);
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clone.set(key, value);
}
return clone;
}
toString(): string {
let dictString = '<<\n';
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
dictString += key.toString() + ' ' + value.toString() + '\n';
}
dictString += '>>';
return dictString;
}
sizeInBytes(): number {
let size = 5;
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
size += key.sizeInBytes() + value.sizeInBytes() + 2;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.LessThan;
buffer[offset++] = CharCodes.LessThan;
buffer[offset++] = CharCodes.Newline;
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
offset += key.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Space;
offset += value.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
}
buffer[offset++] = CharCodes.GreaterThan;
buffer[offset++] = CharCodes.GreaterThan;
return offset - initialOffset;
}
}
export default PDFDict;

View file

@ -0,0 +1,94 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import {
copyStringIntoBuffer,
toHexStringOfMinLength,
utf16Decode,
utf16Encode,
pdfDocEncodingDecode,
parseDate,
hasUtf16BOM,
} from 'src/utils';
import { InvalidPDFDateStringError } from 'src/core/errors';
class PDFHexString extends PDFObject {
static of = (value: string) => new PDFHexString(value);
static fromText = (value: string) => {
const encoded = utf16Encode(value);
let hex = '';
for (let idx = 0, len = encoded.length; idx < len; idx++) {
hex += toHexStringOfMinLength(encoded[idx], 4);
}
return new PDFHexString(hex);
};
private readonly value: string;
constructor(value: string) {
super();
this.value = value;
}
asBytes(): Uint8Array {
// Append a zero if the number of digits is odd. See PDF spec 7.3.4.3
const hex = this.value + (this.value.length % 2 === 1 ? '0' : '');
const hexLength = hex.length;
const bytes = new Uint8Array(hex.length / 2);
let hexOffset = 0;
let bytesOffset = 0;
// Interpret each pair of hex digits as a single byte
while (hexOffset < hexLength) {
const byte = parseInt(hex.substring(hexOffset, hexOffset + 2), 16);
bytes[bytesOffset] = byte;
hexOffset += 2;
bytesOffset += 1;
}
return bytes;
}
decodeText(): string {
const bytes = this.asBytes();
if (hasUtf16BOM(bytes)) return utf16Decode(bytes);
return pdfDocEncodingDecode(bytes);
}
decodeDate(): Date {
const text = this.decodeText();
const date = parseDate(text);
if (!date) throw new InvalidPDFDateStringError(text);
return date;
}
asString(): string {
return this.value;
}
clone(): PDFHexString {
return PDFHexString.of(this.value);
}
toString(): string {
return `<${this.value}>`;
}
sizeInBytes(): number {
return this.value.length + 2;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.LessThan;
offset += copyStringIntoBuffer(this.value, buffer, offset);
buffer[offset++] = CharCodes.GreaterThan;
return this.value.length + 2;
}
}
export default PDFHexString;

View file

@ -0,0 +1,34 @@
import PDFObject from 'src/core/objects/PDFObject';
class PDFInvalidObject extends PDFObject {
static of = (data: Uint8Array) => new PDFInvalidObject(data);
private readonly data: Uint8Array;
private constructor(data: Uint8Array) {
super();
this.data = data;
}
clone(): PDFInvalidObject {
return PDFInvalidObject.of(this.data.slice());
}
toString(): string {
return `PDFInvalidObject(${this.data.length} bytes)`;
}
sizeInBytes(): number {
return this.data.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const length = this.data.length;
for (let idx = 0; idx < length; idx++) {
buffer[offset++] = this.data[idx];
}
return length;
}
}
export default PDFInvalidObject;

View file

@ -0,0 +1,159 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsIrregular } from 'src/core/syntax/Irregular';
import {
charFromHexCode,
copyStringIntoBuffer,
toCharCode,
toHexString,
} from 'src/utils';
const decodeName = (name: string) =>
name.replace(/#([\dABCDEF]{2})/g, (_, hex) => charFromHexCode(hex));
const isRegularChar = (charCode: number) =>
charCode >= CharCodes.ExclamationPoint &&
charCode <= CharCodes.Tilde &&
!IsIrregular[charCode];
const ENFORCER = {};
const pool = new Map<string, PDFName>();
class PDFName extends PDFObject {
static of = (name: string): PDFName => {
const decodedValue = decodeName(name);
let instance = pool.get(decodedValue);
if (!instance) {
instance = new PDFName(ENFORCER, decodedValue);
pool.set(decodedValue, instance);
}
return instance;
};
/* tslint:disable member-ordering */
static readonly Length = PDFName.of('Length');
static readonly FlateDecode = PDFName.of('FlateDecode');
static readonly Resources = PDFName.of('Resources');
static readonly Font = PDFName.of('Font');
static readonly XObject = PDFName.of('XObject');
static readonly ExtGState = PDFName.of('ExtGState');
static readonly Contents = PDFName.of('Contents');
static readonly Type = PDFName.of('Type');
static readonly Parent = PDFName.of('Parent');
static readonly MediaBox = PDFName.of('MediaBox');
static readonly Page = PDFName.of('Page');
static readonly Annots = PDFName.of('Annots');
static readonly TrimBox = PDFName.of('TrimBox');
static readonly ArtBox = PDFName.of('ArtBox');
static readonly BleedBox = PDFName.of('BleedBox');
static readonly CropBox = PDFName.of('CropBox');
static readonly Rotate = PDFName.of('Rotate');
static readonly Title = PDFName.of('Title');
static readonly Author = PDFName.of('Author');
static readonly Subject = PDFName.of('Subject');
static readonly Creator = PDFName.of('Creator');
static readonly Keywords = PDFName.of('Keywords');
static readonly Producer = PDFName.of('Producer');
static readonly CreationDate = PDFName.of('CreationDate');
static readonly ModDate = PDFName.of('ModDate');
/* tslint:enable member-ordering */
private readonly encodedName: string;
private constructor(enforcer: any, name: string) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFName');
super();
let encodedName = '/';
for (let idx = 0, len = name.length; idx < len; idx++) {
const character = name[idx];
const code = toCharCode(character);
encodedName += isRegularChar(code) ? character : `#${toHexString(code)}`;
}
this.encodedName = encodedName;
}
asBytes(): Uint8Array {
const bytes: number[] = [];
let hex = '';
let escaped = false;
const pushByte = (byte?: number) => {
if (byte !== undefined) bytes.push(byte);
escaped = false;
};
for (let idx = 1, len = this.encodedName.length; idx < len; idx++) {
const char = this.encodedName[idx];
const byte = toCharCode(char);
const nextChar = this.encodedName[idx + 1];
if (!escaped) {
if (byte === CharCodes.Hash) escaped = true;
else pushByte(byte);
} else {
if (
(byte >= CharCodes.Zero && byte <= CharCodes.Nine) ||
(byte >= CharCodes.a && byte <= CharCodes.f) ||
(byte >= CharCodes.A && byte <= CharCodes.F)
) {
hex += char;
if (
hex.length === 2 ||
!(
(nextChar >= '0' && nextChar <= '9') ||
(nextChar >= 'a' && nextChar <= 'f') ||
(nextChar >= 'A' && nextChar <= 'F')
)
) {
pushByte(parseInt(hex, 16));
hex = '';
}
} else {
pushByte(byte);
}
}
}
return new Uint8Array(bytes);
}
// TODO: This should probably use `utf8Decode()`
// TODO: Polyfill Array.from?
decodeText(): string {
const bytes = this.asBytes();
return String.fromCharCode(...Array.from(bytes));
}
asString(): string {
return this.encodedName;
}
/** @deprecated in favor of [[PDFName.asString]] */
value(): string {
return this.encodedName;
}
clone(): PDFName {
return this;
}
toString(): string {
return this.encodedName;
}
sizeInBytes(): number {
return this.encodedName.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.encodedName, buffer, offset);
return this.encodedName.length;
}
}
export default PDFName;

View file

@ -0,0 +1,30 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFNull extends PDFObject {
asNull(): null {
return null;
}
clone(): PDFNull {
return this;
}
toString(): string {
return 'null';
}
sizeInBytes(): number {
return 4;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.n;
buffer[offset++] = CharCodes.u;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.l;
return 4;
}
}
export default new PDFNull();

View file

@ -0,0 +1,44 @@
import { copyStringIntoBuffer, numberToString } from 'src/utils/index';
import PDFObject from 'src/core/objects/PDFObject';
class PDFNumber extends PDFObject {
static of = (value: number) => new PDFNumber(value);
private readonly numberValue: number;
private readonly stringValue: string;
private constructor(value: number) {
super();
this.numberValue = value;
this.stringValue = numberToString(value);
}
asNumber(): number {
return this.numberValue;
}
/** @deprecated in favor of [[PDFNumber.asNumber]] */
value(): number {
return this.numberValue;
}
clone(): PDFNumber {
return PDFNumber.of(this.numberValue);
}
toString(): string {
return this.stringValue;
}
sizeInBytes(): number {
return this.stringValue.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.stringValue, buffer, offset);
return this.stringValue.length;
}
}
export default PDFNumber;

View file

@ -0,0 +1,22 @@
import { MethodNotImplementedError } from 'src/core/errors';
import PDFContext from 'src/core/PDFContext';
class PDFObject {
clone(_context?: PDFContext): PDFObject {
throw new MethodNotImplementedError(this.constructor.name, 'clone');
}
toString(): string {
throw new MethodNotImplementedError(this.constructor.name, 'toString');
}
sizeInBytes(): number {
throw new MethodNotImplementedError(this.constructor.name, 'sizeInBytes');
}
copyBytesInto(_buffer: Uint8Array, _offset: number): number {
throw new MethodNotImplementedError(this.constructor.name, 'copyBytesInto');
}
}
export default PDFObject;

View file

@ -0,0 +1,38 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import { arrayAsString } from 'src/utils';
class PDFRawStream extends PDFStream {
static of = (dict: PDFDict, contents: Uint8Array) =>
new PDFRawStream(dict, contents);
readonly contents: Uint8Array;
private constructor(dict: PDFDict, contents: Uint8Array) {
super(dict);
this.contents = contents;
}
asUint8Array(): Uint8Array {
return this.contents.slice();
}
clone(context?: PDFContext): PDFRawStream {
return PDFRawStream.of(this.dict.clone(context), this.contents.slice());
}
getContentsString(): string {
return arrayAsString(this.contents);
}
getContents(): Uint8Array {
return this.contents;
}
getContentsSize(): number {
return this.contents.length;
}
}
export default PDFRawStream;

View file

@ -0,0 +1,55 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import { copyStringIntoBuffer } from 'src/utils';
const ENFORCER = {};
const pool = new Map<string, PDFRef>();
class PDFRef extends PDFObject {
static of = (objectNumber: number, generationNumber = 0) => {
const tag = `${objectNumber} ${generationNumber} R`;
let instance = pool.get(tag);
if (!instance) {
instance = new PDFRef(ENFORCER, objectNumber, generationNumber);
pool.set(tag, instance);
}
return instance;
};
readonly objectNumber: number;
readonly generationNumber: number;
readonly tag: string;
private constructor(
enforcer: any,
objectNumber: number,
generationNumber: number,
) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFRef');
super();
this.objectNumber = objectNumber;
this.generationNumber = generationNumber;
this.tag = `${objectNumber} ${generationNumber} R`;
}
clone(): PDFRef {
return this;
}
toString(): string {
return this.tag;
}
sizeInBytes(): number {
return this.tag.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.tag, buffer, offset);
return this.tag.length;
}
}
export default PDFRef;

View file

@ -0,0 +1,93 @@
import { MethodNotImplementedError } from 'src/core/errors';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFStream extends PDFObject {
readonly dict: PDFDict;
constructor(dict: PDFDict) {
super();
this.dict = dict;
}
clone(_context?: PDFContext): PDFStream {
throw new MethodNotImplementedError(this.constructor.name, 'clone');
}
getContentsString(): string {
throw new MethodNotImplementedError(
this.constructor.name,
'getContentsString',
);
}
getContents(): Uint8Array {
throw new MethodNotImplementedError(this.constructor.name, 'getContents');
}
getContentsSize(): number {
throw new MethodNotImplementedError(
this.constructor.name,
'getContentsSize',
);
}
updateDict(): void {
const contentsSize = this.getContentsSize();
this.dict.set(PDFName.Length, PDFNumber.of(contentsSize));
}
sizeInBytes(): number {
this.updateDict();
return this.dict.sizeInBytes() + this.getContentsSize() + 18;
}
toString(): string {
this.updateDict();
let streamString = this.dict.toString();
streamString += '\nstream\n';
streamString += this.getContentsString();
streamString += '\nendstream';
return streamString;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
this.updateDict();
const initialOffset = offset;
offset += this.dict.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.m;
buffer[offset++] = CharCodes.Newline;
const contents = this.getContents();
for (let idx = 0, len = contents.length; idx < len; idx++) {
buffer[offset++] = contents[idx];
}
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.n;
buffer[offset++] = CharCodes.d;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.m;
return offset - initialOffset;
}
}
export default PDFStream;

View file

@ -0,0 +1,118 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import {
copyStringIntoBuffer,
padStart,
utf16Decode,
pdfDocEncodingDecode,
toCharCode,
parseDate,
hasUtf16BOM,
} from 'src/utils';
import { InvalidPDFDateStringError } from 'src/core/errors';
class PDFString extends PDFObject {
// The PDF spec allows newlines and parens to appear directly within a literal
// string. These character _may_ be escaped. But they do not _have_ to be. So
// for simplicity, we will not bother escaping them.
static of = (value: string) => new PDFString(value);
static fromDate = (date: Date) => {
const year = padStart(String(date.getUTCFullYear()), 4, '0');
const month = padStart(String(date.getUTCMonth() + 1), 2, '0');
const day = padStart(String(date.getUTCDate()), 2, '0');
const hours = padStart(String(date.getUTCHours()), 2, '0');
const mins = padStart(String(date.getUTCMinutes()), 2, '0');
const secs = padStart(String(date.getUTCSeconds()), 2, '0');
return new PDFString(`D:${year}${month}${day}${hours}${mins}${secs}Z`);
};
private readonly value: string;
private constructor(value: string) {
super();
this.value = value;
}
asBytes(): Uint8Array {
const bytes: number[] = [];
let octal = '';
let escaped = false;
const pushByte = (byte?: number) => {
if (byte !== undefined) bytes.push(byte);
escaped = false;
};
for (let idx = 0, len = this.value.length; idx < len; idx++) {
const char = this.value[idx];
const byte = toCharCode(char);
const nextChar = this.value[idx + 1];
if (!escaped) {
if (byte === CharCodes.BackSlash) escaped = true;
else pushByte(byte);
} else {
if (byte === CharCodes.Newline) pushByte();
else if (byte === CharCodes.CarriageReturn) pushByte();
else if (byte === CharCodes.n) pushByte(CharCodes.Newline);
else if (byte === CharCodes.r) pushByte(CharCodes.CarriageReturn);
else if (byte === CharCodes.t) pushByte(CharCodes.Tab);
else if (byte === CharCodes.b) pushByte(CharCodes.Backspace);
else if (byte === CharCodes.f) pushByte(CharCodes.FormFeed);
else if (byte === CharCodes.LeftParen) pushByte(CharCodes.LeftParen);
else if (byte === CharCodes.RightParen) pushByte(CharCodes.RightParen);
else if (byte === CharCodes.Backspace) pushByte(CharCodes.BackSlash);
else if (byte >= CharCodes.Zero && byte <= CharCodes.Seven) {
octal += char;
if (octal.length === 3 || !(nextChar >= '0' && nextChar <= '7')) {
pushByte(parseInt(octal, 8));
octal = '';
}
} else {
pushByte(byte);
}
}
}
return new Uint8Array(bytes);
}
decodeText(): string {
const bytes = this.asBytes();
if (hasUtf16BOM(bytes)) return utf16Decode(bytes);
return pdfDocEncodingDecode(bytes);
}
decodeDate(): Date {
const text = this.decodeText();
const date = parseDate(text);
if (!date) throw new InvalidPDFDateStringError(text);
return date;
}
asString(): string {
return this.value;
}
clone(): PDFString {
return PDFString.of(this.value);
}
toString(): string {
return `(${this.value})`;
}
sizeInBytes(): number {
return this.value.length + 2;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.LeftParen;
offset += copyStringIntoBuffer(this.value, buffer, offset);
buffer[offset++] = CharCodes.RightParen;
return this.value.length + 2;
}
}
export default PDFString;

View file

@ -0,0 +1,79 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFString from 'src/core/objects/PDFString';
import PDFOperatorNames from 'src/core/operators/PDFOperatorNames';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer } from 'src/utils';
export type PDFOperatorArg =
| string
| PDFName
| PDFArray
| PDFNumber
| PDFString
| PDFHexString;
class PDFOperator {
static of = (name: PDFOperatorNames, args?: PDFOperatorArg[]) =>
new PDFOperator(name, args);
private readonly name: PDFOperatorNames;
private readonly args: PDFOperatorArg[];
private constructor(name: PDFOperatorNames, args?: PDFOperatorArg[]) {
this.name = name;
this.args = args || [];
}
clone(context?: PDFContext): PDFOperator {
const args = new Array(this.args.length);
for (let idx = 0, len = args.length; idx < len; idx++) {
const arg = this.args[idx];
args[idx] = arg instanceof PDFObject ? arg.clone(context) : arg;
}
return PDFOperator.of(this.name, args);
}
toString(): string {
let value = '';
for (let idx = 0, len = this.args.length; idx < len; idx++) {
value += String(this.args[idx]) + ' ';
}
value += this.name;
return value;
}
sizeInBytes(): number {
let size = 0;
for (let idx = 0, len = this.args.length; idx < len; idx++) {
const arg = this.args[idx];
size += (arg instanceof PDFObject ? arg.sizeInBytes() : arg.length) + 1;
}
size += this.name.length;
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
for (let idx = 0, len = this.args.length; idx < len; idx++) {
const arg = this.args[idx];
if (arg instanceof PDFObject) {
offset += arg.copyBytesInto(buffer, offset);
} else {
offset += copyStringIntoBuffer(arg, buffer, offset);
}
buffer[offset++] = CharCodes.Space;
}
offset += copyStringIntoBuffer(this.name, buffer, offset);
return offset - initialOffset;
}
}
export default PDFOperator;

View file

@ -0,0 +1,92 @@
enum PDFOperatorNames {
// Non Stroking Color Operators
NonStrokingColor = 'sc',
NonStrokingColorN = 'scn',
NonStrokingColorRgb = 'rg',
NonStrokingColorGray = 'g',
NonStrokingColorCmyk = 'k',
NonStrokingColorspace = 'cs',
// Stroking Color Operators
StrokingColor = 'SC',
StrokingColorN = 'SCN',
StrokingColorRgb = 'RG',
StrokingColorGray = 'G',
StrokingColorCmyk = 'K',
StrokingColorspace = 'CS',
// Marked Content Operators
BeginMarkedContentSequence = 'BDC',
BeginMarkedContent = 'BMC',
EndMarkedContent = 'EMC',
MarkedContentPointWithProps = 'DP',
MarkedContentPoint = 'MP',
DrawObject = 'Do',
// Graphics State Operators
ConcatTransformationMatrix = 'cm',
PopGraphicsState = 'Q',
PushGraphicsState = 'q',
SetFlatness = 'i',
SetGraphicsStateParams = 'gs',
SetLineCapStyle = 'J',
SetLineDashPattern = 'd',
SetLineJoinStyle = 'j',
SetLineMiterLimit = 'M',
SetLineWidth = 'w',
SetTextMatrix = 'Tm',
SetRenderingIntent = 'ri',
// Graphics Operators
AppendRectangle = 're',
BeginInlineImage = 'BI',
BeginInlineImageData = 'ID',
EndInlineImage = 'EI',
ClipEvenOdd = 'W*',
ClipNonZero = 'W',
CloseAndStroke = 's',
CloseFillEvenOddAndStroke = 'b*',
CloseFillNonZeroAndStroke = 'b',
ClosePath = 'h',
AppendBezierCurve = 'c',
CurveToReplicateFinalPoint = 'y',
CurveToReplicateInitialPoint = 'v',
EndPath = 'n',
FillEvenOddAndStroke = 'B*',
FillEvenOdd = 'f*',
FillNonZeroAndStroke = 'B',
FillNonZero = 'f',
LegacyFillNonZero = 'F',
LineTo = 'l',
MoveTo = 'm',
ShadingFill = 'sh',
StrokePath = 'S',
// Text Operators
BeginText = 'BT',
EndText = 'ET',
MoveText = 'Td',
MoveTextSetLeading = 'TD',
NextLine = 'T*',
SetCharacterSpacing = 'Tc',
SetFontAndSize = 'Tf',
SetTextHorizontalScaling = 'Tz',
SetTextLineHeight = 'TL',
SetTextRenderingMode = 'Tr',
SetTextRise = 'Ts',
SetWordSpacing = 'Tw',
ShowText = 'Tj',
ShowTextAdjusted = 'TJ',
ShowTextLine = "'", // tslint:disable-line quotemark
ShowTextLineAndSpace = '"',
// Type3 Font Operators
Type3D0 = 'd0',
Type3D1 = 'd1',
// Compatibility Section Operators
BeginCompatibilitySection = 'BX',
EndCompatibilitySection = 'EX',
}
export default PDFOperatorNames;

View file

@ -0,0 +1,119 @@
import { NumberParsingError } from 'src/core/errors';
import ByteStream from 'src/core/parser/ByteStream';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
import { IsWhitespace } from 'src/core/syntax/Whitespace';
import { charFromCode } from 'src/utils';
const { Newline, CarriageReturn } = CharCodes;
// TODO: Throw error if eof is reached before finishing object parse...
class BaseParser {
protected readonly bytes: ByteStream;
protected readonly capNumbers: boolean;
constructor(bytes: ByteStream, capNumbers = false) {
this.bytes = bytes;
this.capNumbers = capNumbers;
}
protected parseRawInt(): number {
let value = '';
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsDigit[byte]) break;
value += charFromCode(this.bytes.next());
}
const numberValue = Number(value);
if (!value || !isFinite(numberValue)) {
throw new NumberParsingError(this.bytes.position(), value);
}
return numberValue;
}
// TODO: Maybe handle exponential format?
// TODO: Compare performance of string concatenation to charFromCode(...bytes)
protected parseRawNumber(): number {
let value = '';
// Parse integer-part, the leading (+ | - | . | 0-9)
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsNumeric[byte]) break;
value += charFromCode(this.bytes.next());
if (byte === CharCodes.Period) break;
}
// Parse decimal-part, the trailing (0-9)
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsDigit[byte]) break;
value += charFromCode(this.bytes.next());
}
const numberValue = Number(value);
if (!value || !isFinite(numberValue)) {
throw new NumberParsingError(this.bytes.position(), value);
}
if (numberValue > Number.MAX_SAFE_INTEGER) {
if (this.capNumbers) {
const msg = `Parsed number that is too large for some PDF readers: ${value}, using Number.MAX_SAFE_INTEGER instead.`;
console.warn(msg);
return Number.MAX_SAFE_INTEGER;
} else {
const msg = `Parsed number that is too large for some PDF readers: ${value}, not capping.`;
console.warn(msg);
}
}
return numberValue;
}
protected skipWhitespace(): void {
while (!this.bytes.done() && IsWhitespace[this.bytes.peek()]) {
this.bytes.next();
}
}
protected skipLine(): void {
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (byte === Newline || byte === CarriageReturn) return;
this.bytes.next();
}
}
protected skipComment(): boolean {
if (this.bytes.peek() !== CharCodes.Percent) return false;
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (byte === Newline || byte === CarriageReturn) return true;
this.bytes.next();
}
return true;
}
protected skipWhitespaceAndComments(): void {
this.skipWhitespace();
while (this.skipComment()) this.skipWhitespace();
}
protected matchKeyword(keyword: number[]): boolean {
const initialOffset = this.bytes.offset();
for (let idx = 0, len = keyword.length; idx < len; idx++) {
if (this.bytes.done() || this.bytes.next() !== keyword[idx]) {
this.bytes.moveTo(initialOffset);
return false;
}
}
return true;
}
}
export default BaseParser;

View file

@ -0,0 +1,76 @@
import { NextByteAssertionError } from 'src/core/errors';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import { decodePDFRawStream } from 'src/core/streams/decode';
import CharCodes from 'src/core/syntax/CharCodes';
// TODO: See how line/col tracking affects performance
class ByteStream {
static of = (bytes: Uint8Array) => new ByteStream(bytes);
static fromPDFRawStream = (rawStream: PDFRawStream) =>
ByteStream.of(decodePDFRawStream(rawStream).decode());
private readonly bytes: Uint8Array;
private readonly length: number;
private idx = 0;
private line = 0;
private column = 0;
constructor(bytes: Uint8Array) {
this.bytes = bytes;
this.length = this.bytes.length;
}
moveTo(offset: number): void {
this.idx = offset;
}
next(): number {
const byte = this.bytes[this.idx++];
if (byte === CharCodes.Newline) {
this.line += 1;
this.column = 0;
} else {
this.column += 1;
}
return byte;
}
assertNext(expected: number): number {
if (this.peek() !== expected) {
throw new NextByteAssertionError(this.position(), expected, this.peek());
}
return this.next();
}
peek(): number {
return this.bytes[this.idx];
}
peekAhead(steps: number) {
return this.bytes[this.idx + steps];
}
peekAt(offset: number) {
return this.bytes[offset];
}
done(): boolean {
return this.idx >= this.length;
}
offset(): number {
return this.idx;
}
slice(start: number, end: number): Uint8Array {
return this.bytes.slice(start, end);
}
position(): { line: number; column: number; offset: number } {
return { line: this.line, column: this.column, offset: this.idx };
}
}
export default ByteStream;

View file

@ -0,0 +1,274 @@
import {
PDFObjectParsingError,
PDFStreamParsingError,
Position,
UnbalancedParenthesisError,
} from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict, { DictMap } from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import BaseParser from 'src/core/parser/BaseParser';
import ByteStream from 'src/core/parser/ByteStream';
import PDFContext from 'src/core/PDFContext';
import PDFCatalog from 'src/core/structures/PDFCatalog';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
import PDFPageTree from 'src/core/structures/PDFPageTree';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsDelimiter } from 'src/core/syntax/Delimiters';
import { Keywords } from 'src/core/syntax/Keywords';
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
import { IsWhitespace } from 'src/core/syntax/Whitespace';
import { charFromCode } from 'src/utils';
// TODO: Throw error if eof is reached before finishing object parse...
class PDFObjectParser extends BaseParser {
static forBytes = (
bytes: Uint8Array,
context: PDFContext,
capNumbers?: boolean,
) => new PDFObjectParser(ByteStream.of(bytes), context, capNumbers);
static forByteStream = (
byteStream: ByteStream,
context: PDFContext,
capNumbers = false,
) => new PDFObjectParser(byteStream, context, capNumbers);
protected readonly context: PDFContext;
constructor(byteStream: ByteStream, context: PDFContext, capNumbers = false) {
super(byteStream, capNumbers);
this.context = context;
}
// TODO: Is it possible to reduce duplicate parsing for ref lookaheads?
parseObject(): PDFObject {
this.skipWhitespaceAndComments();
if (this.matchKeyword(Keywords.true)) return PDFBool.True;
if (this.matchKeyword(Keywords.false)) return PDFBool.False;
if (this.matchKeyword(Keywords.null)) return PDFNull;
const byte = this.bytes.peek();
if (
byte === CharCodes.LessThan &&
this.bytes.peekAhead(1) === CharCodes.LessThan
) {
return this.parseDictOrStream();
}
if (byte === CharCodes.LessThan) return this.parseHexString();
if (byte === CharCodes.LeftParen) return this.parseString();
if (byte === CharCodes.ForwardSlash) return this.parseName();
if (byte === CharCodes.LeftSquareBracket) return this.parseArray();
if (IsNumeric[byte]) return this.parseNumberOrRef();
throw new PDFObjectParsingError(this.bytes.position(), byte);
}
protected parseNumberOrRef(): PDFNumber | PDFRef {
const firstNum = this.parseRawNumber();
this.skipWhitespaceAndComments();
const lookaheadStart = this.bytes.offset();
if (IsDigit[this.bytes.peek()]) {
const secondNum = this.parseRawNumber();
this.skipWhitespaceAndComments();
if (this.bytes.peek() === CharCodes.R) {
this.bytes.assertNext(CharCodes.R);
return PDFRef.of(firstNum, secondNum);
}
}
this.bytes.moveTo(lookaheadStart);
return PDFNumber.of(firstNum);
}
// TODO: Maybe update PDFHexString.of() logic to remove whitespace and validate input?
protected parseHexString(): PDFHexString {
let value = '';
this.bytes.assertNext(CharCodes.LessThan);
while (!this.bytes.done() && this.bytes.peek() !== CharCodes.GreaterThan) {
value += charFromCode(this.bytes.next());
}
this.bytes.assertNext(CharCodes.GreaterThan);
return PDFHexString.of(value);
}
protected parseString(): PDFString {
let nestingLvl = 0;
let isEscaped = false;
let value = '';
while (!this.bytes.done()) {
const byte = this.bytes.next();
value += charFromCode(byte);
// Check for unescaped parenthesis
if (!isEscaped) {
if (byte === CharCodes.LeftParen) nestingLvl += 1;
if (byte === CharCodes.RightParen) nestingLvl -= 1;
}
// Track whether current character is being escaped or not
if (byte === CharCodes.BackSlash) {
isEscaped = !isEscaped;
} else if (isEscaped) {
isEscaped = false;
}
// Once (if) the unescaped parenthesis balance out, return their contents
if (nestingLvl === 0) {
// Remove the outer parens so they aren't part of the contents
return PDFString.of(value.substring(1, value.length - 1));
}
}
throw new UnbalancedParenthesisError(this.bytes.position());
}
// TODO: Compare performance of string concatenation to charFromCode(...bytes)
// TODO: Maybe preallocate small Uint8Array if can use charFromCode?
protected parseName(): PDFName {
this.bytes.assertNext(CharCodes.ForwardSlash);
let name = '';
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (IsWhitespace[byte] || IsDelimiter[byte]) break;
name += charFromCode(byte);
this.bytes.next();
}
return PDFName.of(name);
}
protected parseArray(): PDFArray {
this.bytes.assertNext(CharCodes.LeftSquareBracket);
this.skipWhitespaceAndComments();
const pdfArray = PDFArray.withContext(this.context);
while (this.bytes.peek() !== CharCodes.RightSquareBracket) {
const element = this.parseObject();
pdfArray.push(element);
this.skipWhitespaceAndComments();
}
this.bytes.assertNext(CharCodes.RightSquareBracket);
return pdfArray;
}
protected parseDict(): PDFDict {
this.bytes.assertNext(CharCodes.LessThan);
this.bytes.assertNext(CharCodes.LessThan);
this.skipWhitespaceAndComments();
const dict: DictMap = new Map();
while (
!this.bytes.done() &&
this.bytes.peek() !== CharCodes.GreaterThan &&
this.bytes.peekAhead(1) !== CharCodes.GreaterThan
) {
const key = this.parseName();
const value = this.parseObject();
dict.set(key, value);
this.skipWhitespaceAndComments();
}
this.skipWhitespaceAndComments();
this.bytes.assertNext(CharCodes.GreaterThan);
this.bytes.assertNext(CharCodes.GreaterThan);
const Type = dict.get(PDFName.of('Type'));
if (Type === PDFName.of('Catalog')) {
return PDFCatalog.fromMapWithContext(dict, this.context);
} else if (Type === PDFName.of('Pages')) {
return PDFPageTree.fromMapWithContext(dict, this.context);
} else if (Type === PDFName.of('Page')) {
return PDFPageLeaf.fromMapWithContext(dict, this.context);
} else {
return PDFDict.fromMapWithContext(dict, this.context);
}
}
protected parseDictOrStream(): PDFDict | PDFStream {
const startPos = this.bytes.position();
const dict = this.parseDict();
this.skipWhitespaceAndComments();
if (
!this.matchKeyword(Keywords.streamEOF1) &&
!this.matchKeyword(Keywords.streamEOF2) &&
!this.matchKeyword(Keywords.streamEOF3) &&
!this.matchKeyword(Keywords.streamEOF4) &&
!this.matchKeyword(Keywords.stream)
) {
return dict;
}
const start = this.bytes.offset();
let end: number;
const Length = dict.get(PDFName.of('Length'));
if (Length instanceof PDFNumber) {
end = start + Length.asNumber();
this.bytes.moveTo(end);
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.endstream)) {
this.bytes.moveTo(start);
end = this.findEndOfStreamFallback(startPos);
}
} else {
end = this.findEndOfStreamFallback(startPos);
}
const contents = this.bytes.slice(start, end);
return PDFRawStream.of(dict, contents);
}
protected findEndOfStreamFallback(startPos: Position) {
// Move to end of stream, while handling nested streams
let nestingLvl = 1;
let end = this.bytes.offset();
while (!this.bytes.done()) {
end = this.bytes.offset();
if (this.matchKeyword(Keywords.stream)) {
nestingLvl += 1;
} else if (
this.matchKeyword(Keywords.EOF1endstream) ||
this.matchKeyword(Keywords.EOF2endstream) ||
this.matchKeyword(Keywords.EOF3endstream) ||
this.matchKeyword(Keywords.endstream)
) {
nestingLvl -= 1;
} else {
this.bytes.next();
}
if (nestingLvl === 0) break;
}
if (nestingLvl !== 0) throw new PDFStreamParsingError(startPos);
return end;
}
}
export default PDFObjectParser;

View file

@ -0,0 +1,67 @@
import { ReparseError } from 'src/core/errors';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFObjectParser from 'src/core/parser/PDFObjectParser';
import { waitForTick } from 'src/utils';
class PDFObjectStreamParser extends PDFObjectParser {
static forStream = (
rawStream: PDFRawStream,
shouldWaitForTick?: () => boolean,
) => new PDFObjectStreamParser(rawStream, shouldWaitForTick);
private alreadyParsed: boolean;
private readonly shouldWaitForTick: () => boolean;
private readonly firstOffset: number;
private readonly objectCount: number;
constructor(rawStream: PDFRawStream, shouldWaitForTick?: () => boolean) {
super(ByteStream.fromPDFRawStream(rawStream), rawStream.dict.context);
const { dict } = rawStream;
this.alreadyParsed = false;
this.shouldWaitForTick = shouldWaitForTick || (() => false);
this.firstOffset = dict.lookup(PDFName.of('First'), PDFNumber).asNumber();
this.objectCount = dict.lookup(PDFName.of('N'), PDFNumber).asNumber();
}
async parseIntoContext(): Promise<void> {
if (this.alreadyParsed) {
throw new ReparseError('PDFObjectStreamParser', 'parseIntoContext');
}
this.alreadyParsed = true;
const offsetsAndObjectNumbers = this.parseOffsetsAndObjectNumbers();
for (let idx = 0, len = offsetsAndObjectNumbers.length; idx < len; idx++) {
const { objectNumber, offset } = offsetsAndObjectNumbers[idx];
this.bytes.moveTo(this.firstOffset + offset);
const object = this.parseObject();
const ref = PDFRef.of(objectNumber, 0);
this.context.assign(ref, object);
if (this.shouldWaitForTick()) await waitForTick();
}
}
private parseOffsetsAndObjectNumbers(): {
objectNumber: number;
offset: number;
}[] {
const offsetsAndObjectNumbers = [];
for (let idx = 0, len = this.objectCount; idx < len; idx++) {
this.skipWhitespaceAndComments();
const objectNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
const offset = this.parseRawInt();
offsetsAndObjectNumbers.push({ objectNumber, offset });
}
return offsetsAndObjectNumbers;
}
}
export default PDFObjectStreamParser;

View file

@ -0,0 +1,364 @@
import PDFCrossRefSection from 'src/core/document/PDFCrossRefSection';
import PDFHeader from 'src/core/document/PDFHeader';
import PDFTrailer from 'src/core/document/PDFTrailer';
import {
MissingKeywordError,
MissingPDFHeaderError,
PDFInvalidObjectParsingError,
ReparseError,
StalledParserError,
} from 'src/core/errors';
import PDFDict from 'src/core/objects/PDFDict';
import PDFInvalidObject from 'src/core/objects/PDFInvalidObject';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFObjectParser from 'src/core/parser/PDFObjectParser';
import PDFObjectStreamParser from 'src/core/parser/PDFObjectStreamParser';
import PDFXRefStreamParser from 'src/core/parser/PDFXRefStreamParser';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { Keywords } from 'src/core/syntax/Keywords';
import { IsDigit } from 'src/core/syntax/Numeric';
import { waitForTick } from 'src/utils';
class PDFParser extends PDFObjectParser {
static forBytesWithOptions = (
pdfBytes: Uint8Array,
objectsPerTick?: number,
throwOnInvalidObject?: boolean,
capNumbers?: boolean,
) =>
new PDFParser(pdfBytes, objectsPerTick, throwOnInvalidObject, capNumbers);
private readonly objectsPerTick: number;
private readonly throwOnInvalidObject: boolean;
private alreadyParsed = false;
private parsedObjects = 0;
constructor(
pdfBytes: Uint8Array,
objectsPerTick = Infinity,
throwOnInvalidObject = false,
capNumbers = false,
) {
super(ByteStream.of(pdfBytes), PDFContext.create(), capNumbers);
this.objectsPerTick = objectsPerTick;
this.throwOnInvalidObject = throwOnInvalidObject;
}
async parseDocument(): Promise<PDFContext> {
if (this.alreadyParsed) {
throw new ReparseError('PDFParser', 'parseDocument');
}
this.alreadyParsed = true;
this.context.header = this.parseHeader();
let prevOffset;
while (!this.bytes.done()) {
await this.parseDocumentSection();
const offset = this.bytes.offset();
if (offset === prevOffset) {
throw new StalledParserError(this.bytes.position());
}
prevOffset = offset;
}
this.maybeRecoverRoot();
if (this.context.lookup(PDFRef.of(0))) {
console.warn('Removing parsed object: 0 0 R');
this.context.delete(PDFRef.of(0));
}
return this.context;
}
private maybeRecoverRoot(): void {
const isValidCatalog = (obj?: PDFObject) =>
obj instanceof PDFDict &&
obj.lookup(PDFName.of('Type')) === PDFName.of('Catalog');
const catalog = this.context.lookup(this.context.trailerInfo.Root);
if (!isValidCatalog(catalog)) {
const indirectObjects = this.context.enumerateIndirectObjects();
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const [ref, object] = indirectObjects[idx];
if (isValidCatalog(object)) {
this.context.trailerInfo.Root = ref;
}
}
}
}
private parseHeader(): PDFHeader {
while (!this.bytes.done()) {
if (this.matchKeyword(Keywords.header)) {
const major = this.parseRawInt();
this.bytes.assertNext(CharCodes.Period);
const minor = this.parseRawInt();
const header = PDFHeader.forVersion(major, minor);
this.skipBinaryHeaderComment();
return header;
}
this.bytes.next();
}
throw new MissingPDFHeaderError(this.bytes.position());
}
private parseIndirectObjectHeader(): PDFRef {
this.skipWhitespaceAndComments();
const objectNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
const generationNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.obj)) {
throw new MissingKeywordError(this.bytes.position(), Keywords.obj);
}
return PDFRef.of(objectNumber, generationNumber);
}
private matchIndirectObjectHeader(): boolean {
const initialOffset = this.bytes.offset();
try {
this.parseIndirectObjectHeader();
return true;
} catch (e) {
this.bytes.moveTo(initialOffset);
return false;
}
}
private shouldWaitForTick = () => {
this.parsedObjects += 1;
return this.parsedObjects % this.objectsPerTick === 0;
};
private async parseIndirectObject(): Promise<PDFRef> {
const ref = this.parseIndirectObjectHeader();
this.skipWhitespaceAndComments();
const object = this.parseObject();
this.skipWhitespaceAndComments();
// if (!this.matchKeyword(Keywords.endobj)) {
// throw new MissingKeywordError(this.bytes.position(), Keywords.endobj);
// }
// TODO: Log a warning if this fails...
this.matchKeyword(Keywords.endobj);
if (
object instanceof PDFRawStream &&
object.dict.lookup(PDFName.of('Type')) === PDFName.of('ObjStm')
) {
await PDFObjectStreamParser.forStream(
object,
this.shouldWaitForTick,
).parseIntoContext();
} else if (
object instanceof PDFRawStream &&
object.dict.lookup(PDFName.of('Type')) === PDFName.of('XRef')
) {
PDFXRefStreamParser.forStream(object).parseIntoContext();
} else {
this.context.assign(ref, object);
}
return ref;
}
// TODO: Improve and clean this up
private tryToParseInvalidIndirectObject() {
const startPos = this.bytes.position();
const msg = `Trying to parse invalid object: ${JSON.stringify(startPos)})`;
if (this.throwOnInvalidObject) throw new Error(msg);
console.warn(msg);
const ref = this.parseIndirectObjectHeader();
console.warn(`Invalid object ref: ${ref}`);
this.skipWhitespaceAndComments();
const start = this.bytes.offset();
let failed = true;
while (!this.bytes.done()) {
if (this.matchKeyword(Keywords.endobj)) {
failed = false;
}
if (!failed) break;
this.bytes.next();
}
if (failed) throw new PDFInvalidObjectParsingError(startPos);
const end = this.bytes.offset() - Keywords.endobj.length;
const object = PDFInvalidObject.of(this.bytes.slice(start, end));
this.context.assign(ref, object);
return ref;
}
private async parseIndirectObjects(): Promise<void> {
this.skipWhitespaceAndComments();
while (!this.bytes.done() && IsDigit[this.bytes.peek()]) {
const initialOffset = this.bytes.offset();
try {
await this.parseIndirectObject();
} catch (e) {
// TODO: Add tracing/logging mechanism to track when this happens!
this.bytes.moveTo(initialOffset);
this.tryToParseInvalidIndirectObject();
}
this.skipWhitespaceAndComments();
// TODO: Can this be done only when needed, to avoid harming performance?
this.skipJibberish();
if (this.shouldWaitForTick()) await waitForTick();
}
}
private maybeParseCrossRefSection(): PDFCrossRefSection | void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.xref)) return;
this.skipWhitespaceAndComments();
let objectNumber = -1;
const xref = PDFCrossRefSection.createEmpty();
while (!this.bytes.done() && IsDigit[this.bytes.peek()]) {
const firstInt = this.parseRawInt();
this.skipWhitespaceAndComments();
const secondInt = this.parseRawInt();
this.skipWhitespaceAndComments();
const byte = this.bytes.peek();
if (byte === CharCodes.n || byte === CharCodes.f) {
const ref = PDFRef.of(objectNumber, secondInt);
if (this.bytes.next() === CharCodes.n) {
xref.addEntry(ref, firstInt);
} else {
// this.context.delete(ref);
xref.addDeletedEntry(ref, firstInt);
}
objectNumber += 1;
} else {
objectNumber = firstInt;
}
this.skipWhitespaceAndComments();
}
return xref;
}
private maybeParseTrailerDict(): void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.trailer)) return;
this.skipWhitespaceAndComments();
const dict = this.parseDict();
const { context } = this;
context.trailerInfo = {
Root: dict.get(PDFName.of('Root')) || context.trailerInfo.Root,
Encrypt: dict.get(PDFName.of('Encrypt')) || context.trailerInfo.Encrypt,
Info: dict.get(PDFName.of('Info')) || context.trailerInfo.Info,
ID: dict.get(PDFName.of('ID')) || context.trailerInfo.ID,
};
}
private maybeParseTrailer(): PDFTrailer | void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.startxref)) return;
this.skipWhitespaceAndComments();
const offset = this.parseRawInt();
this.skipWhitespace();
this.matchKeyword(Keywords.eof);
this.skipWhitespaceAndComments();
this.matchKeyword(Keywords.eof);
this.skipWhitespaceAndComments();
return PDFTrailer.forLastCrossRefSectionOffset(offset);
}
private async parseDocumentSection(): Promise<void> {
await this.parseIndirectObjects();
this.maybeParseCrossRefSection();
this.maybeParseTrailerDict();
this.maybeParseTrailer();
// TODO: Can this be done only when needed, to avoid harming performance?
this.skipJibberish();
}
/**
* This operation is not necessary for valid PDF files. But some invalid PDFs
* contain jibberish in between indirect objects. This method is designed to
* skip past that jibberish, should it exist, until it reaches the next
* indirect object header, an xref table section, or the file trailer.
*/
private skipJibberish(): void {
this.skipWhitespaceAndComments();
while (!this.bytes.done()) {
const initialOffset = this.bytes.offset();
const byte = this.bytes.peek();
const isAlphaNumeric = byte >= CharCodes.Space && byte <= CharCodes.Tilde;
if (isAlphaNumeric) {
if (
this.matchKeyword(Keywords.xref) ||
this.matchKeyword(Keywords.trailer) ||
this.matchKeyword(Keywords.startxref) ||
this.matchIndirectObjectHeader()
) {
this.bytes.moveTo(initialOffset);
break;
}
}
this.bytes.next();
}
}
/**
* Skips the binary comment following a PDF header. The specification
* defines this binary comment (section 7.5.2 File Header) as a sequence of 4
* or more bytes that are 128 or greater, and which are preceded by a "%".
*
* This would imply that to strip out this binary comment, we could check for
* a sequence of bytes starting with "%", and remove all subsequent bytes that
* are 128 or greater. This works for many documents that properly comply with
* the spec. But in the wild, there are PDFs that omit the leading "%", and
* include bytes that are less than 128 (e.g. 0 or 1). So in order to parse
* these headers correctly, we just throw out all bytes leading up to the
* first indirect object header.
*/
private skipBinaryHeaderComment(): void {
this.skipWhitespaceAndComments();
try {
const initialOffset = this.bytes.offset();
this.parseIndirectObjectHeader();
this.bytes.moveTo(initialOffset);
} catch (e) {
this.bytes.next();
this.skipWhitespaceAndComments();
}
}
}
export default PDFParser;

View file

@ -0,0 +1,130 @@
import { ReparseError } from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFContext from 'src/core/PDFContext';
export interface Entry {
ref: PDFRef;
offset: number;
deleted: boolean;
inObjectStream: boolean;
}
class PDFXRefStreamParser {
static forStream = (rawStream: PDFRawStream) =>
new PDFXRefStreamParser(rawStream);
private alreadyParsed: boolean;
private readonly dict: PDFDict;
private readonly context: PDFContext;
private readonly bytes: ByteStream;
private readonly subsections: {
firstObjectNumber: number;
length: number;
}[];
private readonly byteWidths: [number, number, number];
constructor(rawStream: PDFRawStream) {
this.alreadyParsed = false;
this.dict = rawStream.dict;
this.bytes = ByteStream.fromPDFRawStream(rawStream);
this.context = this.dict.context;
const Size = this.dict.lookup(PDFName.of('Size'), PDFNumber);
const Index = this.dict.lookup(PDFName.of('Index'));
if (Index instanceof PDFArray) {
this.subsections = [];
for (let idx = 0, len = Index.size(); idx < len; idx += 2) {
const firstObjectNumber = Index.lookup(idx + 0, PDFNumber).asNumber();
const length = Index.lookup(idx + 1, PDFNumber).asNumber();
this.subsections.push({ firstObjectNumber, length });
}
} else {
this.subsections = [{ firstObjectNumber: 0, length: Size.asNumber() }];
}
const W = this.dict.lookup(PDFName.of('W'), PDFArray);
this.byteWidths = [-1, -1, -1];
for (let idx = 0, len = W.size(); idx < len; idx++) {
this.byteWidths[idx] = W.lookup(idx, PDFNumber).asNumber();
}
}
parseIntoContext(): Entry[] {
if (this.alreadyParsed) {
throw new ReparseError('PDFXRefStreamParser', 'parseIntoContext');
}
this.alreadyParsed = true;
this.context.trailerInfo = {
Root: this.dict.get(PDFName.of('Root')),
Encrypt: this.dict.get(PDFName.of('Encrypt')),
Info: this.dict.get(PDFName.of('Info')),
ID: this.dict.get(PDFName.of('ID')),
};
const entries = this.parseEntries();
// for (let idx = 0, len = entries.length; idx < len; idx++) {
// const entry = entries[idx];
// if (entry.deleted) this.context.delete(entry.ref);
// }
return entries;
}
private parseEntries(): Entry[] {
const entries = [];
const [typeFieldWidth, offsetFieldWidth, genFieldWidth] = this.byteWidths;
for (
let subsectionIdx = 0, subsectionLen = this.subsections.length;
subsectionIdx < subsectionLen;
subsectionIdx++
) {
const { firstObjectNumber, length } = this.subsections[subsectionIdx];
for (let objIdx = 0; objIdx < length; objIdx++) {
let type = 0;
for (let idx = 0, len = typeFieldWidth; idx < len; idx++) {
type = (type << 8) | this.bytes.next();
}
let offset = 0;
for (let idx = 0, len = offsetFieldWidth; idx < len; idx++) {
offset = (offset << 8) | this.bytes.next();
}
let generationNumber = 0;
for (let idx = 0, len = genFieldWidth; idx < len; idx++) {
generationNumber = (generationNumber << 8) | this.bytes.next();
}
// When the `type` field is absent, it defaults to 1
if (typeFieldWidth === 0) type = 1;
const objectNumber = firstObjectNumber + objIdx;
const entry = {
ref: PDFRef.of(objectNumber, generationNumber),
offset,
deleted: type === 0,
inObjectStream: type === 2,
};
entries.push(entry);
}
}
return entries;
}
}
export default PDFXRefStreamParser;

View file

@ -0,0 +1,98 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The Ascii85Stream class contained in this file is a TypeScript port of the
* JavaScript Ascii85Stream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
const isSpace = (ch: number) =>
ch === 0x20 || ch === 0x09 || ch === 0x0d || ch === 0x0a;
class Ascii85Stream extends DecodeStream {
private stream: StreamType;
private input: Uint8Array;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
this.input = new Uint8Array(5);
// Most streams increase in size when decoded, but Ascii85 streams
// typically shrink by ~20%.
if (maybeLength) {
maybeLength = 0.8 * maybeLength;
}
}
protected readBlock() {
const TILDA_CHAR = 0x7e; // '~'
const Z_LOWER_CHAR = 0x7a; // 'z'
const EOF = -1;
const stream = this.stream;
let c = stream.getByte();
while (isSpace(c)) {
c = stream.getByte();
}
if (c === EOF || c === TILDA_CHAR) {
this.eof = true;
return;
}
const bufferLength = this.bufferLength;
let buffer;
let i;
// special code for z
if (c === Z_LOWER_CHAR) {
buffer = this.ensureBuffer(bufferLength + 4);
for (i = 0; i < 4; ++i) {
buffer[bufferLength + i] = 0;
}
this.bufferLength += 4;
} else {
const input = this.input;
input[0] = c;
for (i = 1; i < 5; ++i) {
c = stream.getByte();
while (isSpace(c)) {
c = stream.getByte();
}
input[i] = c;
if (c === EOF || c === TILDA_CHAR) {
break;
}
}
buffer = this.ensureBuffer(bufferLength + i - 1);
this.bufferLength += i - 1;
// partial ending;
if (i < 5) {
for (; i < 5; ++i) {
input[i] = 0x21 + 84;
}
this.eof = true;
}
let t = 0;
for (i = 0; i < 5; ++i) {
t = t * 85 + (input[i] - 0x21);
}
for (i = 3; i >= 0; --i) {
buffer[bufferLength + i] = t & 0xff;
t >>= 8;
}
}
}
}
export default Ascii85Stream;

View file

@ -0,0 +1,77 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The AsciiHexStream class contained in this file is a TypeScript port of the
* JavaScript AsciiHexStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
class AsciiHexStream extends DecodeStream {
private stream: StreamType;
private firstDigit: number;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
this.firstDigit = -1;
// Most streams increase in size when decoded, but AsciiHex streams shrink
// by 50%.
if (maybeLength) {
maybeLength = 0.5 * maybeLength;
}
}
protected readBlock() {
const UPSTREAM_BLOCK_SIZE = 8000;
const bytes = this.stream.getBytes(UPSTREAM_BLOCK_SIZE);
if (!bytes.length) {
this.eof = true;
return;
}
const maxDecodeLength = (bytes.length + 1) >> 1;
const buffer = this.ensureBuffer(this.bufferLength + maxDecodeLength);
let bufferLength = this.bufferLength;
let firstDigit = this.firstDigit;
for (let i = 0, ii = bytes.length; i < ii; i++) {
const ch = bytes[i];
let digit;
if (ch >= 0x30 && ch <= 0x39) {
// '0'-'9'
digit = ch & 0x0f;
} else if ((ch >= 0x41 && ch <= 0x46) || (ch >= 0x61 && ch <= 0x66)) {
// 'A'-'Z', 'a'-'z'
digit = (ch & 0x0f) + 9;
} else if (ch === 0x3e) {
// '>'
this.eof = true;
break;
} else {
// probably whitespace
continue; // ignoring
}
if (firstDigit < 0) {
firstDigit = digit;
} else {
buffer[bufferLength++] = (firstDigit << 4) | digit;
firstDigit = -1;
}
}
if (firstDigit >= 0 && this.eof) {
// incomplete byte
buffer[bufferLength++] = firstDigit << 4;
firstDigit = -1;
}
this.firstDigit = firstDigit;
this.bufferLength = bufferLength;
}
}
export default AsciiHexStream;

View file

@ -0,0 +1,170 @@
import { MethodNotImplementedError } from 'src/core/errors';
import Stream, { StreamType } from 'src/core/streams/Stream';
/*
* Copyright 2012 Mozilla Foundation
*
* The DecodeStream class contained in this file is a TypeScript port of the
* JavaScript DecodeStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
// Lots of DecodeStreams are created whose buffers are never used. For these
// we share a single empty buffer. This is (a) space-efficient and (b) avoids
// having special cases that would be required if we used |null| for an empty
// buffer.
const emptyBuffer = new Uint8Array(0);
/**
* Super class for the decoding streams
*/
class DecodeStream implements StreamType {
protected bufferLength: number;
protected buffer: Uint8Array;
protected eof: boolean;
private pos: number;
private minBufferLength: number;
constructor(maybeMinBufferLength?: number) {
this.pos = 0;
this.bufferLength = 0;
this.eof = false;
this.buffer = emptyBuffer;
this.minBufferLength = 512;
if (maybeMinBufferLength) {
// Compute the first power of two that is as big as maybeMinBufferLength.
while (this.minBufferLength < maybeMinBufferLength) {
this.minBufferLength *= 2;
}
}
}
get isEmpty() {
while (!this.eof && this.bufferLength === 0) {
this.readBlock();
}
return this.bufferLength === 0;
}
getByte() {
const pos = this.pos;
while (this.bufferLength <= pos) {
if (this.eof) {
return -1;
}
this.readBlock();
}
return this.buffer[this.pos++];
}
getUint16() {
const b0 = this.getByte();
const b1 = this.getByte();
if (b0 === -1 || b1 === -1) {
return -1;
}
return (b0 << 8) + b1;
}
getInt32() {
const b0 = this.getByte();
const b1 = this.getByte();
const b2 = this.getByte();
const b3 = this.getByte();
return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
}
getBytes(length: number, forceClamped = false) {
let end;
const pos = this.pos;
if (length) {
this.ensureBuffer(pos + length);
end = pos + length;
while (!this.eof && this.bufferLength < end) {
this.readBlock();
}
const bufEnd = this.bufferLength;
if (end > bufEnd) {
end = bufEnd;
}
} else {
while (!this.eof) {
this.readBlock();
}
end = this.bufferLength;
}
this.pos = end;
const subarray = this.buffer.subarray(pos, end);
// `this.buffer` is either a `Uint8Array` or `Uint8ClampedArray` here.
return forceClamped && !(subarray instanceof Uint8ClampedArray)
? new Uint8ClampedArray(subarray)
: subarray;
}
peekByte() {
const peekedByte = this.getByte();
this.pos--;
return peekedByte;
}
peekBytes(length: number, forceClamped = false) {
const bytes = this.getBytes(length, forceClamped);
this.pos -= bytes.length;
return bytes;
}
skip(n: number) {
if (!n) {
n = 1;
}
this.pos += n;
}
reset() {
this.pos = 0;
}
makeSubStream(start: number, length: number /* dict */) {
const end = start + length;
while (this.bufferLength <= end && !this.eof) {
this.readBlock();
}
return new Stream(this.buffer, start, length /* dict */);
}
decode(): Uint8Array {
while (!this.eof) this.readBlock();
return this.buffer.subarray(0, this.bufferLength);
}
protected readBlock(): void {
throw new MethodNotImplementedError(this.constructor.name, 'readBlock');
}
protected ensureBuffer(requested: number) {
const buffer = this.buffer;
if (requested <= buffer.byteLength) {
return buffer;
}
let size = this.minBufferLength;
while (size < requested) {
size *= 2;
}
const buffer2 = new Uint8Array(size);
buffer2.set(buffer);
return (this.buffer = buffer2);
}
// getBaseStreams() {
// if (this.str && this.str.getBaseStreams) {
// return this.str.getBaseStreams();
// }
// return [];
// }
}
export default DecodeStream;

View file

@ -0,0 +1,407 @@
/*
* Copyright 1996-2003 Glyph & Cog, LLC
*
* The flate stream implementation contained in this file is a JavaScript port
* of XPDF's implementation, made available under the Apache 2.0 open source
* license.
*/
/*
* Copyright 2012 Mozilla Foundation
*
* The FlateStream class contained in this file is a TypeScript port of the
* JavaScript FlateStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
/* tslint:disable no-conditional-assignment */
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
// prettier-ignore
const codeLenCodeMap = new Int32Array([
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
]);
// prettier-ignore
const lengthDecode = new Int32Array([
0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a,
0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f,
0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073,
0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102
]);
// prettier-ignore
const distDecode = new Int32Array([
0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d,
0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1,
0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01,
0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001
]);
// prettier-ignore
const fixedLitCodeTab = [new Int32Array([
0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0,
0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0,
0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0,
0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0,
0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8,
0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, 0x80084, 0x80044, 0x900e8,
0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, 0x8003c, 0x900d8,
0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, 0x900f8,
0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4,
0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4,
0x70105, 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4,
0x7010d, 0x8006a, 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4,
0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc,
0x7010b, 0x80066, 0x80026, 0x900ac, 0x80006, 0x80086, 0x80046, 0x900ec,
0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, 0x8007e, 0x8003e, 0x900dc,
0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, 0x8004e, 0x900fc,
0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c2,
0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2,
0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2,
0x7010c, 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2,
0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca,
0x7010a, 0x80065, 0x80025, 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea,
0x70106, 0x8005d, 0x8001d, 0x9009a, 0x70116, 0x8007d, 0x8003d, 0x900da,
0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, 0x8008d, 0x8004d, 0x900fa,
0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c6,
0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, 0x900e6,
0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6,
0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6,
0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce,
0x7010b, 0x80067, 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee,
0x70107, 0x8005f, 0x8001f, 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de,
0x7010f, 0x8006f, 0x8002f, 0x900be, 0x8000f, 0x8008f, 0x8004f, 0x900fe,
0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c1,
0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, 0x80040, 0x900e1,
0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, 0x900d1,
0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1,
0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9,
0x7010a, 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9,
0x70106, 0x8005c, 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9,
0x7010e, 0x8006c, 0x8002c, 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9,
0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c5,
0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, 0x80082, 0x80042, 0x900e5,
0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, 0x8003a, 0x900d5,
0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, 0x900f5,
0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd,
0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed,
0x70107, 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd,
0x7010f, 0x8006e, 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd,
0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3,
0x70108, 0x80061, 0x80021, 0x900a3, 0x80001, 0x80081, 0x80041, 0x900e3,
0x70104, 0x80059, 0x80019, 0x90093, 0x70114, 0x80079, 0x80039, 0x900d3,
0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, 0x80049, 0x900f3,
0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900cb,
0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb,
0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db,
0x7010e, 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb,
0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7,
0x70109, 0x80063, 0x80023, 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7,
0x70105, 0x8005b, 0x8001b, 0x90097, 0x70115, 0x8007b, 0x8003b, 0x900d7,
0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, 0x8008b, 0x8004b, 0x900f7,
0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900cf,
0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef,
0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df,
0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff
]), 9] as [Int32Array, number];
// prettier-ignore
const fixedDistCodeTab = [new Int32Array([
0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c,
0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000,
0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d,
0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000
]), 5] as [Int32Array, number];
class FlateStream extends DecodeStream {
private stream: StreamType;
private codeSize: number;
private codeBuf: number;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
const cmf = stream.getByte();
const flg = stream.getByte();
if (cmf === -1 || flg === -1) {
throw new Error(`Invalid header in flate stream: ${cmf}, ${flg}`);
}
if ((cmf & 0x0f) !== 0x08) {
throw new Error(
`Unknown compression method in flate stream: ${cmf}, ${flg}`,
);
}
if (((cmf << 8) + flg) % 31 !== 0) {
throw new Error(`Bad FCHECK in flate stream: ${cmf}, ${flg}`);
}
if (flg & 0x20) {
throw new Error(`FDICT bit set in flate stream: ${cmf}, ${flg}`);
}
this.codeSize = 0;
this.codeBuf = 0;
}
protected readBlock() {
let buffer;
let len;
const str = this.stream;
// read block header
let hdr = this.getBits(3);
if (hdr & 1) {
this.eof = true;
}
hdr >>= 1;
if (hdr === 0) {
// uncompressed block
let b;
if ((b = str.getByte()) === -1) {
throw new Error('Bad block header in flate stream');
}
let blockLen = b;
if ((b = str.getByte()) === -1) {
throw new Error('Bad block header in flate stream');
}
blockLen |= b << 8;
if ((b = str.getByte()) === -1) {
throw new Error('Bad block header in flate stream');
}
let check = b;
if ((b = str.getByte()) === -1) {
throw new Error('Bad block header in flate stream');
}
check |= b << 8;
if (check !== (~blockLen & 0xffff) && (blockLen !== 0 || check !== 0)) {
// Ignoring error for bad "empty" block (see issue 1277)
throw new Error('Bad uncompressed block length in flate stream');
}
this.codeBuf = 0;
this.codeSize = 0;
const bufferLength = this.bufferLength;
buffer = this.ensureBuffer(bufferLength + blockLen);
const end = bufferLength + blockLen;
this.bufferLength = end;
if (blockLen === 0) {
if (str.peekByte() === -1) {
this.eof = true;
}
} else {
for (let n = bufferLength; n < end; ++n) {
if ((b = str.getByte()) === -1) {
this.eof = true;
break;
}
buffer[n] = b;
}
}
return;
}
let litCodeTable;
let distCodeTable;
if (hdr === 1) {
// compressed block, fixed codes
litCodeTable = fixedLitCodeTab;
distCodeTable = fixedDistCodeTab;
} else if (hdr === 2) {
// compressed block, dynamic codes
const numLitCodes = this.getBits(5) + 257;
const numDistCodes = this.getBits(5) + 1;
const numCodeLenCodes = this.getBits(4) + 4;
// build the code lengths code table
const codeLenCodeLengths = new Uint8Array(codeLenCodeMap.length);
let i;
for (i = 0; i < numCodeLenCodes; ++i) {
codeLenCodeLengths[codeLenCodeMap[i]] = this.getBits(3);
}
const codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths);
// build the literal and distance code tables
len = 0;
i = 0;
const codes = numLitCodes + numDistCodes;
const codeLengths = new Uint8Array(codes);
let bitsLength;
let bitsOffset;
let what;
while (i < codes) {
const code = this.getCode(codeLenCodeTab);
if (code === 16) {
bitsLength = 2;
bitsOffset = 3;
what = len;
} else if (code === 17) {
bitsLength = 3;
bitsOffset = 3;
what = len = 0;
} else if (code === 18) {
bitsLength = 7;
bitsOffset = 11;
what = len = 0;
} else {
codeLengths[i++] = len = code;
continue;
}
let repeatLength = this.getBits(bitsLength) + bitsOffset;
while (repeatLength-- > 0) {
codeLengths[i++] = what;
}
}
litCodeTable = this.generateHuffmanTable(
codeLengths.subarray(0, numLitCodes),
);
distCodeTable = this.generateHuffmanTable(
codeLengths.subarray(numLitCodes, codes),
);
} else {
throw new Error('Unknown block type in flate stream');
}
buffer = this.buffer;
let limit = buffer ? buffer.length : 0;
let pos = this.bufferLength;
while (true) {
let code1 = this.getCode(litCodeTable);
if (code1 < 256) {
if (pos + 1 >= limit) {
buffer = this.ensureBuffer(pos + 1);
limit = buffer.length;
}
buffer[pos++] = code1;
continue;
}
if (code1 === 256) {
this.bufferLength = pos;
return;
}
code1 -= 257;
code1 = lengthDecode[code1];
let code2 = code1 >> 16;
if (code2 > 0) {
code2 = this.getBits(code2);
}
len = (code1 & 0xffff) + code2;
code1 = this.getCode(distCodeTable);
code1 = distDecode[code1];
code2 = code1 >> 16;
if (code2 > 0) {
code2 = this.getBits(code2);
}
const dist = (code1 & 0xffff) + code2;
if (pos + len >= limit) {
buffer = this.ensureBuffer(pos + len);
limit = buffer.length;
}
for (let k = 0; k < len; ++k, ++pos) {
buffer[pos] = buffer[pos - dist];
}
}
}
private getBits(bits: number) {
const str = this.stream;
let codeSize = this.codeSize;
let codeBuf = this.codeBuf;
let b;
while (codeSize < bits) {
if ((b = str.getByte()) === -1) {
throw new Error('Bad encoding in flate stream');
}
codeBuf |= b << codeSize;
codeSize += 8;
}
b = codeBuf & ((1 << bits) - 1);
this.codeBuf = codeBuf >> bits;
this.codeSize = codeSize -= bits;
return b;
}
private getCode(table: [Int32Array, number]) {
const str = this.stream;
const codes = table[0];
const maxLen = table[1];
let codeSize = this.codeSize;
let codeBuf = this.codeBuf;
let b;
while (codeSize < maxLen) {
if ((b = str.getByte()) === -1) {
// premature end of stream. code might however still be valid.
// codeSize < codeLen check below guards against incomplete codeVal.
break;
}
codeBuf |= b << codeSize;
codeSize += 8;
}
const code = codes[codeBuf & ((1 << maxLen) - 1)];
if (typeof codes === 'number') {
console.log('FLATE:', code);
}
const codeLen = code >> 16;
const codeVal = code & 0xffff;
if (codeLen < 1 || codeSize < codeLen) {
throw new Error('Bad encoding in flate stream');
}
this.codeBuf = codeBuf >> codeLen;
this.codeSize = codeSize - codeLen;
return codeVal;
}
private generateHuffmanTable(lengths: Uint8Array): [Int32Array, number] {
const n = lengths.length;
// find max code length
let maxLen = 0;
let i;
for (i = 0; i < n; ++i) {
if (lengths[i] > maxLen) {
maxLen = lengths[i];
}
}
// build the table
const size = 1 << maxLen;
const codes = new Int32Array(size);
for (
let len = 1, code = 0, skip = 2;
len <= maxLen;
++len, code <<= 1, skip <<= 1
) {
for (let val = 0; val < n; ++val) {
if (lengths[val] === len) {
// bit-reverse the code
let code2 = 0;
let t = code;
for (i = 0; i < len; ++i) {
code2 = (code2 << 1) | (t & 1);
t >>= 1;
}
// fill the table entries
for (i = code2; i < size; i += skip) {
codes[i] = (len << 16) | val;
}
++code;
}
}
}
return [codes, maxLen];
}
}
export default FlateStream;

View file

@ -0,0 +1,164 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The LZWStream class contained in this file is a TypeScript port of the
* JavaScript LZWStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
class LZWStream extends DecodeStream {
private stream: StreamType;
private cachedData: number;
private bitsCached: number;
private lzwState: {
earlyChange: 0 | 1;
codeLength: number;
nextCode: number;
dictionaryValues: Uint8Array;
dictionaryLengths: Uint16Array;
dictionaryPrevCodes: Uint16Array;
currentSequence: Uint8Array;
currentSequenceLength: number;
prevCode?: number | null;
};
constructor(
stream: StreamType,
maybeLength: number | undefined,
earlyChange: 0 | 1,
) {
super(maybeLength);
this.stream = stream;
this.cachedData = 0;
this.bitsCached = 0;
const maxLzwDictionarySize = 4096;
const lzwState = {
earlyChange,
codeLength: 9,
nextCode: 258,
dictionaryValues: new Uint8Array(maxLzwDictionarySize),
dictionaryLengths: new Uint16Array(maxLzwDictionarySize),
dictionaryPrevCodes: new Uint16Array(maxLzwDictionarySize),
currentSequence: new Uint8Array(maxLzwDictionarySize),
currentSequenceLength: 0,
};
for (let i = 0; i < 256; ++i) {
lzwState.dictionaryValues[i] = i;
lzwState.dictionaryLengths[i] = 1;
}
this.lzwState = lzwState;
}
protected readBlock() {
const blockSize = 512;
let estimatedDecodedSize = blockSize * 2;
const decodedSizeDelta = blockSize;
let i;
let j;
let q;
const lzwState = this.lzwState;
if (!lzwState) {
return; // eof was found
}
const earlyChange = lzwState.earlyChange;
let nextCode = lzwState.nextCode;
const dictionaryValues = lzwState.dictionaryValues;
const dictionaryLengths = lzwState.dictionaryLengths;
const dictionaryPrevCodes = lzwState.dictionaryPrevCodes;
let codeLength = lzwState.codeLength;
let prevCode = lzwState.prevCode;
const currentSequence = lzwState.currentSequence;
let currentSequenceLength = lzwState.currentSequenceLength;
let decodedLength = 0;
let currentBufferLength = this.bufferLength;
let buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize);
for (i = 0; i < blockSize; i++) {
const code = this.readBits(codeLength);
const hasPrev = currentSequenceLength > 0;
if (!code || code < 256) {
currentSequence[0] = code as number;
currentSequenceLength = 1;
} else if (code >= 258) {
if (code < nextCode) {
currentSequenceLength = dictionaryLengths[code];
for (j = currentSequenceLength - 1, q = code; j >= 0; j--) {
currentSequence[j] = dictionaryValues[q];
q = dictionaryPrevCodes[q];
}
} else {
currentSequence[currentSequenceLength++] = currentSequence[0];
}
} else if (code === 256) {
codeLength = 9;
nextCode = 258;
currentSequenceLength = 0;
continue;
} else {
this.eof = true;
delete this.lzwState;
break;
}
if (hasPrev) {
dictionaryPrevCodes[nextCode] = prevCode as number;
dictionaryLengths[nextCode] = dictionaryLengths[prevCode as number] + 1;
dictionaryValues[nextCode] = currentSequence[0];
nextCode++;
codeLength =
(nextCode + earlyChange) & (nextCode + earlyChange - 1)
? codeLength
: Math.min(
Math.log(nextCode + earlyChange) / 0.6931471805599453 + 1,
12,
) | 0;
}
prevCode = code;
decodedLength += currentSequenceLength;
if (estimatedDecodedSize < decodedLength) {
do {
estimatedDecodedSize += decodedSizeDelta;
} while (estimatedDecodedSize < decodedLength);
buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize);
}
for (j = 0; j < currentSequenceLength; j++) {
buffer[currentBufferLength++] = currentSequence[j];
}
}
lzwState.nextCode = nextCode;
lzwState.codeLength = codeLength;
lzwState.prevCode = prevCode;
lzwState.currentSequenceLength = currentSequenceLength;
this.bufferLength = currentBufferLength;
}
private readBits(n: number) {
let bitsCached = this.bitsCached;
let cachedData = this.cachedData;
while (bitsCached < n) {
const c = this.stream.getByte();
if (c === -1) {
this.eof = true;
return null;
}
cachedData = (cachedData << 8) | c;
bitsCached += 8;
}
this.bitsCached = bitsCached -= n;
this.cachedData = cachedData;
return (cachedData >>> bitsCached) & ((1 << n) - 1);
}
}
export default LZWStream;

View file

@ -0,0 +1,55 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The RunLengthStream class contained in this file is a TypeScript port of the
* JavaScript RunLengthStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
class RunLengthStream extends DecodeStream {
private stream: StreamType;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
}
protected readBlock() {
// The repeatHeader has following format. The first byte defines type of run
// and amount of bytes to repeat/copy: n = 0 through 127 - copy next n bytes
// (in addition to the second byte from the header), n = 129 through 255 -
// duplicate the second byte from the header (257 - n) times, n = 128 - end.
const repeatHeader = this.stream.getBytes(2);
if (!repeatHeader || repeatHeader.length < 2 || repeatHeader[0] === 128) {
this.eof = true;
return;
}
let buffer;
let bufferLength = this.bufferLength;
let n = repeatHeader[0];
if (n < 128) {
// copy n bytes
buffer = this.ensureBuffer(bufferLength + n + 1);
buffer[bufferLength++] = repeatHeader[1];
if (n > 0) {
const source = this.stream.getBytes(n);
buffer.set(source, bufferLength);
bufferLength += n;
}
} else {
n = 257 - n;
const b = repeatHeader[1];
buffer = this.ensureBuffer(bufferLength + n + 1);
for (let i = 0; i < n; i++) {
buffer[bufferLength++] = b;
}
}
this.bufferLength = bufferLength;
}
}
export default RunLengthStream;

View file

@ -0,0 +1,132 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The Stream class contained in this file is a TypeScript port of the
* JavaScript Stream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
export interface StreamType {
isEmpty: boolean;
getByte(): number;
getUint16(): number;
getInt32(): number;
getBytes(
length: number,
forceClamped?: boolean,
): Uint8Array | Uint8ClampedArray;
peekByte(): number;
peekBytes(
length: number,
forceClamped?: boolean,
): Uint8Array | Uint8ClampedArray;
skip(n: number): void;
reset(): void;
makeSubStream(start: number, length: number): StreamType;
decode(): Uint8Array;
}
class Stream implements StreamType {
private bytes: Uint8Array;
private start: number;
private pos: number;
private end: number;
constructor(buffer: Uint8Array, start?: number, length?: number) {
this.bytes = buffer;
this.start = start || 0;
this.pos = this.start;
this.end = !!start && !!length ? start + length : this.bytes.length;
}
get length() {
return this.end - this.start;
}
get isEmpty() {
return this.length === 0;
}
getByte() {
if (this.pos >= this.end) {
return -1;
}
return this.bytes[this.pos++];
}
getUint16() {
const b0 = this.getByte();
const b1 = this.getByte();
if (b0 === -1 || b1 === -1) {
return -1;
}
return (b0 << 8) + b1;
}
getInt32() {
const b0 = this.getByte();
const b1 = this.getByte();
const b2 = this.getByte();
const b3 = this.getByte();
return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
}
// Returns subarray of original buffer, should only be read.
getBytes(length: number, forceClamped = false) {
const bytes = this.bytes;
const pos = this.pos;
const strEnd = this.end;
if (!length) {
const subarray = bytes.subarray(pos, strEnd);
// `this.bytes` is always a `Uint8Array` here.
return forceClamped ? new Uint8ClampedArray(subarray) : subarray;
} else {
let end = pos + length;
if (end > strEnd) {
end = strEnd;
}
this.pos = end;
const subarray = bytes.subarray(pos, end);
// `this.bytes` is always a `Uint8Array` here.
return forceClamped ? new Uint8ClampedArray(subarray) : subarray;
}
}
peekByte() {
const peekedByte = this.getByte();
this.pos--;
return peekedByte;
}
peekBytes(length: number, forceClamped = false) {
const bytes = this.getBytes(length, forceClamped);
this.pos -= bytes.length;
return bytes;
}
skip(n: number) {
if (!n) {
n = 1;
}
this.pos += n;
}
reset() {
this.pos = this.start;
}
moveStart() {
this.start = this.pos;
}
makeSubStream(start: number, length: number) {
return new Stream(this.bytes, start, length);
}
decode(): Uint8Array {
return this.bytes;
}
}
export default Stream;

View file

@ -0,0 +1,73 @@
import {
UnexpectedObjectTypeError,
UnsupportedEncodingError,
} from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import Ascii85Stream from 'src/core/streams/Ascii85Stream';
import AsciiHexStream from 'src/core/streams/AsciiHexStream';
import FlateStream from 'src/core/streams/FlateStream';
import LZWStream from 'src/core/streams/LZWStream';
import RunLengthStream from 'src/core/streams/RunLengthStream';
import Stream, { StreamType } from 'src/core/streams/Stream';
const decodeStream = (
stream: StreamType,
encoding: PDFName,
params: undefined | typeof PDFNull | PDFDict,
) => {
if (encoding === PDFName.of('FlateDecode')) {
return new FlateStream(stream);
}
if (encoding === PDFName.of('LZWDecode')) {
let earlyChange = 1;
if (params instanceof PDFDict) {
const EarlyChange = params.lookup(PDFName.of('EarlyChange'));
if (EarlyChange instanceof PDFNumber) {
earlyChange = EarlyChange.asNumber();
}
}
return new LZWStream(stream, undefined, earlyChange as 0 | 1);
}
if (encoding === PDFName.of('ASCII85Decode')) {
return new Ascii85Stream(stream);
}
if (encoding === PDFName.of('ASCIIHexDecode')) {
return new AsciiHexStream(stream);
}
if (encoding === PDFName.of('RunLengthDecode')) {
return new RunLengthStream(stream);
}
throw new UnsupportedEncodingError(encoding.asString());
};
export const decodePDFRawStream = ({ dict, contents }: PDFRawStream) => {
let stream: StreamType = new Stream(contents);
const Filter = dict.lookup(PDFName.of('Filter'));
const DecodeParms = dict.lookup(PDFName.of('DecodeParms'));
if (Filter instanceof PDFName) {
stream = decodeStream(
stream,
Filter,
DecodeParms as PDFDict | typeof PDFNull | undefined,
);
} else if (Filter instanceof PDFArray) {
for (let idx = 0, len = Filter.size(); idx < len; idx++) {
stream = decodeStream(
stream,
Filter.lookup(idx, PDFName),
DecodeParms && (DecodeParms as PDFArray).lookupMaybe(idx, PDFDict),
);
}
} else if (!!Filter) {
throw new UnexpectedObjectTypeError([PDFName, PDFArray], Filter);
}
return stream;
};

View file

@ -0,0 +1,85 @@
import PDFDict, { DictMap } from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFPageTree from 'src/core/structures/PDFPageTree';
import { PDFAcroForm } from 'src/core/acroform';
import ViewerPreferences from '../interactive/ViewerPreferences';
class PDFCatalog extends PDFDict {
static withContextAndPages = (
context: PDFContext,
pages: PDFPageTree | PDFRef,
) => {
const dict = new Map();
dict.set(PDFName.of('Type'), PDFName.of('Catalog'));
dict.set(PDFName.of('Pages'), pages);
return new PDFCatalog(dict, context);
};
static fromMapWithContext = (map: DictMap, context: PDFContext) =>
new PDFCatalog(map, context);
Pages(): PDFPageTree {
return this.lookup(PDFName.of('Pages'), PDFDict) as PDFPageTree;
}
AcroForm(): PDFDict | undefined {
return this.lookupMaybe(PDFName.of('AcroForm'), PDFDict);
}
getAcroForm(): PDFAcroForm | undefined {
const dict = this.AcroForm();
if (!dict) return undefined;
return PDFAcroForm.fromDict(dict);
}
getOrCreateAcroForm(): PDFAcroForm {
let acroForm = this.getAcroForm();
if (!acroForm) {
acroForm = PDFAcroForm.create(this.context);
const acroFormRef = this.context.register(acroForm.dict);
this.set(PDFName.of('AcroForm'), acroFormRef);
}
return acroForm;
}
ViewerPreferences(): PDFDict | undefined {
return this.lookupMaybe(PDFName.of('ViewerPreferences'), PDFDict);
}
getViewerPreferences(): ViewerPreferences | undefined {
const dict = this.ViewerPreferences();
if (!dict) return undefined;
return ViewerPreferences.fromDict(dict);
}
getOrCreateViewerPreferences(): ViewerPreferences {
let viewerPrefs = this.getViewerPreferences();
if (!viewerPrefs) {
viewerPrefs = ViewerPreferences.create(this.context);
const viewerPrefsRef = this.context.register(viewerPrefs.dict);
this.set(PDFName.of('ViewerPreferences'), viewerPrefsRef);
}
return viewerPrefs;
}
/**
* Inserts the given ref as a leaf node of this catalog's page tree at the
* specified index (zero-based). Also increments the `Count` of each node in
* the page tree hierarchy to accomodate the new page.
*
* Returns the ref of the PDFPageTree node into which `leafRef` was inserted.
*/
insertLeafNode(leafRef: PDFRef, index: number): PDFRef {
const pagesRef = this.get(PDFName.of('Pages')) as PDFRef;
const maybeParentRef = this.Pages().insertLeafNode(leafRef, index);
return maybeParentRef || pagesRef;
}
removeLeafNode(index: number): void {
this.Pages().removeLeafNode(index);
}
}
export default PDFCatalog;

View file

@ -0,0 +1,58 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFOperator from 'src/core/operators/PDFOperator';
import PDFContext from 'src/core/PDFContext';
import PDFFlateStream from 'src/core/structures/PDFFlateStream';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFContentStream extends PDFFlateStream {
static of = (dict: PDFDict, operators: PDFOperator[], encode = true) =>
new PDFContentStream(dict, operators, encode);
private readonly operators: PDFOperator[];
private constructor(dict: PDFDict, operators: PDFOperator[], encode = true) {
super(dict, encode);
this.operators = operators;
}
push(...operators: PDFOperator[]): void {
this.operators.push(...operators);
}
clone(context?: PDFContext): PDFContentStream {
const operators = new Array(this.operators.length);
for (let idx = 0, len = this.operators.length; idx < len; idx++) {
operators[idx] = this.operators[idx].clone(context);
}
const { dict, encode } = this;
return PDFContentStream.of(dict.clone(context), operators, encode);
}
getContentsString(): string {
let value = '';
for (let idx = 0, len = this.operators.length; idx < len; idx++) {
value += `${this.operators[idx]}\n`;
}
return value;
}
getUnencodedContents(): Uint8Array {
const buffer = new Uint8Array(this.getUnencodedContentsSize());
let offset = 0;
for (let idx = 0, len = this.operators.length; idx < len; idx++) {
offset += this.operators[idx].copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
}
return buffer;
}
getUnencodedContentsSize(): number {
let size = 0;
for (let idx = 0, len = this.operators.length; idx < len; idx++) {
size += this.operators[idx].sizeInBytes() + 1;
}
return size;
}
}
export default PDFContentStream;

View file

@ -0,0 +1,246 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFFlateStream from 'src/core/structures/PDFFlateStream';
import { bytesFor, Cache, reverseArray, sizeInBytes, sum } from 'src/utils';
export enum EntryType {
Deleted = 0,
Uncompressed = 1,
Compressed = 2,
}
export interface DeletedEntry {
type: EntryType.Deleted;
ref: PDFRef;
nextFreeObjectNumber: number;
}
export interface UncompressedEntry {
type: EntryType.Uncompressed;
ref: PDFRef;
offset: number;
}
export interface CompressedEntry {
type: EntryType.Compressed;
ref: PDFRef;
objectStreamRef: PDFRef;
index: number;
}
export type Entry = DeletedEntry | UncompressedEntry | CompressedEntry;
export type EntryTuple = [number, number, number];
/**
* Entries should be added using the [[addDeletedEntry]],
* [[addUncompressedEntry]], and [[addCompressedEntry]] methods
* **in order of ascending object number**.
*/
class PDFCrossRefStream extends PDFFlateStream {
static create = (dict: PDFDict, encode = true) => {
const stream = new PDFCrossRefStream(dict, [], encode);
stream.addDeletedEntry(PDFRef.of(0, 65535), 0);
return stream;
};
static of = (dict: PDFDict, entries: Entry[], encode = true) =>
new PDFCrossRefStream(dict, entries, encode);
private readonly entries: Entry[];
private readonly entryTuplesCache: Cache<EntryTuple[]>;
private readonly maxByteWidthsCache: Cache<[number, number, number]>;
private readonly indexCache: Cache<number[]>;
private constructor(dict: PDFDict, entries?: Entry[], encode = true) {
super(dict, encode);
this.entries = entries || [];
this.entryTuplesCache = Cache.populatedBy(this.computeEntryTuples);
this.maxByteWidthsCache = Cache.populatedBy(this.computeMaxEntryByteWidths);
this.indexCache = Cache.populatedBy(this.computeIndex);
dict.set(PDFName.of('Type'), PDFName.of('XRef'));
}
addDeletedEntry(ref: PDFRef, nextFreeObjectNumber: number) {
const type = EntryType.Deleted;
this.entries.push({ type, ref, nextFreeObjectNumber });
this.entryTuplesCache.invalidate();
this.maxByteWidthsCache.invalidate();
this.indexCache.invalidate();
this.contentsCache.invalidate();
}
addUncompressedEntry(ref: PDFRef, offset: number) {
const type = EntryType.Uncompressed;
this.entries.push({ type, ref, offset });
this.entryTuplesCache.invalidate();
this.maxByteWidthsCache.invalidate();
this.indexCache.invalidate();
this.contentsCache.invalidate();
}
addCompressedEntry(ref: PDFRef, objectStreamRef: PDFRef, index: number) {
const type = EntryType.Compressed;
this.entries.push({ type, ref, objectStreamRef, index });
this.entryTuplesCache.invalidate();
this.maxByteWidthsCache.invalidate();
this.indexCache.invalidate();
this.contentsCache.invalidate();
}
clone(context?: PDFContext): PDFCrossRefStream {
const { dict, entries, encode } = this;
return PDFCrossRefStream.of(dict.clone(context), entries.slice(), encode);
}
getContentsString(): string {
const entryTuples = this.entryTuplesCache.access();
const byteWidths = this.maxByteWidthsCache.access();
let value = '';
for (
let entryIdx = 0, entriesLen = entryTuples.length;
entryIdx < entriesLen;
entryIdx++
) {
const [first, second, third] = entryTuples[entryIdx];
const firstBytes = reverseArray(bytesFor(first));
const secondBytes = reverseArray(bytesFor(second));
const thirdBytes = reverseArray(bytesFor(third));
for (let idx = byteWidths[0] - 1; idx >= 0; idx--) {
value += (firstBytes[idx] || 0).toString(2);
}
for (let idx = byteWidths[1] - 1; idx >= 0; idx--) {
value += (secondBytes[idx] || 0).toString(2);
}
for (let idx = byteWidths[2] - 1; idx >= 0; idx--) {
value += (thirdBytes[idx] || 0).toString(2);
}
}
return value;
}
getUnencodedContents(): Uint8Array {
const entryTuples = this.entryTuplesCache.access();
const byteWidths = this.maxByteWidthsCache.access();
const buffer = new Uint8Array(this.getUnencodedContentsSize());
let offset = 0;
for (
let entryIdx = 0, entriesLen = entryTuples.length;
entryIdx < entriesLen;
entryIdx++
) {
const [first, second, third] = entryTuples[entryIdx];
const firstBytes = reverseArray(bytesFor(first));
const secondBytes = reverseArray(bytesFor(second));
const thirdBytes = reverseArray(bytesFor(third));
for (let idx = byteWidths[0] - 1; idx >= 0; idx--) {
buffer[offset++] = firstBytes[idx] || 0;
}
for (let idx = byteWidths[1] - 1; idx >= 0; idx--) {
buffer[offset++] = secondBytes[idx] || 0;
}
for (let idx = byteWidths[2] - 1; idx >= 0; idx--) {
buffer[offset++] = thirdBytes[idx] || 0;
}
}
return buffer;
}
getUnencodedContentsSize(): number {
const byteWidths = this.maxByteWidthsCache.access();
const entryWidth = sum(byteWidths);
return entryWidth * this.entries.length;
}
updateDict(): void {
super.updateDict();
const byteWidths = this.maxByteWidthsCache.access();
const index = this.indexCache.access();
const { context } = this.dict;
this.dict.set(PDFName.of('W'), context.obj(byteWidths));
this.dict.set(PDFName.of('Index'), context.obj(index));
}
// Returns an array of integer pairs for each subsection of the cross ref
// section, where each integer pair represents:
// firstObjectNumber(OfSection), length(OfSection)
private computeIndex = (): number[] => {
const subsections: number[] = [];
let subsectionLength = 0;
for (let idx = 0, len = this.entries.length; idx < len; idx++) {
const currEntry = this.entries[idx];
const prevEntry = this.entries[idx - 1];
if (idx === 0) {
subsections.push(currEntry.ref.objectNumber);
} else if (currEntry.ref.objectNumber - prevEntry.ref.objectNumber > 1) {
subsections.push(subsectionLength);
subsections.push(currEntry.ref.objectNumber);
subsectionLength = 0;
}
subsectionLength += 1;
}
subsections.push(subsectionLength);
return subsections;
};
private computeEntryTuples = (): EntryTuple[] => {
const entryTuples: EntryTuple[] = new Array(this.entries.length);
for (let idx = 0, len = this.entries.length; idx < len; idx++) {
const entry = this.entries[idx];
if (entry.type === EntryType.Deleted) {
const { type, nextFreeObjectNumber, ref } = entry;
entryTuples[idx] = [type, nextFreeObjectNumber, ref.generationNumber];
}
if (entry.type === EntryType.Uncompressed) {
const { type, offset, ref } = entry;
entryTuples[idx] = [type, offset, ref.generationNumber];
}
if (entry.type === EntryType.Compressed) {
const { type, objectStreamRef, index } = entry;
entryTuples[idx] = [type, objectStreamRef.objectNumber, index];
}
}
return entryTuples;
};
private computeMaxEntryByteWidths = (): [number, number, number] => {
const entryTuples = this.entryTuplesCache.access();
const widths: [number, number, number] = [0, 0, 0];
for (let idx = 0, len = entryTuples.length; idx < len; idx++) {
const [first, second, third] = entryTuples[idx];
const firstSize = sizeInBytes(first);
const secondSize = sizeInBytes(second);
const thirdSize = sizeInBytes(third);
if (firstSize > widths[0]) widths[0] = firstSize;
if (secondSize > widths[1]) widths[1] = secondSize;
if (thirdSize > widths[2]) widths[2] = thirdSize;
}
return widths;
};
}
export default PDFCrossRefStream;

View file

@ -0,0 +1,43 @@
import pako from 'pako';
import { MethodNotImplementedError } from 'src/core/errors';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFStream from 'src/core/objects/PDFStream';
import { Cache } from 'src/utils';
class PDFFlateStream extends PDFStream {
protected readonly contentsCache: Cache<Uint8Array>;
protected readonly encode: boolean;
constructor(dict: PDFDict, encode: boolean) {
super(dict);
this.encode = encode;
if (encode) dict.set(PDFName.of('Filter'), PDFName.of('FlateDecode'));
this.contentsCache = Cache.populatedBy(this.computeContents);
}
computeContents = (): Uint8Array => {
const unencodedContents = this.getUnencodedContents();
return this.encode ? pako.deflate(unencodedContents) : unencodedContents;
};
getContents(): Uint8Array {
return this.contentsCache.access();
}
getContentsSize(): number {
return this.contentsCache.access().length;
}
getUnencodedContents(): Uint8Array {
throw new MethodNotImplementedError(
this.constructor.name,
'getUnencodedContents',
);
}
}
export default PDFFlateStream;

View file

@ -0,0 +1,101 @@
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFFlateStream from 'src/core/structures/PDFFlateStream';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer, last } from 'src/utils';
export type IndirectObject = [PDFRef, PDFObject];
class PDFObjectStream extends PDFFlateStream {
static withContextAndObjects = (
context: PDFContext,
objects: IndirectObject[],
encode = true,
) => new PDFObjectStream(context, objects, encode);
private readonly objects: IndirectObject[];
private readonly offsets: [number, number][];
private readonly offsetsString: string;
private constructor(
context: PDFContext,
objects: IndirectObject[],
encode = true,
) {
super(context.obj({}), encode);
this.objects = objects;
this.offsets = this.computeObjectOffsets();
this.offsetsString = this.computeOffsetsString();
this.dict.set(PDFName.of('Type'), PDFName.of('ObjStm'));
this.dict.set(PDFName.of('N'), PDFNumber.of(this.objects.length));
this.dict.set(PDFName.of('First'), PDFNumber.of(this.offsetsString.length));
}
getObjectsCount(): number {
return this.objects.length;
}
clone(context?: PDFContext): PDFObjectStream {
return PDFObjectStream.withContextAndObjects(
context || this.dict.context,
this.objects.slice(),
this.encode,
);
}
getContentsString(): string {
let value = this.offsetsString;
for (let idx = 0, len = this.objects.length; idx < len; idx++) {
const [, object] = this.objects[idx];
value += `${object}\n`;
}
return value;
}
getUnencodedContents(): Uint8Array {
const buffer = new Uint8Array(this.getUnencodedContentsSize());
let offset = copyStringIntoBuffer(this.offsetsString, buffer, 0);
for (let idx = 0, len = this.objects.length; idx < len; idx++) {
const [, object] = this.objects[idx];
offset += object.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
}
return buffer;
}
getUnencodedContentsSize(): number {
return (
this.offsetsString.length +
last(this.offsets)[1] +
last(this.objects)[1].sizeInBytes() +
1
);
}
private computeOffsetsString(): string {
let offsetsString = '';
for (let idx = 0, len = this.offsets.length; idx < len; idx++) {
const [objectNumber, offset] = this.offsets[idx];
offsetsString += `${objectNumber} ${offset} `;
}
return offsetsString;
}
private computeObjectOffsets(): [number, number][] {
let offset = 0;
const offsets = new Array(this.objects.length);
for (let idx = 0, len = this.objects.length; idx < len; idx++) {
const [ref, object] = this.objects[idx];
offsets[idx] = [ref.objectNumber, offset];
offset += object.sizeInBytes() + 1; // '\n'
}
return offsets;
}
}
export default PDFObjectStream;

View file

@ -0,0 +1,263 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict, { DictMap } from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import PDFPageTree from 'src/core/structures/PDFPageTree';
class PDFPageLeaf extends PDFDict {
static readonly InheritableEntries = [
'Resources',
'MediaBox',
'CropBox',
'Rotate',
];
static withContextAndParent = (context: PDFContext, parent: PDFRef) => {
const dict = new Map();
dict.set(PDFName.Type, PDFName.Page);
dict.set(PDFName.Parent, parent);
dict.set(PDFName.Resources, context.obj({}));
dict.set(PDFName.MediaBox, context.obj([0, 0, 612, 792]));
return new PDFPageLeaf(dict, context, false);
};
static fromMapWithContext = (
map: DictMap,
context: PDFContext,
autoNormalizeCTM = true,
) => new PDFPageLeaf(map, context, autoNormalizeCTM);
private normalized = false;
private readonly autoNormalizeCTM: boolean;
private constructor(
map: DictMap,
context: PDFContext,
autoNormalizeCTM = true,
) {
super(map, context);
this.autoNormalizeCTM = autoNormalizeCTM;
}
clone(context?: PDFContext): PDFPageLeaf {
const clone = PDFPageLeaf.fromMapWithContext(
new Map(),
context || this.context,
this.autoNormalizeCTM,
);
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clone.set(key, value);
}
return clone;
}
Parent(): PDFPageTree | undefined {
return this.lookupMaybe(PDFName.Parent, PDFDict) as PDFPageTree | undefined;
}
Contents(): PDFStream | PDFArray | undefined {
return this.lookup(PDFName.of('Contents')) as
| PDFStream
| PDFArray
| undefined;
}
Annots(): PDFArray | undefined {
return this.lookupMaybe(PDFName.Annots, PDFArray);
}
BleedBox(): PDFArray | undefined {
return this.lookupMaybe(PDFName.BleedBox, PDFArray);
}
TrimBox(): PDFArray | undefined {
return this.lookupMaybe(PDFName.TrimBox, PDFArray);
}
ArtBox(): PDFArray | undefined {
return this.lookupMaybe(PDFName.ArtBox, PDFArray);
}
Resources(): PDFDict | undefined {
const dictOrRef = this.getInheritableAttribute(PDFName.Resources);
return this.context.lookupMaybe(dictOrRef, PDFDict);
}
MediaBox(): PDFArray {
const arrayOrRef = this.getInheritableAttribute(PDFName.MediaBox);
return this.context.lookup(arrayOrRef, PDFArray);
}
CropBox(): PDFArray | undefined {
const arrayOrRef = this.getInheritableAttribute(PDFName.CropBox);
return this.context.lookupMaybe(arrayOrRef, PDFArray);
}
Rotate(): PDFNumber | undefined {
const numberOrRef = this.getInheritableAttribute(PDFName.Rotate);
return this.context.lookupMaybe(numberOrRef, PDFNumber);
}
getInheritableAttribute(name: PDFName): PDFObject | undefined {
let attribute: PDFObject | undefined;
this.ascend((node) => {
if (!attribute) attribute = node.get(name);
});
return attribute;
}
setParent(parentRef: PDFRef): void {
this.set(PDFName.Parent, parentRef);
}
addContentStream(contentStreamRef: PDFRef): void {
const Contents = this.normalizedEntries().Contents || this.context.obj([]);
this.set(PDFName.Contents, Contents);
Contents.push(contentStreamRef);
}
wrapContentStreams(startStream: PDFRef, endStream: PDFRef): boolean {
const Contents = this.Contents();
if (Contents instanceof PDFArray) {
Contents.insert(0, startStream);
Contents.push(endStream);
return true;
}
return false;
}
addAnnot(annotRef: PDFRef): void {
const { Annots } = this.normalizedEntries();
Annots.push(annotRef);
}
removeAnnot(annotRef: PDFRef) {
const { Annots } = this.normalizedEntries();
const index = Annots.indexOf(annotRef);
if (index !== undefined) {
Annots.remove(index);
}
}
setFontDictionary(name: PDFName, fontDictRef: PDFRef): void {
const { Font } = this.normalizedEntries();
Font.set(name, fontDictRef);
}
newFontDictionaryKey(tag: string): PDFName {
const { Font } = this.normalizedEntries();
return Font.uniqueKey(tag);
}
newFontDictionary(tag: string, fontDictRef: PDFRef): PDFName {
const key = this.newFontDictionaryKey(tag);
this.setFontDictionary(key, fontDictRef);
return key;
}
setXObject(name: PDFName, xObjectRef: PDFRef): void {
const { XObject } = this.normalizedEntries();
XObject.set(name, xObjectRef);
}
newXObjectKey(tag: string): PDFName {
const { XObject } = this.normalizedEntries();
return XObject.uniqueKey(tag);
}
newXObject(tag: string, xObjectRef: PDFRef): PDFName {
const key = this.newXObjectKey(tag);
this.setXObject(key, xObjectRef);
return key;
}
setExtGState(name: PDFName, extGStateRef: PDFRef | PDFDict): void {
const { ExtGState } = this.normalizedEntries();
ExtGState.set(name, extGStateRef);
}
newExtGStateKey(tag: string): PDFName {
const { ExtGState } = this.normalizedEntries();
return ExtGState.uniqueKey(tag);
}
newExtGState(tag: string, extGStateRef: PDFRef | PDFDict): PDFName {
const key = this.newExtGStateKey(tag);
this.setExtGState(key, extGStateRef);
return key;
}
ascend(visitor: (node: PDFPageTree | PDFPageLeaf) => any): void {
visitor(this);
const Parent = this.Parent();
if (Parent) Parent.ascend(visitor);
}
normalize() {
if (this.normalized) return;
const { context } = this;
const contentsRef = this.get(PDFName.Contents);
const contents = this.context.lookup(contentsRef);
if (contents instanceof PDFStream) {
this.set(PDFName.Contents, context.obj([contentsRef]));
}
if (this.autoNormalizeCTM) {
this.wrapContentStreams(
this.context.getPushGraphicsStateContentStream(),
this.context.getPopGraphicsStateContentStream(),
);
}
// TODO: Clone `Resources` if it is inherited
const dictOrRef = this.getInheritableAttribute(PDFName.Resources);
const Resources =
context.lookupMaybe(dictOrRef, PDFDict) || context.obj({});
this.set(PDFName.Resources, Resources);
// TODO: Clone `Font` if it is inherited
const Font =
Resources.lookupMaybe(PDFName.Font, PDFDict) || context.obj({});
Resources.set(PDFName.Font, Font);
// TODO: Clone `XObject` if it is inherited
const XObject =
Resources.lookupMaybe(PDFName.XObject, PDFDict) || context.obj({});
Resources.set(PDFName.XObject, XObject);
// TODO: Clone `ExtGState` if it is inherited
const ExtGState =
Resources.lookupMaybe(PDFName.ExtGState, PDFDict) || context.obj({});
Resources.set(PDFName.ExtGState, ExtGState);
const Annots = this.Annots() || context.obj([]);
this.set(PDFName.Annots, Annots);
this.normalized = true;
}
normalizedEntries() {
this.normalize();
const Annots = this.Annots()!;
const Resources = this.Resources()!;
const Contents = this.Contents() as PDFArray | undefined;
return {
Annots,
Resources,
Contents,
Font: Resources.lookup(PDFName.Font, PDFDict),
XObject: Resources.lookup(PDFName.XObject, PDFDict),
ExtGState: Resources.lookup(PDFName.ExtGState, PDFDict),
};
}
}
export default PDFPageLeaf;

View file

@ -0,0 +1,195 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict, { DictMap } from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
import { InvalidTargetIndexError, CorruptPageTreeError } from 'src/core/errors';
export type TreeNode = PDFPageTree | PDFPageLeaf;
class PDFPageTree extends PDFDict {
static withContext = (context: PDFContext, parent?: PDFRef) => {
const dict = new Map();
dict.set(PDFName.of('Type'), PDFName.of('Pages'));
dict.set(PDFName.of('Kids'), context.obj([]));
dict.set(PDFName.of('Count'), context.obj(0));
if (parent) dict.set(PDFName.of('Parent'), parent);
return new PDFPageTree(dict, context);
};
static fromMapWithContext = (map: DictMap, context: PDFContext) =>
new PDFPageTree(map, context);
Parent(): PDFPageTree | undefined {
return this.lookup(PDFName.of('Parent')) as PDFPageTree | undefined;
}
Kids(): PDFArray {
return this.lookup(PDFName.of('Kids'), PDFArray);
}
Count(): PDFNumber {
return this.lookup(PDFName.of('Count'), PDFNumber);
}
pushTreeNode(treeRef: PDFRef): void {
const Kids = this.Kids();
Kids.push(treeRef);
}
pushLeafNode(leafRef: PDFRef): void {
const Kids = this.Kids();
this.insertLeafKid(Kids.size(), leafRef);
}
/**
* Inserts the given ref as a leaf node of this page tree at the specified
* index (zero-based). Also increments the `Count` of each page tree in the
* hierarchy to accomodate the new page.
*
* Returns the ref of the PDFPageTree node into which `leafRef` was inserted,
* or `undefined` if it was inserted into the root node (the PDFPageTree upon
* which the method was first called).
*/
insertLeafNode(leafRef: PDFRef, targetIndex: number): PDFRef | undefined {
const Kids = this.Kids();
const Count = this.Count().asNumber();
if (targetIndex > Count) {
throw new InvalidTargetIndexError(targetIndex, Count);
}
let leafsRemainingUntilTarget = targetIndex;
for (let idx = 0, len = Kids.size(); idx < len; idx++) {
if (leafsRemainingUntilTarget === 0) {
// Insert page and return
this.insertLeafKid(idx, leafRef);
return undefined;
}
const kidRef = Kids.get(idx) as PDFRef;
const kid = this.context.lookup(kidRef);
if (kid instanceof PDFPageTree) {
if (kid.Count().asNumber() > leafsRemainingUntilTarget) {
// Dig in
return (
kid.insertLeafNode(leafRef, leafsRemainingUntilTarget) || kidRef
);
} else {
// Move on
leafsRemainingUntilTarget -= kid.Count().asNumber();
}
}
if (kid instanceof PDFPageLeaf) {
// Move on
leafsRemainingUntilTarget -= 1;
}
}
if (leafsRemainingUntilTarget === 0) {
// Insert page at the end and return
this.insertLeafKid(Kids.size(), leafRef);
return undefined;
}
// Should never get here if `targetIndex` is valid
throw new CorruptPageTreeError(targetIndex, 'insertLeafNode');
}
/**
* Removes the leaf node at the specified index (zero-based) from this page
* tree. Also decrements the `Count` of each page tree in the hierarchy to
* account for the removed page.
*
* If `prune` is true, then intermediate tree nodes will be removed from the
* tree if they contain 0 children after the leaf node is removed.
*/
removeLeafNode(targetIndex: number, prune = true): void {
const Kids = this.Kids();
const Count = this.Count().asNumber();
if (targetIndex >= Count) {
throw new InvalidTargetIndexError(targetIndex, Count);
}
let leafsRemainingUntilTarget = targetIndex;
for (let idx = 0, len = Kids.size(); idx < len; idx++) {
const kidRef = Kids.get(idx) as PDFRef;
const kid = this.context.lookup(kidRef);
if (kid instanceof PDFPageTree) {
if (kid.Count().asNumber() > leafsRemainingUntilTarget) {
// Dig in
kid.removeLeafNode(leafsRemainingUntilTarget, prune);
if (prune && kid.Kids().size() === 0) Kids.remove(idx);
return;
} else {
// Move on
leafsRemainingUntilTarget -= kid.Count().asNumber();
}
}
if (kid instanceof PDFPageLeaf) {
if (leafsRemainingUntilTarget === 0) {
// Remove page and return
this.removeKid(idx);
return;
} else {
// Move on
leafsRemainingUntilTarget -= 1;
}
}
}
// Should never get here if `targetIndex` is valid
throw new CorruptPageTreeError(targetIndex, 'removeLeafNode');
}
ascend(visitor: (node: PDFPageTree) => any): void {
visitor(this);
const Parent = this.Parent();
if (Parent) Parent.ascend(visitor);
}
/** Performs a Post-Order traversal of this page tree */
traverse(visitor: (node: TreeNode, ref: PDFRef) => any): void {
const Kids = this.Kids();
for (let idx = 0, len = Kids.size(); idx < len; idx++) {
const kidRef = Kids.get(idx) as PDFRef;
const kid = this.context.lookup(kidRef) as TreeNode;
if (kid instanceof PDFPageTree) kid.traverse(visitor);
visitor(kid, kidRef);
}
}
private insertLeafKid(kidIdx: number, leafRef: PDFRef): void {
const Kids = this.Kids();
this.ascend((node) => {
const newCount = node.Count().asNumber() + 1;
node.set(PDFName.of('Count'), PDFNumber.of(newCount));
});
Kids.insert(kidIdx, leafRef);
}
private removeKid(kidIdx: number): void {
const Kids = this.Kids();
const kid = Kids.lookup(kidIdx);
if (kid instanceof PDFPageLeaf) {
this.ascend((node) => {
const newCount = node.Count().asNumber() - 1;
node.set(PDFName.of('Count'), PDFNumber.of(newCount));
});
}
Kids.remove(kidIdx);
}
}
export default PDFPageTree;

View file

@ -0,0 +1,62 @@
enum CharCodes {
Null = 0,
Backspace = 8,
Tab = 9,
Newline = 10,
FormFeed = 12,
CarriageReturn = 13,
Space = 32,
ExclamationPoint = 33,
Hash = 35,
Percent = 37,
LeftParen = 40,
RightParen = 41,
Plus = 43,
Minus = 45,
Dash = 45,
Period = 46,
ForwardSlash = 47,
Zero = 48,
One = 49,
Two = 50,
Three = 51,
Four = 52,
Five = 53,
Six = 54,
Seven = 55,
Eight = 56,
Nine = 57,
LessThan = 60,
GreaterThan = 62,
A = 65,
D = 68,
E = 69,
F = 70,
O = 79,
P = 80,
R = 82,
LeftSquareBracket = 91,
BackSlash = 92,
RightSquareBracket = 93,
a = 97,
b = 98,
d = 100,
e = 101,
f = 102,
i = 105,
j = 106,
l = 108,
m = 109,
n = 110,
o = 111,
r = 114,
s = 115,
t = 116,
u = 117,
x = 120,
LeftCurly = 123,
RightCurly = 125,
Tilde = 126,
}
export default CharCodes;

View file

@ -0,0 +1,14 @@
import CharCodes from 'src/core/syntax/CharCodes';
export const IsDelimiter = new Uint8Array(256);
IsDelimiter[CharCodes.LeftParen] = 1;
IsDelimiter[CharCodes.RightParen] = 1;
IsDelimiter[CharCodes.LessThan] = 1;
IsDelimiter[CharCodes.GreaterThan] = 1;
IsDelimiter[CharCodes.LeftSquareBracket] = 1;
IsDelimiter[CharCodes.RightSquareBracket] = 1;
IsDelimiter[CharCodes.LeftCurly] = 1;
IsDelimiter[CharCodes.RightCurly] = 1;
IsDelimiter[CharCodes.ForwardSlash] = 1;
IsDelimiter[CharCodes.Percent] = 1;

View file

@ -0,0 +1,10 @@
import CharCodes from 'src/core/syntax/CharCodes';
import { IsDelimiter } from 'src/core/syntax/Delimiters';
import { IsWhitespace } from 'src/core/syntax/Whitespace';
export const IsIrregular = new Uint8Array(256);
for (let idx = 0, len = 256; idx < len; idx++) {
IsIrregular[idx] = IsWhitespace[idx] || IsDelimiter[idx] ? 1 : 0;
}
IsIrregular[CharCodes.Hash] = 1;

View file

@ -0,0 +1,83 @@
import CharCodes from 'src/core/syntax/CharCodes';
const { Space, CarriageReturn, Newline } = CharCodes;
const stream = [
CharCodes.s,
CharCodes.t,
CharCodes.r,
CharCodes.e,
CharCodes.a,
CharCodes.m,
];
const endstream = [
CharCodes.e,
CharCodes.n,
CharCodes.d,
CharCodes.s,
CharCodes.t,
CharCodes.r,
CharCodes.e,
CharCodes.a,
CharCodes.m,
];
export const Keywords = {
header: [
CharCodes.Percent,
CharCodes.P,
CharCodes.D,
CharCodes.F,
CharCodes.Dash,
],
eof: [
CharCodes.Percent,
CharCodes.Percent,
CharCodes.E,
CharCodes.O,
CharCodes.F,
],
obj: [CharCodes.o, CharCodes.b, CharCodes.j],
endobj: [
CharCodes.e,
CharCodes.n,
CharCodes.d,
CharCodes.o,
CharCodes.b,
CharCodes.j,
],
xref: [CharCodes.x, CharCodes.r, CharCodes.e, CharCodes.f],
trailer: [
CharCodes.t,
CharCodes.r,
CharCodes.a,
CharCodes.i,
CharCodes.l,
CharCodes.e,
CharCodes.r,
],
startxref: [
CharCodes.s,
CharCodes.t,
CharCodes.a,
CharCodes.r,
CharCodes.t,
CharCodes.x,
CharCodes.r,
CharCodes.e,
CharCodes.f,
],
true: [CharCodes.t, CharCodes.r, CharCodes.u, CharCodes.e],
false: [CharCodes.f, CharCodes.a, CharCodes.l, CharCodes.s, CharCodes.e],
null: [CharCodes.n, CharCodes.u, CharCodes.l, CharCodes.l],
stream,
streamEOF1: [...stream, Space, CarriageReturn, Newline],
streamEOF2: [...stream, CarriageReturn, Newline],
streamEOF3: [...stream, CarriageReturn],
streamEOF4: [...stream, Newline],
endstream,
EOF1endstream: [CarriageReturn, Newline, ...endstream],
EOF2endstream: [CarriageReturn, ...endstream],
EOF3endstream: [Newline, ...endstream],
};

View file

@ -0,0 +1,26 @@
import CharCodes from 'src/core/syntax/CharCodes';
export const IsDigit = new Uint8Array(256);
IsDigit[CharCodes.Zero] = 1;
IsDigit[CharCodes.One] = 1;
IsDigit[CharCodes.Two] = 1;
IsDigit[CharCodes.Three] = 1;
IsDigit[CharCodes.Four] = 1;
IsDigit[CharCodes.Five] = 1;
IsDigit[CharCodes.Six] = 1;
IsDigit[CharCodes.Seven] = 1;
IsDigit[CharCodes.Eight] = 1;
IsDigit[CharCodes.Nine] = 1;
export const IsNumericPrefix = new Uint8Array(256);
IsNumericPrefix[CharCodes.Period] = 1;
IsNumericPrefix[CharCodes.Plus] = 1;
IsNumericPrefix[CharCodes.Minus] = 1;
export const IsNumeric = new Uint8Array(256);
for (let idx = 0, len = 256; idx < len; idx++) {
IsNumeric[idx] = IsDigit[idx] || IsNumericPrefix[idx] ? 1 : 0;
}

View file

@ -0,0 +1,10 @@
import CharCodes from 'src/core/syntax/CharCodes';
export const IsWhitespace = new Uint8Array(256);
IsWhitespace[CharCodes.Null] = 1;
IsWhitespace[CharCodes.Tab] = 1;
IsWhitespace[CharCodes.Newline] = 1;
IsWhitespace[CharCodes.FormFeed] = 1;
IsWhitespace[CharCodes.CarriageReturn] = 1;
IsWhitespace[CharCodes.Space] = 1;

View file

@ -0,0 +1,123 @@
import PDFHeader from 'src/core/document/PDFHeader';
import PDFTrailer from 'src/core/document/PDFTrailer';
import PDFInvalidObject from 'src/core/objects/PDFInvalidObject';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import PDFCrossRefStream from 'src/core/structures/PDFCrossRefStream';
import PDFObjectStream from 'src/core/structures/PDFObjectStream';
import PDFWriter from 'src/core/writers/PDFWriter';
import { last, waitForTick } from 'src/utils';
class PDFStreamWriter extends PDFWriter {
static forContext = (
context: PDFContext,
objectsPerTick: number,
encodeStreams = true,
objectsPerStream = 50,
) =>
new PDFStreamWriter(
context,
objectsPerTick,
encodeStreams,
objectsPerStream,
);
private readonly encodeStreams: boolean;
private readonly objectsPerStream: number;
private constructor(
context: PDFContext,
objectsPerTick: number,
encodeStreams: boolean,
objectsPerStream: number,
) {
super(context, objectsPerTick);
this.encodeStreams = encodeStreams;
this.objectsPerStream = objectsPerStream;
}
protected async computeBufferSize() {
let objectNumber = this.context.largestObjectNumber + 1;
const header = PDFHeader.forVersion(1, 7);
let size = header.sizeInBytes() + 2;
const xrefStream = PDFCrossRefStream.create(
this.createTrailerDict(),
this.encodeStreams,
);
const uncompressedObjects: [PDFRef, PDFObject][] = [];
const compressedObjects: [PDFRef, PDFObject][][] = [];
const objectStreamRefs: PDFRef[] = [];
const indirectObjects = this.context.enumerateIndirectObjects();
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const indirectObject = indirectObjects[idx];
const [ref, object] = indirectObject;
const shouldNotCompress =
ref === this.context.trailerInfo.Encrypt ||
object instanceof PDFStream ||
object instanceof PDFInvalidObject ||
ref.generationNumber !== 0;
if (shouldNotCompress) {
uncompressedObjects.push(indirectObject);
xrefStream.addUncompressedEntry(ref, size);
size += this.computeIndirectObjectSize(indirectObject);
if (this.shouldWaitForTick(1)) await waitForTick();
} else {
let chunk = last(compressedObjects);
let objectStreamRef = last(objectStreamRefs);
if (!chunk || chunk.length % this.objectsPerStream === 0) {
chunk = [];
compressedObjects.push(chunk);
objectStreamRef = PDFRef.of(objectNumber++);
objectStreamRefs.push(objectStreamRef);
}
xrefStream.addCompressedEntry(ref, objectStreamRef, chunk.length);
chunk.push(indirectObject);
}
}
for (let idx = 0, len = compressedObjects.length; idx < len; idx++) {
const chunk = compressedObjects[idx];
const ref = objectStreamRefs[idx];
const objectStream = PDFObjectStream.withContextAndObjects(
this.context,
chunk,
this.encodeStreams,
);
xrefStream.addUncompressedEntry(ref, size);
size += this.computeIndirectObjectSize([ref, objectStream]);
uncompressedObjects.push([ref, objectStream]);
if (this.shouldWaitForTick(chunk.length)) await waitForTick();
}
const xrefStreamRef = PDFRef.of(objectNumber++);
xrefStream.dict.set(PDFName.of('Size'), PDFNumber.of(objectNumber));
xrefStream.addUncompressedEntry(xrefStreamRef, size);
const xrefOffset = size;
size += this.computeIndirectObjectSize([xrefStreamRef, xrefStream]);
uncompressedObjects.push([xrefStreamRef, xrefStream]);
const trailer = PDFTrailer.forLastCrossRefSectionOffset(xrefOffset);
size += trailer.sizeInBytes();
return { size, header, indirectObjects: uncompressedObjects, trailer };
}
}
export default PDFStreamWriter;

View file

@ -0,0 +1,156 @@
import PDFCrossRefSection from 'src/core/document/PDFCrossRefSection';
import PDFHeader from 'src/core/document/PDFHeader';
import PDFTrailer from 'src/core/document/PDFTrailer';
import PDFTrailerDict from 'src/core/document/PDFTrailerDict';
import PDFDict from 'src/core/objects/PDFDict';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFObjectStream from 'src/core/structures/PDFObjectStream';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer, waitForTick } from 'src/utils';
export interface SerializationInfo {
size: number;
header: PDFHeader;
indirectObjects: [PDFRef, PDFObject][];
xref?: PDFCrossRefSection;
trailerDict?: PDFTrailerDict;
trailer: PDFTrailer;
}
class PDFWriter {
static forContext = (context: PDFContext, objectsPerTick: number) =>
new PDFWriter(context, objectsPerTick);
protected readonly context: PDFContext;
protected readonly objectsPerTick: number;
private parsedObjects = 0;
protected constructor(context: PDFContext, objectsPerTick: number) {
this.context = context;
this.objectsPerTick = objectsPerTick;
}
async serializeToBuffer(): Promise<Uint8Array> {
const {
size,
header,
indirectObjects,
xref,
trailerDict,
trailer,
} = await this.computeBufferSize();
let offset = 0;
const buffer = new Uint8Array(size);
offset += header.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Newline;
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const [ref, object] = indirectObjects[idx];
const objectNumber = String(ref.objectNumber);
offset += copyStringIntoBuffer(objectNumber, buffer, offset);
buffer[offset++] = CharCodes.Space;
const generationNumber = String(ref.generationNumber);
offset += copyStringIntoBuffer(generationNumber, buffer, offset);
buffer[offset++] = CharCodes.Space;
buffer[offset++] = CharCodes.o;
buffer[offset++] = CharCodes.b;
buffer[offset++] = CharCodes.j;
buffer[offset++] = CharCodes.Newline;
offset += object.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.n;
buffer[offset++] = CharCodes.d;
buffer[offset++] = CharCodes.o;
buffer[offset++] = CharCodes.b;
buffer[offset++] = CharCodes.j;
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Newline;
const n =
object instanceof PDFObjectStream ? object.getObjectsCount() : 1;
if (this.shouldWaitForTick(n)) await waitForTick();
}
if (xref) {
offset += xref.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
}
if (trailerDict) {
offset += trailerDict.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Newline;
}
offset += trailer.copyBytesInto(buffer, offset);
return buffer;
}
protected computeIndirectObjectSize([ref, object]: [
PDFRef,
PDFObject,
]): number {
const refSize = ref.sizeInBytes() + 3; // 'R' -> 'obj\n'
const objectSize = object.sizeInBytes() + 9; // '\nendobj\n\n'
return refSize + objectSize;
}
protected createTrailerDict(): PDFDict {
return this.context.obj({
Size: this.context.largestObjectNumber + 1,
Root: this.context.trailerInfo.Root,
Encrypt: this.context.trailerInfo.Encrypt,
Info: this.context.trailerInfo.Info,
ID: this.context.trailerInfo.ID,
});
}
protected async computeBufferSize(): Promise<SerializationInfo> {
const header = PDFHeader.forVersion(1, 7);
let size = header.sizeInBytes() + 2;
const xref = PDFCrossRefSection.create();
const indirectObjects = this.context.enumerateIndirectObjects();
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const indirectObject = indirectObjects[idx];
const [ref] = indirectObject;
xref.addEntry(ref, size);
size += this.computeIndirectObjectSize(indirectObject);
if (this.shouldWaitForTick(1)) await waitForTick();
}
const xrefOffset = size;
size += xref.sizeInBytes() + 1; // '\n'
const trailerDict = PDFTrailerDict.of(this.createTrailerDict());
size += trailerDict.sizeInBytes() + 2; // '\n\n'
const trailer = PDFTrailer.forLastCrossRefSectionOffset(xrefOffset);
size += trailer.sizeInBytes();
return { size, header, indirectObjects, xref, trailerDict, trailer };
}
protected shouldWaitForTick = (n: number) => {
this.parsedObjects += n;
return this.parsedObjects % this.objectsPerTick === 0;
};
}
export default PDFWriter;