You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
12 KiB
470 lines
12 KiB
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
|
|
|
'use strict'; |
|
|
|
function log(msg) { |
|
if (console && console.log) |
|
console.log(msg); |
|
else if (print) |
|
print(msg); |
|
} |
|
|
|
function warn(msg) { |
|
if (verbosity >= WARNINGS) |
|
log('Warning: ' + msg); |
|
} |
|
|
|
function backtrace() { |
|
try { |
|
throw new Error(); |
|
} catch (e) { |
|
return e.stack ? e.stack.split('\n').slice(2).join('\n') : ''; |
|
} |
|
} |
|
|
|
function error(msg) { |
|
log('Error: ' + msg); |
|
log(backtrace()); |
|
throw new Error(msg); |
|
} |
|
|
|
function TODO(what) { |
|
if (verbosity >= TODOS) |
|
log('TODO: ' + what); |
|
} |
|
|
|
function malformed(msg) { |
|
error('Malformed PDF: ' + msg); |
|
} |
|
|
|
function assert(cond, msg) { |
|
if (!cond) |
|
error(msg); |
|
} |
|
|
|
// In a well-formed PDF, |cond| holds. If it doesn't, subsequent |
|
// behavior is undefined. |
|
function assertWellFormed(cond, msg) { |
|
if (!cond) |
|
malformed(msg); |
|
} |
|
|
|
function shadow(obj, prop, value) { |
|
Object.defineProperty(obj, prop, { value: value, |
|
enumerable: true, |
|
configurable: true, |
|
writable: false }); |
|
return value; |
|
} |
|
|
|
function bytesToString(bytes) { |
|
var str = ''; |
|
var length = bytes.length; |
|
for (var n = 0; n < length; ++n) |
|
str += String.fromCharCode(bytes[n]); |
|
return str; |
|
} |
|
|
|
function stringToBytes(str) { |
|
var length = str.length; |
|
var bytes = new Uint8Array(length); |
|
for (var n = 0; n < length; ++n) |
|
bytes[n] = str.charCodeAt(n) & 0xFF; |
|
return bytes; |
|
} |
|
|
|
var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; |
|
|
|
var Util = (function UtilClosure() { |
|
function Util() {} |
|
|
|
Util.makeCssRgb = function makergb(r, g, b) { |
|
var ri = (255 * r) | 0, gi = (255 * g) | 0, bi = (255 * b) | 0; |
|
return 'rgb(' + ri + ',' + gi + ',' + bi + ')'; |
|
}; |
|
|
|
Util.makeCssCmyk = function makecmyk(c, m, y, k) { |
|
c = (new DeviceCmykCS()).getRgb([c, m, y, k]); |
|
var ri = (255 * c[0]) | 0, gi = (255 * c[1]) | 0, bi = (255 * c[2]) | 0; |
|
return 'rgb(' + ri + ',' + gi + ',' + bi + ')'; |
|
}; |
|
|
|
// For 2d affine transforms |
|
Util.applyTransform = function apply(p, m) { |
|
var xt = p[0] * m[0] + p[1] * m[2] + m[4]; |
|
var yt = p[0] * m[1] + p[1] * m[3] + m[5]; |
|
return [xt, yt]; |
|
}; |
|
|
|
// Apply a generic 3d matrix M on a 3-vector v: |
|
// | a b c | | X | |
|
// | d e f | x | Y | |
|
// | g h i | | Z | |
|
// M is assumed to be serialized as [a,b,c,d,e,f,g,h,i], |
|
// with v as [X,Y,Z] |
|
Util.apply3dTransform = function apply3d(m, v) { |
|
return [ |
|
m[0] * v[0] + m[1] * v[1] + m[2] * v[2], |
|
m[3] * v[0] + m[4] * v[1] + m[5] * v[2], |
|
m[6] * v[0] + m[7] * v[1] + m[8] * v[2] |
|
]; |
|
} |
|
|
|
// Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2) |
|
// For coordinate systems whose origin lies in the bottom-left, this |
|
// means normalization to (BL,TR) ordering. For systems with origin in the |
|
// top-left, this means (TL,BR) ordering. |
|
Util.normalizeRect = function normalizeRect(rect) { |
|
var r = rect.slice(0); // clone rect |
|
if (rect[0] > rect[2]) { |
|
r[0] = rect[2]; |
|
r[2] = rect[0]; |
|
} |
|
if (rect[1] > rect[3]) { |
|
r[1] = rect[3]; |
|
r[3] = rect[1]; |
|
} |
|
return r; |
|
} |
|
|
|
// Returns a rectangle [x1, y1, x2, y2] corresponding to the |
|
// intersection of rect1 and rect2. If no intersection, returns 'false' |
|
// The rectangle coordinates of rect1, rect2 should be [x1, y1, x2, y2] |
|
Util.intersect = function intersect(rect1, rect2) { |
|
function compare(a, b) { |
|
return a - b; |
|
}; |
|
|
|
// Order points along the axes |
|
var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare), |
|
orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare), |
|
result = []; |
|
|
|
rect1 = Util.normalizeRect(rect1); |
|
rect2 = Util.normalizeRect(rect2); |
|
|
|
// X: first and second points belong to different rectangles? |
|
if ((orderedX[0] === rect1[0] && orderedX[1] === rect2[0]) || |
|
(orderedX[0] === rect2[0] && orderedX[1] === rect1[0])) { |
|
// Intersection must be between second and third points |
|
result[0] = orderedX[1]; |
|
result[2] = orderedX[2]; |
|
} else { |
|
return false; |
|
} |
|
|
|
// Y: first and second points belong to different rectangles? |
|
if ((orderedY[0] === rect1[1] && orderedY[1] === rect2[1]) || |
|
(orderedY[0] === rect2[1] && orderedY[1] === rect1[1])) { |
|
// Intersection must be between second and third points |
|
result[1] = orderedY[1]; |
|
result[3] = orderedY[2]; |
|
} else { |
|
return false; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
Util.sign = function sign(num) { |
|
return num < 0 ? -1 : 1; |
|
}; |
|
|
|
return Util; |
|
})(); |
|
|
|
var PDFStringTranslateTable = [ |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, |
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, |
|
0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, |
|
0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160, |
|
0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC |
|
]; |
|
|
|
function stringToPDFString(str) { |
|
var i, n = str.length, str2 = ''; |
|
if (str[0] === '\xFE' && str[1] === '\xFF') { |
|
// UTF16BE BOM |
|
for (i = 2; i < n; i += 2) |
|
str2 += String.fromCharCode( |
|
(str.charCodeAt(i) << 8) | str.charCodeAt(i + 1)); |
|
} else { |
|
for (i = 0; i < n; ++i) { |
|
var code = PDFStringTranslateTable[str.charCodeAt(i)]; |
|
str2 += code ? String.fromCharCode(code) : str.charAt(i); |
|
} |
|
} |
|
return str2; |
|
} |
|
|
|
function isBool(v) { |
|
return typeof v == 'boolean'; |
|
} |
|
|
|
function isInt(v) { |
|
return typeof v == 'number' && ((v | 0) == v); |
|
} |
|
|
|
function isNum(v) { |
|
return typeof v == 'number'; |
|
} |
|
|
|
function isString(v) { |
|
return typeof v == 'string'; |
|
} |
|
|
|
function isNull(v) { |
|
return v === null; |
|
} |
|
|
|
function isName(v) { |
|
return v instanceof Name; |
|
} |
|
|
|
function isCmd(v, cmd) { |
|
return v instanceof Cmd && (!cmd || v.cmd == cmd); |
|
} |
|
|
|
function isDict(v, type) { |
|
return v instanceof Dict && (!type || v.get('Type').name == type); |
|
} |
|
|
|
function isArray(v) { |
|
return v instanceof Array; |
|
} |
|
|
|
function isStream(v) { |
|
return typeof v == 'object' && v != null && ('getChar' in v); |
|
} |
|
|
|
function isArrayBuffer(v) { |
|
return typeof v == 'object' && v != null && ('byteLength' in v); |
|
} |
|
|
|
function isRef(v) { |
|
return v instanceof Ref; |
|
} |
|
|
|
function isPDFFunction(v) { |
|
var fnDict; |
|
if (typeof v != 'object') |
|
return false; |
|
else if (isDict(v)) |
|
fnDict = v; |
|
else if (isStream(v)) |
|
fnDict = v.dict; |
|
else |
|
return false; |
|
return fnDict.has('FunctionType'); |
|
} |
|
|
|
/** |
|
* 'Promise' object. |
|
* Each object that is stored in PDFObjects is based on a Promise object that |
|
* contains the status of the object and the data. There migth be situations, |
|
* where a function want to use the value of an object, but it isn't ready at |
|
* that time. To get a notification, once the object is ready to be used, s.o. |
|
* can add a callback using the `then` method on the promise that then calls |
|
* the callback once the object gets resolved. |
|
* A promise can get resolved only once and only once the data of the promise |
|
* can be set. If any of these happens twice or the data is required before |
|
* it was set, an exception is throw. |
|
*/ |
|
var Promise = (function PromiseClosure() { |
|
var EMPTY_PROMISE = {}; |
|
|
|
/** |
|
* If `data` is passed in this constructor, the promise is created resolved. |
|
* If there isn't data, it isn't resolved at the beginning. |
|
*/ |
|
function Promise(name, data) { |
|
this.name = name; |
|
this.isRejected = false; |
|
this.error = null; |
|
// If you build a promise and pass in some data it's already resolved. |
|
if (data != null) { |
|
this.isResolved = true; |
|
this._data = data; |
|
this.hasData = true; |
|
} else { |
|
this.isResolved = false; |
|
this._data = EMPTY_PROMISE; |
|
} |
|
this.callbacks = []; |
|
this.errbacks = []; |
|
}; |
|
/** |
|
* Builds a promise that is resolved when all the passed in promises are |
|
* resolved. |
|
* @param {Promise[]} promises Array of promises to wait for. |
|
* @return {Promise} New dependant promise. |
|
*/ |
|
Promise.all = function(promises) { |
|
var deferred = new Promise(); |
|
var unresolved = promises.length; |
|
var results = []; |
|
if (unresolved === 0) { |
|
deferred.resolve(results); |
|
return deferred; |
|
} |
|
for (var i = 0; i < unresolved; ++i) { |
|
var promise = promises[i]; |
|
promise.then((function(i) { |
|
return function(value) { |
|
results[i] = value; |
|
unresolved--; |
|
if (unresolved === 0) |
|
deferred.resolve(results); |
|
}; |
|
})(i)); |
|
} |
|
return deferred; |
|
}; |
|
Promise.prototype = { |
|
hasData: false, |
|
|
|
set data(value) { |
|
if (value === undefined) { |
|
return; |
|
} |
|
if (this._data !== EMPTY_PROMISE) { |
|
error('Promise ' + this.name + |
|
': Cannot set the data of a promise twice'); |
|
} |
|
this._data = value; |
|
this.hasData = true; |
|
|
|
if (this.onDataCallback) { |
|
this.onDataCallback(value); |
|
} |
|
}, |
|
|
|
get data() { |
|
if (this._data === EMPTY_PROMISE) { |
|
error('Promise ' + this.name + ': Cannot get data that isn\'t set'); |
|
} |
|
return this._data; |
|
}, |
|
|
|
onData: function promiseOnData(callback) { |
|
if (this._data !== EMPTY_PROMISE) { |
|
callback(this._data); |
|
} else { |
|
this.onDataCallback = callback; |
|
} |
|
}, |
|
|
|
resolve: function promiseResolve(data) { |
|
if (this.isResolved) { |
|
error('A Promise can be resolved only once ' + this.name); |
|
} |
|
if (this.isRejected) { |
|
error('The Promise was already rejected ' + this.name); |
|
} |
|
|
|
this.isResolved = true; |
|
this.data = data || null; |
|
var callbacks = this.callbacks; |
|
|
|
for (var i = 0, ii = callbacks.length; i < ii; i++) { |
|
callbacks[i].call(null, data); |
|
} |
|
}, |
|
|
|
reject: function proimseReject(reason) { |
|
if (this.isRejected) { |
|
error('A Promise can be rejected only once ' + this.name); |
|
} |
|
if (this.isResolved) { |
|
error('The Promise was already resolved ' + this.name); |
|
} |
|
|
|
this.isRejected = true; |
|
this.error = reason || null; |
|
var errbacks = this.errbacks; |
|
|
|
for (var i = 0, ii = errbacks.length; i < ii; i++) { |
|
errbacks[i].call(null, reason); |
|
} |
|
}, |
|
|
|
then: function promiseThen(callback, errback) { |
|
if (!callback) { |
|
error('Requiring callback' + this.name); |
|
} |
|
|
|
// If the promise is already resolved, call the callback directly. |
|
if (this.isResolved) { |
|
var data = this.data; |
|
callback.call(null, data); |
|
} else if (this.isRejected && errback) { |
|
var error = this.error; |
|
errback.call(null, error); |
|
} else { |
|
this.callbacks.push(callback); |
|
if (errback) |
|
this.errbacks.push(errback); |
|
} |
|
} |
|
}; |
|
|
|
return Promise; |
|
})(); |
|
|
|
var StatTimer = (function StatTimerClosure() { |
|
function rpad(str, pad, length) { |
|
while (str.length < length) |
|
str += pad; |
|
return str; |
|
} |
|
function StatTimer() { |
|
this.started = {}; |
|
this.times = []; |
|
this.enabled = true; |
|
} |
|
StatTimer.prototype = { |
|
time: function statTimerTime(name) { |
|
if (!this.enabled) |
|
return; |
|
if (name in this.started) |
|
throw 'Timer is already running for ' + name; |
|
this.started[name] = Date.now(); |
|
}, |
|
timeEnd: function statTimerTimeEnd(name) { |
|
if (!this.enabled) |
|
return; |
|
if (!(name in this.started)) |
|
throw 'Timer has not been started for ' + name; |
|
this.times.push({ |
|
'name': name, |
|
'start': this.started[name], |
|
'end': Date.now() |
|
}); |
|
// Remove timer from started so it can be called again. |
|
delete this.started[name]; |
|
}, |
|
toString: function statTimerToString() { |
|
var times = this.times; |
|
var out = ''; |
|
// Find the longest name for padding purposes. |
|
var longest = 0; |
|
for (var i = 0, ii = times.length; i < ii; ++i) { |
|
var name = times[i]['name']; |
|
if (name.length > longest) |
|
longest = name.length; |
|
} |
|
for (var i = 0, ii = times.length; i < ii; ++i) { |
|
var span = times[i]; |
|
var duration = span.end - span.start; |
|
out += rpad(span['name'], ' ', longest) + ' ' + duration + 'ms\n'; |
|
} |
|
return out; |
|
} |
|
}; |
|
return StatTimer; |
|
})();
|
|
|