diff --git a/Makefile b/Makefile index 0666dfe2c..62565670a 100644 --- a/Makefile +++ b/Makefile @@ -226,6 +226,7 @@ FIREFOX_CONTENT_DIR := $(EXTENSION_SRC)/firefox/$(CONTENT_DIR)/ FIREFOX_EXTENSION_FILES_TO_COPY = \ *.js \ *.rdf \ + *.png \ install.rdf.in \ README.mozilla \ components \ @@ -234,12 +235,16 @@ FIREFOX_EXTENSION_FILES_TO_COPY = \ FIREFOX_EXTENSION_FILES = \ bootstrap.js \ install.rdf \ + icon.png \ + icon64.png \ components \ content \ LICENSE \ $(NULL) FIREFOX_MC_EXTENSION_FILES = \ bootstrap.js \ + icon.png \ + icon64.png \ components \ content \ LICENSE \ diff --git a/extensions/firefox/bootstrap.js b/extensions/firefox/bootstrap.js index 627fc7a89..d03812bcb 100644 --- a/extensions/firefox/bootstrap.js +++ b/extensions/firefox/bootstrap.js @@ -10,10 +10,14 @@ let Cc = Components.classes; let Ci = Components.interfaces; let Cm = Components.manager; let Cu = Components.utils; +let application = Cc['@mozilla.org/fuel/application;1'] + .getService(Ci.fuelIApplication); Cu.import('resource://gre/modules/Services.jsm'); function log(str) { + if (!application.prefs.getValue(EXT_PREFIX + '.pdfBugEnabled', false)) + return; dump(str + '\n'); } @@ -60,12 +64,7 @@ function startup(aData, aReason) { var ioService = Services.io; var resProt = ioService.getProtocolHandler('resource') .QueryInterface(Ci.nsIResProtocolHandler); - var aliasFile = Cc['@mozilla.org/file/local;1'] - .createInstance(Ci.nsILocalFile); - var componentPath = aData.installPath.clone(); - componentPath.append('content'); - aliasFile.initWithPath(componentPath.path); - var aliasURI = ioService.newFileURI(aliasFile); + var aliasURI = ioService.newURI('content/', 'UTF-8', aData.resourceURI); resProt.setSubstitution(RESOURCE_NAME, aliasURI); // Load the component and register it. @@ -73,12 +72,9 @@ function startup(aData, aReason) { 'components/PdfStreamConverter.js'; Cu.import(pdfStreamConverterUrl); Factory.register(PdfStreamConverter); - Services.prefs.setBoolPref('extensions.pdf.js.active', true); } function shutdown(aData, aReason) { - if (Services.prefs.getBoolPref('extensions.pdf.js.active')) - Services.prefs.setBoolPref('extensions.pdf.js.active', false); if (aReason == APP_SHUTDOWN) return; var ioService = Services.io; @@ -89,18 +85,14 @@ function shutdown(aData, aReason) { // Remove the contract/component. Factory.unregister(); // Unload the converter - if (pdfStreamConverterUrl) { - Cu.unload(pdfStreamConverterUrl); - pdfStreamConverterUrl = null; - } + Cu.unload(pdfStreamConverterUrl); + pdfStreamConverterUrl = null; } function install(aData, aReason) { - Services.prefs.setBoolPref('extensions.pdf.js.active', false); } function uninstall(aData, aReason) { - Services.prefs.clearUserPref('extensions.pdf.js.active'); application.prefs.setValue(EXT_PREFIX + '.database', '{}'); } diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index 8aeb4a0c9..9375a2690 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -17,7 +17,15 @@ const MAX_DATABASE_LENGTH = 4096; Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/Services.jsm'); +let application = Cc['@mozilla.org/fuel/application;1'] + .getService(Ci.fuelIApplication); +let privateBrowsing = Cc['@mozilla.org/privatebrowsing;1'] + .getService(Ci.nsIPrivateBrowsingService); +let inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled; + function log(aMsg) { + if (!application.prefs.getValue(EXT_PREFIX + '.pdfBugEnabled', false)) + return; let msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); Services.console.logStringMessage(msg); dump(msg + '\n'); @@ -40,11 +48,6 @@ function topWindow(win) { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); } -let application = Cc['@mozilla.org/fuel/application;1'] - .getService(Ci.fuelIApplication); -let privateBrowsing = Cc['@mozilla.org/privatebrowsing;1'] - .getService(Ci.nsIPrivateBrowsingService); -let inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled; // All the priviledged actions. function ChromeActions() { @@ -125,9 +128,6 @@ PdfStreamConverter.prototype = { // nsIStreamConverter::asyncConvertData asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { - if (!Services.prefs.getBoolPref('extensions.pdf.js.active')) - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - // Ignoring HTTP POST requests -- pdf.js has to repeat the request. var skipConversion = false; try { diff --git a/extensions/firefox/icon.png b/extensions/firefox/icon.png new file mode 100644 index 000000000..64763756e Binary files /dev/null and b/extensions/firefox/icon.png differ diff --git a/extensions/firefox/icon64.png b/extensions/firefox/icon64.png new file mode 100644 index 000000000..0131bbb5c Binary files /dev/null and b/extensions/firefox/icon64.png differ diff --git a/extensions/firefox/install.rdf b/extensions/firefox/install.rdf index 14938b669..0a0d813b2 100644 --- a/extensions/firefox/install.rdf +++ b/extensions/firefox/install.rdf @@ -5,9 +5,8 @@ uriloader@pdf.js - pdf.js + PDF Viewer PDFJSSCRIPT_VERSION - chrome://pdf.js/skin/logo.png {ec8030f7-c20a-464f-9b0e-13a3a9e97384} @@ -16,9 +15,8 @@ true - true - Mozilla Labs - pdf.js uri loader + Mozilla + Uses HTML5 to display PDF files directly in Firefox. https://github.com/mozilla/pdf.js/ 2 diff --git a/extensions/firefox/install.rdf.in b/extensions/firefox/install.rdf.in index dc2893c69..3f8f87cda 100644 --- a/extensions/firefox/install.rdf.in +++ b/extensions/firefox/install.rdf.in @@ -7,22 +7,20 @@ uriloader@pdf.js - pdf.js + PDF Viewer PDFJSSCRIPT_VERSION - chrome://pdf.js/skin/logo.png {ec8030f7-c20a-464f-9b0e-13a3a9e97384} @FIREFOX_VERSION@ @FIREFOX_VERSION@ - true + true true - true - Mozilla Labs - pdf.js uri loader - https://github.com/mozilla/pdf.js/ + Mozilla + Uses HTML5 to display PDF files directly in Firefox. + http://support.mozilla.org/kb/using-mozilla-pdf-viewer 2 diff --git a/make.js b/make.js index f2e499f8a..33771aeb7 100755 --- a/make.js +++ b/make.js @@ -220,6 +220,7 @@ target.firefox = function() { FIREFOX_EXTENSION_FILES_TO_COPY = ['*.js', '*.rdf', + '*.png', 'install.rdf.in', 'README.mozilla', 'components', @@ -227,11 +228,15 @@ target.firefox = function() { FIREFOX_EXTENSION_FILES = ['bootstrap.js', 'install.rdf', + 'icon.png', + 'icon64.png', 'components', 'content', 'LICENSE']; FIREFOX_MC_EXTENSION_FILES = ['bootstrap.js', + 'icon.png', + 'icon64.png', 'components', 'content', 'LICENSE']; diff --git a/src/bidi.js b/src/bidi.js index b03e66ce4..aab477dbc 100644 --- a/src/bidi.js +++ b/src/bidi.js @@ -124,7 +124,7 @@ var bidi = PDFJS.bidi = (function bidiClosure() { } } - return (function bidi(text, startLevel) { + function bidi(text, startLevel) { var str = text.str; var strLength = str.length; if (strLength == 0) @@ -429,5 +429,8 @@ var bidi = PDFJS.bidi = (function bidiClosure() { result += ch; } return result; - }); + } + + return bidi; })(); + diff --git a/src/canvas.js b/src/canvas.js index 54ab5b4d0..7bf94a642 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -70,7 +70,7 @@ var CanvasExtraState = (function CanvasExtraStateClosure() { return CanvasExtraState; })(); -function ScratchCanvas(width, height) { +function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -188,9 +188,9 @@ function addContextCurrentTransform(ctx) { } var CanvasGraphics = (function CanvasGraphicsClosure() { - // Defines the time the executeIRQueue is going to be executing + // Defines the time the executeOperatorList is going to be executing // before it stops and shedules a continue of execution. - var kExecutionTime = 50; + var kExecutionTime = 15; function CanvasGraphics(canvasCtx, objs, textLayer) { this.ctx = canvasCtx; @@ -199,7 +199,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.pendingClip = null; this.res = null; this.xobjs = null; - this.ScratchCanvas = ScratchCanvas; this.objs = objs; this.textLayer = textLayer; if (canvasCtx) { @@ -229,7 +228,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { 'setStrokeColor': true, 'setStrokeColorN': true, 'setFillColor': true, - 'setFillColorN_IR': true, + 'setFillColorN': true, 'setStrokeGray': true, 'setFillGray': true, 'setStrokeRGBColor': true, @@ -268,15 +267,16 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.textLayer.beginLayout(); }, - executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, - executionStartIdx, continueCallback, - stepper) { - var argsArray = codeIR.argsArray; - var fnArray = codeIR.fnArray; + executeOperatorList: function canvasGraphicsExecuteOperatorList( + operatorList, + executionStartIdx, continueCallback, + stepper) { + var argsArray = operatorList.argsArray; + var fnArray = operatorList.fnArray; var i = executionStartIdx || 0; var argsArrayLen = argsArray.length; - // Sometimes the IRQueue to execute is empty. + // Sometimes the OperatorList to execute is empty. if (argsArrayLen == i) { return i; } @@ -314,7 +314,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { i++; - // If the entire IRQueue was executed, stop as were done. + // If the entire operatorList was executed, stop as were done. if (i == argsArrayLen) { return i; } @@ -327,8 +327,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { return i; } - // If the IRQueue isn't executed completly yet OR the execution time - // was short enough, do another execution round. + // If the operatorList isn't executed completely yet OR the execution + // time was short enough, do another execution round. } }, @@ -556,7 +556,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.current.leading = -leading; }, setFont: function canvasGraphicsSetFont(fontRefName, size) { - var fontObj = this.objs.get(fontRefName).fontObj; + var fontObj = this.objs.get(fontRefName); var current = this.current; if (!fontObj) @@ -707,7 +707,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.save(); ctx.scale(fontSize, fontSize); ctx.transform.apply(ctx, fontMatrix); - this.executeIRQueue(glyph.codeIRQueue); + this.executeOperatorList(glyph.operatorList); this.restore(); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); @@ -908,7 +908,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.ctx.strokeStyle = color; this.current.strokeColor = color; }, - getColorN_IR_Pattern: function canvasGraphicsGetColorN_IR_Pattern(IR, cs) { + getColorN_Pattern: function canvasGraphicsGetColorN_Pattern(IR, cs) { if (IR[0] == 'TilingPattern') { var args = IR[1]; var base = cs.base; @@ -930,11 +930,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } return pattern; }, - setStrokeColorN_IR: function canvasGraphicsSetStrokeColorN(/*...*/) { + setStrokeColorN: function canvasGraphicsSetStrokeColorN(/*...*/) { var cs = this.current.strokeColorSpace; if (cs.name == 'Pattern') { - this.current.strokeColor = this.getColorN_IR_Pattern(arguments, cs); + this.current.strokeColor = this.getColorN_Pattern(arguments, cs); } else { this.setStrokeColor.apply(this, arguments); } @@ -946,11 +946,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.ctx.fillStyle = color; this.current.fillColor = color; }, - setFillColorN_IR: function canvasGraphicsSetFillColorN(/*...*/) { + setFillColorN: function canvasGraphicsSetFillColorN(/*...*/) { var cs = this.current.fillColorSpace; if (cs.name == 'Pattern') { - this.current.fillColor = this.getColorN_IR_Pattern(arguments, cs); + this.current.fillColor = this.getColorN_Pattern(arguments, cs); } else { this.setFillColor.apply(this, arguments); } @@ -1116,7 +1116,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { // scale the image to the unit square ctx.scale(1 / w, -1 / h); - var tmpCanvas = new this.ScratchCanvas(w, h); + var tmpCanvas = createScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext('2d'); var fillColor = this.current.fillColor; @@ -1147,7 +1147,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { // scale the image to the unit square ctx.scale(1 / w, -1 / h); - var tmpCanvas = new this.ScratchCanvas(w, h); + var tmpCanvas = createScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext('2d'); this.putBinaryImageData(tmpCtx, imgData, w, h); diff --git a/src/colorspace.js b/src/colorspace.js index 57bc9c846..d3d392361 100644 --- a/src/colorspace.js +++ b/src/colorspace.js @@ -135,6 +135,7 @@ var ColorSpace = (function ColorSpaceClosure() { basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res); return ['PatternCS', basePatternCS]; case 'Indexed': + case 'I': var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res); var hiVal = cs[2] + 1; var lookup = xref.fetchIfRef(cs[3]); diff --git a/src/core.js b/src/core.js index e425e9ab5..ecc2c94a5 100644 --- a/src/core.js +++ b/src/core.js @@ -170,10 +170,10 @@ var Page = (function PageClosure() { return shadow(this, 'rotate', rotate); }, - startRenderingFromIRQueue: function pageStartRenderingFromIRQueue( - IRQueue, fonts) { + startRenderingFromOperatorList: function pageStartRenderingFromOperatorList( + operatorList, fonts) { var self = this; - this.IRQueue = IRQueue; + this.operatorList = operatorList; var displayContinuation = function pageDisplayContinuation() { // Always defer call to display() to work around bug in @@ -184,15 +184,16 @@ var Page = (function PageClosure() { }; this.ensureFonts(fonts, - function pageStartRenderingFromIRQueueEnsureFonts() { - displayContinuation(); - }); + function pageStartRenderingFromOperatorListEnsureFonts() { + displayContinuation(); + } + ); }, - getIRQueue: function pageGetIRQueue(handler, dependency) { - if (this.IRQueue) { + getOperatorList: function pageGetOperatorList(handler, dependency) { + if (this.operatorList) { // content was compiled - return this.IRQueue; + return this.operatorList; } this.stats.time('Build IR Queue'); @@ -213,11 +214,10 @@ var Page = (function PageClosure() { var pe = this.pe = new PartialEvaluator( xref, handler, 'p' + this.pageNumber + '_'); - var IRQueue = {}; - this.IRQueue = pe.getIRQueue(content, resources, IRQueue, dependency); + this.operatorList = pe.getOperatorList(content, resources, dependency); this.stats.timeEnd('Build IR Queue'); - return this.IRQueue; + return this.operatorList; }, ensureFonts: function pageEnsureFonts(fonts, callback) { @@ -228,14 +228,13 @@ var Page = (function PageClosure() { } // Load all the fonts - var fontObjs = FontLoader.bind( + FontLoader.bind( fonts, function pageEnsureFontsFontObjs(fontObjs) { this.stats.timeEnd('Font Loading'); callback.call(this); - }.bind(this), - this.objs + }.bind(this) ); }, @@ -255,18 +254,19 @@ var Page = (function PageClosure() { rotate: this.rotate }); var startIdx = 0; - var length = this.IRQueue.fnArray.length; - var IRQueue = this.IRQueue; + var length = this.operatorList.fnArray.length; + var operatorList = this.operatorList; var stepper = null; if (PDFJS.pdfBug && StepperManager.enabled) { stepper = StepperManager.create(this.pageNumber); - stepper.init(IRQueue); + stepper.init(operatorList); stepper.nextBreakPoint = stepper.getNextBreakPoint(); } var self = this; function next() { - startIdx = gfx.executeIRQueue(IRQueue, startIdx, next, stepper); + startIdx = + gfx.executeOperatorList(operatorList, startIdx, next, stepper); if (startIdx == length) { gfx.endDrawing(); stats.timeEnd('Rendering'); @@ -436,13 +436,14 @@ var Page = (function PageClosure() { startRendering: function pageStartRendering(ctx, callback, textLayer) { var stats = this.stats; stats.time('Overall'); - // If there is no displayReadyPromise yet, then the IRQueue was never + // If there is no displayReadyPromise yet, then the operatorList was never // requested before. Make the request and create the promise. if (!this.displayReadyPromise) { this.pdf.startRendering(this); this.displayReadyPromise = new Promise(); } - // Once the IRQueue and fonts are loaded, perform the actual rendering. + + // Once the operatorList and fonts are loaded, do the actual rendering. this.displayReadyPromise.then( function pageDisplayReadyPromise() { var gfx = new CanvasGraphics(ctx, this.objs, textLayer); @@ -474,9 +475,6 @@ var Page = (function PageClosure() { * Right now there exists one PDFDocModel on the main thread + one object * for each worker. If there is no worker support enabled, there are two * `PDFDocModel` objects on the main thread created. - * TODO: Refactor the internal object structure, such that there is no - * need for the `PDFDocModel` anymore and there is only one object on the - * main thread and not one entire copy on each worker instance. */ var PDFDocModel = (function PDFDocModelClosure() { function PDFDocModel(arg, callback) { @@ -645,9 +643,9 @@ var PDFDoc = (function PDFDocClosure() { this.data = data; this.stream = stream; - this.pdf = new PDFDocModel(stream); - this.fingerprint = this.pdf.getFingerprint(); - this.catalog = this.pdf.catalog; + this.pdfModel = new PDFDocModel(stream); + this.fingerprint = this.pdfModel.getFingerprint(); + this.catalog = this.pdfModel.catalog; this.objs = new PDFObjects(); this.pageCache = []; @@ -733,8 +731,9 @@ var PDFDoc = (function PDFDocClosure() { var pageNum = data.pageNum; var page = this.pageCache[pageNum]; var depFonts = data.depFonts; + page.stats.timeEnd('Page Request'); - page.startRenderingFromIRQueue(data.IRQueue, depFonts); + page.startRenderingFromOperatorList(data.operatorList, depFonts); }, this); messageHandler.on('obj', function pdfDocObj(data) { @@ -761,31 +760,16 @@ var PDFDoc = (function PDFDocClosure() { file = new Stream(file, 0, file.length, fontFileDict); } - // For now, resolve the font object here direclty. The real font - // object is then created in FontLoader.bind(). - this.objs.resolve(id, { - name: name, - file: file, - properties: properties - }); + // At this point, only the font object is created but the font is + // not yet attached to the DOM. This is done in `FontLoader.bind`. + var font = new Font(name, file, properties); + this.objs.resolve(id, font); break; default: error('Got unkown object type ' + type); } }, this); - messageHandler.on('font_ready', function pdfDocFontReady(data) { - var id = data[0]; - var font = new FontShape(data[1]); - - // If there is no string, then there is nothing to attach to the DOM. - if (!font.str) { - this.objs.resolve(id, font); - } else { - this.objs.setData(id, font); - } - }.bind(this)); - messageHandler.on('page_error', function pdfDocError(data) { var page = this.pageCache[data.pageNum]; if (page.displayReadyPromise) @@ -807,7 +791,7 @@ var PDFDoc = (function PDFDocClosure() { var size = width * height; var rgbaLength = size * 4; var buf = new Uint8Array(size * components); - var tmpCanvas = new ScratchCanvas(width, height); + var tmpCanvas = createScratchCanvas(width, height); var tmpCtx = tmpCanvas.getContext('2d'); tmpCtx.drawImage(img, 0, 0); var data = tmpCtx.getImageData(0, 0, width, height).data; @@ -836,7 +820,7 @@ var PDFDoc = (function PDFDocClosure() { }, get numPages() { - return this.pdf.numPages; + return this.pdfModel.numPages; }, startRendering: function pdfDocStartRendering(page) { @@ -851,7 +835,7 @@ var PDFDoc = (function PDFDocClosure() { if (this.pageCache[n]) return this.pageCache[n]; - var page = this.pdf.getPage(n); + var page = this.pdfModel.getPage(n); // Add a reference to the objects such that Page can forward the reference // to the CanvasGraphics and so on. page.objs = this.objs; diff --git a/src/crypto.js b/src/crypto.js index 7c34a8506..b1a298223 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -570,7 +570,6 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { }; } error('Unknown crypto method'); - return null; } CipherTransformFactory.prototype = { diff --git a/src/evaluator.js b/src/evaluator.js index e34787e41..1d83c380b 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -112,8 +112,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }; PartialEvaluator.prototype = { - getIRQueue: function partialEvaluatorGetIRQueue(stream, resources, - queue, dependency) { + getOperatorList: function partialEvaluatorGetOperatorList(stream, resources, + dependency, queue) { var self = this; var xref = this.xref; @@ -136,8 +136,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var fontRes = resources.get('Font'); - // TODO: TOASK: Is it possible to get here? If so, what does - // args[0].name should be like??? assert(fontRes, 'fontRes not available'); fontRes = xref.fetchIfRef(fontRes); @@ -177,7 +175,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // Ensure the font is ready before the font is set // and later on used for drawing. - // TODO: This should get insert to the IRQueue only once per + // OPTIMIZE: This should get insert to the operatorList only once per // page. insertDependency([loadedName]); return loadedName; @@ -239,6 +237,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handler, xref, resources, image, inline); } + if (!queue) + queue = {}; + if (!queue.argsArray) { queue.argsArray = []; } @@ -280,9 +281,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // TODO figure out how to type-check vararg functions if ((cmd == 'SCN' || cmd == 'scn') && !args[args.length - 1].code) { - // Use the IR version for setStroke/FillColorN. - fn += '_IR'; - // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors @@ -295,15 +293,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (typeNum == TILING_PATTERN) { // Create an IR of the pattern code. var depIdx = dependencyArray.length; - var queueObj = {}; - var codeIR = this.getIRQueue(pattern, dict.get('Resources') || - resources, queueObj, dependencyArray); + var operatorList = this.getOperatorList(pattern, + dict.get('Resources') || resources, dependencyArray); // Add the dependencies that are required to execute the - // codeIR. + // operatorList. insertDependency(dependencyArray.slice(depIdx)); - args = TilingPattern.getIR(codeIR, dict, args); + args = TilingPattern.getIR(operatorList, dict, args); } else if (typeNum == SHADING_PATTERN) { var shading = xref.fetchIfRef(dict.get('Shading')); @@ -337,14 +334,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { fnArray.push('paintFormXObjectBegin'); argsArray.push([matrix, bbox]); - // This adds the IRQueue of the xObj to the current queue. + // This adds the operatorList of the xObj to the current queue. var depIdx = dependencyArray.length; - this.getIRQueue(xobj, xobj.dict.get('Resources') || resources, - queue, dependencyArray); + // Pass in the current `queue` object. That means the `fnArray` + // and the `argsArray` in this scope is reused and new commands + // are added to them. + this.getOperatorList(xobj, + xobj.dict.get('Resources') || resources, + dependencyArray, queue); // Add the dependencies that are required to execute the - // codeIR. + // operatorList. insertDependency(dependencyArray.slice(depIdx)); fn = 'paintFormXObjectEnd'; @@ -454,10 +455,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } } - return { - fnArray: fnArray, - argsArray: argsArray - }; + return queue; }, extractDataStructures: function @@ -855,12 +853,11 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var charProcs = xref.fetchIfRef(dict.get('CharProcs')); var fontResources = xref.fetchIfRef(dict.get('Resources')) || resources; properties.resources = fontResources; - properties.charProcIRQueues = {}; + properties.charProcOperatorList = {}; for (var key in charProcs.map) { var glyphStream = xref.fetchIfRef(charProcs.map[key]); - var queueObj = {}; - properties.charProcIRQueues[key] = - this.getIRQueue(glyphStream, fontResources, queueObj, dependency); + properties.charProcOperatorList[key] = + this.getOperatorList(glyphStream, fontResources, dependency); } } diff --git a/src/fonts.js b/src/fonts.js index af720516c..8473196a0 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -409,8 +409,8 @@ var FontLoader = { bind: function fontLoaderBind(fonts, callback) { function checkFontsLoaded() { - for (var i = 0, ii = objs.length; i < ii; i++) { - var fontObj = objs[i]; + for (var i = 0, ii = fonts.length; i < ii; i++) { + var fontObj = fonts[i]; if (fontObj.loading) { return false; } @@ -423,52 +423,45 @@ var FontLoader = { return true; } - var rules = [], names = [], objs = []; + var rules = [], names = [], fontsToLoad = []; + var fontCreateTimer = 0; for (var i = 0, ii = fonts.length; i < ii; i++) { var font = fonts[i]; - // If there is already a fontObj on the font, then it was loaded/attached - // to the page already and we don't have to do anything for this font - // here future. - if (font.fontObj) { + // Add the font to the DOM only once or skip if the font + // is already loaded. + if (font.attached || font.loading == false) { continue; } + font.attached = true; - var obj = new Font(font.name, font.file, font.properties); - - // Store the fontObj on the font such that `setFont` in CanvasGraphics - // can reuse it later again. - font.fontObj = obj; - - objs.push(obj); + fontsToLoad.push(font); var str = ''; - var data = obj.data; + var data = font.data; if (data) { var length = data.length; for (var j = 0; j < length; j++) str += String.fromCharCode(data[j]); - var rule = isWorker ? obj.bindWorker(str) : obj.bindDOM(str); + var rule = font.bindDOM(str); if (rule) { rules.push(rule); - names.push(obj.loadedName); + names.push(font.loadedName); } } } this.listeningForFontLoad = false; if (!isWorker && rules.length) { - FontLoader.prepareFontLoadEvent(rules, names, objs); + FontLoader.prepareFontLoadEvent(rules, names, fontsToLoad); } if (!checkFontsLoaded()) { document.documentElement.addEventListener( 'pdfjsFontLoad', checkFontsLoaded, false); } - - return objs; }, // Set things up so that at least one pdfjsFontLoad event is // dispatched when all the @font-face |rules| for |names| have been @@ -476,7 +469,7 @@ var FontLoader = { // has already started in this (outer) document, so that they should // be ordered before the load in the subdocument. prepareFontLoadEvent: function fontLoaderPrepareFontLoadEvent(rules, names, - objs) { + fonts) { /** Hack begin */ // There's no event when a font has finished downloading so the // following code is a dirty hack to 'guess' when a font is @@ -503,7 +496,10 @@ var FontLoader = { // Validate the names parameter -- the values can used to construct HTML. if (!/^\w+$/.test(names.join(''))) { error('Invalid font name(s): ' + names.join()); - return; // Keep the return in case if error() did not throw. + + // Normally the error-function throws. But if a malicious code + // intercepts the function call then the return is needed. + return; } var div = document.createElement('div'); @@ -523,8 +519,8 @@ var FontLoader = { 'message', function fontLoaderMessage(e) { var fontNames = JSON.parse(e.data); - for (var i = 0, ii = objs.length; i < ii; ++i) { - var font = objs[i]; + for (var i = 0, ii = fonts.length; i < ii; ++i) { + var font = fonts[i]; font.loading = false; } var evt = document.createEvent('Events'); @@ -770,7 +766,7 @@ var Font = (function FontClosure() { function Font(name, file, properties) { this.name = name; this.coded = properties.coded; - this.charProcIRQueues = properties.charProcIRQueues; + this.charProcOperatorList = properties.charProcOperatorList; this.resources = properties.resources; this.sizes = []; @@ -843,7 +839,7 @@ var Font = (function FontClosure() { var subtype = properties.subtype; var cff = (subtype == 'Type1C' || subtype == 'CIDFontType0C') ? - new Type2CFF(file, properties) : new CFF(name, file, properties); + new CFFFont(file, properties) : new Type1Font(name, file, properties); // Wrap the CFF data inside an OTF font file data = this.convert(name, cff, properties); @@ -868,7 +864,7 @@ var Font = (function FontClosure() { this.widthMultiplier = !properties.fontMatrix ? 1.0 : 1.0 / properties.fontMatrix[0]; this.encoding = properties.baseEncoding; - this.loadedName = getUniqueName(); + this.loadedName = properties.loadedName; this.loading = true; }; @@ -2278,17 +2274,6 @@ var Font = (function FontClosure() { } }, - bindWorker: function font_bindWorker(data) { - postMessage({ - action: 'font', - data: { - raw: data, - fontName: this.loadedName, - mimetype: this.mimetype - } - }); - }, - bindDOM: function font_bindDom(data) { var fontName = this.loadedName; @@ -2342,7 +2327,7 @@ var Font = (function FontClosure() { }, charToGlyph: function fonts_charToGlyph(charcode) { - var fontCharCode, width, codeIRQueue; + var fontCharCode, width, operatorList; var width = this.widths[charcode]; @@ -2377,7 +2362,7 @@ var Font = (function FontClosure() { break; case 'Type3': var glyphName = this.differences[charcode] || this.encoding[charcode]; - codeIRQueue = this.charProcIRQueues[glyphName]; + operatorList = this.charProcOperatorList[glyphName]; fontCharCode = charcode; break; case 'TrueType': @@ -2420,7 +2405,7 @@ var Font = (function FontClosure() { fontChar: String.fromCharCode(fontCharCode), unicode: unicodeChars, width: width, - codeIRQueue: codeIRQueue + operatorList: operatorList }; }, @@ -2989,7 +2974,7 @@ var Type1Parser = function type1Parser() { * The CFF class takes a Type1 file and wrap it into a * 'Compact Font Format' which itself embed Type2 charstrings. */ -var CFFStrings = [ +var CFFStandardStrings = [ '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', @@ -3059,7 +3044,8 @@ var CFFStrings = [ var type1Parser = new Type1Parser(); -var CFF = function cffCFF(name, file, properties) { +// Type1Font is also a CIDFontType0. +var Type1Font = function Type1Font(name, file, properties) { // Get the data block containing glyphs and subrs informations var headerBlock = file.getBytes(properties.length1); type1Parser.extractFontHeader(headerBlock, properties); @@ -3079,8 +3065,8 @@ var CFF = function cffCFF(name, file, properties) { subrs, properties); }; -CFF.prototype = { - createCFFIndexHeader: function cff_createCFFIndexHeader(objects, isByte) { +Type1Font.prototype = { + createCFFIndexHeader: function createCFFIndexHeader(objects, isByte) { // First 2 bytes contains the number of objects contained into this index var count = objects.length; @@ -3116,7 +3102,7 @@ CFF.prototype = { return data; }, - encodeNumber: function cff_encodeNumber(value) { + encodeNumber: function encodeNumber(value) { // some of the fonts has ouf-of-range values // they are just arithmetic overflows // make sanitizer happy @@ -3134,7 +3120,7 @@ CFF.prototype = { } }, - getOrderedCharStrings: function cff_getOrderedCharStrings(glyphs, + getOrderedCharStrings: function type1Font_getOrderedCharStrings(glyphs, properties) { var charstrings = []; var i, length, glyphName; @@ -3160,7 +3146,7 @@ CFF.prototype = { return charstrings; }, - getType2Charstrings: function cff_getType2Charstrings(type1Charstrings) { + getType2Charstrings: function getType2Charstrings(type1Charstrings) { var type2Charstrings = []; var count = type1Charstrings.length; for (var i = 0; i < count; i++) { @@ -3171,7 +3157,7 @@ CFF.prototype = { return type2Charstrings; }, - getType2Subrs: function cff_getType2Subrs(type1Subrs) { + getType2Subrs: function getType2Subrs(type1Subrs) { var bias = 0; var count = type1Subrs.length; if (count < 1240) @@ -3319,7 +3305,7 @@ CFF.prototype = { var count = glyphs.length; for (var i = 0; i < count; i++) { - var index = CFFStrings.indexOf(charstrings[i].glyph); + var index = CFFStandardStrings.indexOf(charstrings[i].glyph); // Some characters like asterikmath && circlecopyrt are // missing from the original strings, for the moment let's // map them to .notdef and see later if it cause any @@ -3388,106 +3374,31 @@ CFF.prototype = { } }; -var Type2CFF = (function Type2CFFClosure() { - // TODO: replace parsing code with the Type2Parser in font_utils.js - function Type2CFF(file, properties) { - var bytes = file.getBytes(); - this.bytes = bytes; +var CFFFont = (function CFFFontClosure() { + function CFFFont(file, properties) { this.properties = properties; - this.data = this.parse(); + var parser = new CFFParser(file, properties); + var cff = parser.parse(); + var compiler = new CFFCompiler(cff); + this.readExtra(cff); + try { + this.data = compiler.compile(); + } catch (e) { + warn('Failed to compile font ' + properties.loadedName); + // There may have just been an issue with the compiler, set the data + // anyway and hope the font loaded. + this.data = file; + } } - Type2CFF.prototype = { - parse: function cff_parse() { - var header = this.parseHeader(); - var properties = this.properties; - - var nameIndex = this.parseIndex(header.endPos); - this.sanitizeName(nameIndex); - - var dictIndex = this.parseIndex(nameIndex.endPos); - if (dictIndex.length != 1) - error('CFF contains more than 1 font'); - - var stringIndex = this.parseIndex(dictIndex.endPos); - var gsubrIndex = this.parseIndex(stringIndex.endPos); - - var strings = this.getStrings(stringIndex); - - var baseDict = this.parseDict(dictIndex.get(0).data); - var topDict = this.getTopDict(baseDict, strings); - - var bytes = this.bytes; - - var privateDict = {}; - var privateInfo = topDict.Private; - if (privateInfo) { - var privOffset = privateInfo[1], privLength = privateInfo[0]; - var privBytes = bytes.subarray(privOffset, privOffset + privLength); - baseDict = this.parseDict(privBytes); - privateDict = this.getPrivDict(baseDict, strings); - } else { - privateDict.defaultWidthX = properties.defaultWidth; - } - - var charStrings = this.parseIndex(topDict.CharStrings); - - var charset, encoding; - var isCIDFont = properties.subtype == 'CIDFontType0C'; - if (isCIDFont) { - charset = ['.notdef']; - for (var i = 1, ii = charStrings.length; i < ii; ++i) - charset.push('glyph' + i); - - encoding = this.parseCidMap(topDict.charset, - charStrings.length); - } else { - charset = this.parseCharsets(topDict.charset, - charStrings.length, strings); - encoding = this.parseEncoding(topDict.Encoding, properties, - strings, charset); - } - - // The font sanitizer does not support CFF encoding with a - // supplement, since the encoding is not really use to map - // between gid to glyph, let's overwrite what is declared in - // the top dictionary to let the sanitizer think the font use - // StandardEncoding, that's a lie but that's ok. - if (encoding.hasSupplement) - bytes[topDict.Encoding] &= 0x7F; - - // The CFF specification state that the 'dotsection' command - // (12, 0) is deprecated and treated as a no-op, but all Type2 - // charstrings processors should support them. Unfortunately - // the font sanitizer don't. As a workaround the sequence (12, 0) - // is replaced by a useless (0, hmoveto). - var count = charStrings.length; - for (var i = 0; i < count; i++) { - var charstring = charStrings.get(i); - - var start = charstring.start; - var data = charstring.data; - var length = data.length; - for (var j = 0; j <= length; j) { - var value = data[j++]; - if (value == 12 && data[j++] == 0) { - bytes[start + j - 2] = 139; - bytes[start + j - 1] = 22; - } else if (value === 28) { - j += 2; - } else if (value >= 247 && value <= 254) { - j++; - } else if (value == 255) { - j += 4; - } - } - } - + CFFFont.prototype = { + readExtra: function readExtra(cff) { // charstrings contains info about glyphs (one element per glyph // containing mappings for {unicode, width}) - var charstrings = this.getCharStrings(charset, encoding.encoding, - privateDict, this.properties); + var charset = cff.charset.charset; + var encoding = cff.encoding ? cff.encoding.encoding : null; + var charstrings = this.getCharStrings(charset, encoding); // create the mapping between charstring and glyph id var glyphIds = []; @@ -3496,21 +3407,18 @@ var Type2CFF = (function Type2CFFClosure() { this.charstrings = charstrings; this.glyphIds = glyphIds; - - var data = []; - for (var i = 0, ii = bytes.length; i < ii; ++i) - data.push(bytes[i]); - return data; }, - - getCharStrings: function cff_charstrings(charsets, encoding, - privateDict, properties) { + getCharStrings: function getCharStrings(charsets, encoding) { var charstrings = []; var unicodeUsed = []; var unassignedUnicodeItems = []; var inverseEncoding = []; - for (var charcode in encoding) - inverseEncoding[encoding[charcode]] = charcode | 0; + // CID fonts don't have an encoding. + if (encoding !== null) + for (var charcode in encoding) + inverseEncoding[encoding[charcode]] = charcode | 0; + else + inverseEncoding = charsets; for (var i = 0, ii = charsets.length; i < ii; i++) { var glyph = charsets[i]; if (glyph == '.notdef') @@ -3546,284 +3454,80 @@ var Type2CFF = (function Type2CFFClosure() { } // sort the array by the unicode value (again) - charstrings.sort(function type2CFFGetCharStringsSort(a, b) { + charstrings.sort(function getCharStringsSort(a, b) { return a.unicode - b.unicode; }); return charstrings; - }, + } + }; - parseEncoding: function cff_parseencoding(pos, properties, strings, - charset) { - var encoding = {}; - var bytes = this.bytes; - var result = { - encoding: encoding, - hasSupplement: false - }; + return CFFFont; +})(); - function readSupplement() { - var supplementsCount = bytes[pos++]; - for (var i = 0; i < supplementsCount; i++) { - var code = bytes[pos++]; - var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff); - encoding[code] = properties.differences.indexOf(strings[sid]); - } - } - - if (pos == 0 || pos == 1) { - var gid = 1; - var baseEncoding = pos ? Encodings.ExpertEncoding : - Encodings.StandardEncoding; - for (var i = 0, ii = charset.length; i < ii; i++) { - var index = baseEncoding.indexOf(charset[i]); - if (index != -1) - encoding[index] = gid++; +var CFFParser = (function CFFParserClosure() { + function CFFParser(file, properties) { + this.bytes = file.getBytes(); + this.properties = properties; + } + CFFParser.prototype = { + parse: function parse() { + var properties = this.properties; + var cff = new CFF(); + this.cff = cff; + + // The first five sections must be in order, all the others are reached + // via offsets contained in one of the below. + var header = this.parseHeader(); + var nameIndex = this.parseIndex(header.endPos); + var topDictIndex = this.parseIndex(nameIndex.endPos); + var stringIndex = this.parseIndex(topDictIndex.endPos); + var globalSubrIndex = this.parseIndex(stringIndex.endPos); + + var topDictParsed = this.parseDict(topDictIndex.obj.get(0)); + var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings); + + cff.header = header.obj; + cff.names = this.parseNameIndex(nameIndex.obj); + cff.strings = this.parseStringIndex(stringIndex.obj); + cff.topDict = topDict; + cff.globalSubrIndex = globalSubrIndex.obj; + + this.parsePrivateDict(cff.topDict); + + cff.isCIDFont = topDict.hasName('ROS'); + + var charStringOffset = topDict.getByName('CharStrings'); + cff.charStrings = this.parseCharStrings(charStringOffset); + + var charset, encoding; + if (cff.isCIDFont) { + var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj; + for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) { + var dictRaw = fdArrayIndex.get(i); + var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw), + cff.strings); + this.parsePrivateDict(fontDict); + cff.fdArray.push(fontDict); } + // cid fonts don't have an encoding + encoding = null; + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, true); + cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'), + cff.charStrings.count); } else { - var format = bytes[pos++]; - switch (format & 0x7f) { - case 0: - var glyphsCount = bytes[pos++]; - for (var i = 1; i <= glyphsCount; i++) - encoding[bytes[pos++]] = i; - break; - - case 1: - var rangesCount = bytes[pos++]; - var gid = 1; - for (var i = 0; i < rangesCount; i++) { - var start = bytes[pos++]; - var left = bytes[pos++]; - for (var j = start; j <= start + left; j++) - encoding[j] = gid++; - } - break; - - default: - error('Unknow encoding format: ' + format + ' in CFF'); - break; - } - if (format & 0x80) { - readSupplement(); - result.hasSupplement = true; - } + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, false); + encoding = this.parseEncoding(topDict.getByName('Encoding'), + properties, + cff.strings, charset.charset); } - return result; + cff.charset = charset; + cff.encoding = encoding; + + return cff; }, - - parseCharsets: function cff_parsecharsets(pos, length, strings) { - if (pos == 0) { - return ISOAdobeCharset.slice(); - } else if (pos == 1) { - return ExpertCharset.slice(); - } else if (pos == 2) { - return ExpertSubsetCharset.slice(); - } - - var bytes = this.bytes; - var format = bytes[pos++]; - var charset = ['.notdef']; - - // subtract 1 for the .notdef glyph - length -= 1; - - switch (format) { - case 0: - for (var i = 0; i < length; i++) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - charset.push(strings[sid]); - } - break; - case 1: - while (charset.length <= length) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - var count = bytes[pos++]; - for (var i = 0; i <= count; i++) - charset.push(strings[sid++]); - } - break; - case 2: - while (charset.length <= length) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - var count = (bytes[pos++] << 8) | bytes[pos++]; - for (var i = 0; i <= count; i++) - charset.push(strings[sid++]); - } - break; - default: - error('Unknown charset format'); - } - return charset; - }, - - parseCidMap: function cff_parsecharsets(pos, length) { - var bytes = this.bytes; - var format = bytes[pos++]; - - var encoding = {}; - var map = {encoding: encoding}; - - encoding[0] = 0; - - var gid = 1; - switch (format) { - case 0: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - encoding[cid] = gid++; - } - break; - case 1: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - var count = bytes[pos++]; - for (var i = 0; i <= count; i++) - encoding[cid++] = gid++; - } - break; - case 2: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - var count = (bytes[pos++] << 8) | bytes[pos++]; - for (var i = 0; i <= count; i++) - encoding[cid++] = gid++; - } - break; - default: - error('Unknown charset format'); - } - return map; - }, - - getPrivDict: function cff_getprivdict(baseDict, strings) { - var dict = {}; - - // default values - dict['defaultWidthX'] = 0; - dict['nominalWidthX'] = 0; - - for (var i = 0, ii = baseDict.length; i < ii; ++i) { - var pair = baseDict[i]; - var key = pair[0]; - var value = pair[1]; - switch (key) { - case 20: - dict['defaultWidthX'] = value[0]; - case 21: - dict['nominalWidthX'] = value[0]; - default: - TODO('interpret top dict key: ' + key); - } - } - return dict; - }, - getTopDict: function cff_gettopdict(baseDict, strings) { - var dict = {}; - - // default values - dict['Encoding'] = 0; - dict['charset'] = 0; - - for (var i = 0, ii = baseDict.length; i < ii; ++i) { - var pair = baseDict[i]; - var key = pair[0]; - var value = pair[1]; - switch (key) { - case 1: - dict['Notice'] = strings[value[0]]; - break; - case 4: - dict['Weight'] = strings[value[0]]; - break; - case 3094: - dict['BaseFontName'] = strings[value[0]]; - break; - case 5: - dict['FontBBox'] = value; - break; - case 13: - dict['UniqueID'] = value[0]; - break; - case 15: - dict['charset'] = value[0]; - break; - case 16: - dict['Encoding'] = value[0]; - break; - case 17: - dict['CharStrings'] = value[0]; - break; - case 18: - dict['Private'] = value; - break; - case 3102: - case 3103: - case 3104: - case 3105: - case 3106: - case 3107: - case 3108: - case 3109: - case 3110: - dict['cidOperatorPresent'] = true; - break; - default: - TODO('interpret top dict key: ' + key); - } - } - return dict; - }, - sanitizeName: function cff_sanitizeName(nameIndex) { - // There should really only be one font, but loop to make sure. - for (var i = 0, ii = nameIndex.length; i < ii; ++i) { - var data = nameIndex.get(i).data; - var length = data.length; - if (length > 127) - warn('Font had name longer than 127 chars, will be rejected.'); - // Only certain chars are permitted in the font name. - for (var j = 0; j < length; ++j) { - var c = data[j]; - if (j === 0 && c === 0) - continue; - if (c < 33 || c > 126) { - data[j] = 95; - continue; - } - switch (c) { - case 91: // [ - case 93: // ] - case 40: // ( - case 41: // ) - case 123: // { - case 125: // } - case 60: // < - case 62: // > - case 47: // / - case 37: // % - data[j] = 95; - break; - } - } - } - }, - getStrings: function cff_getStrings(stringIndex) { - function bytesToString(bytesArray) { - var str = ''; - for (var i = 0, ii = bytesArray.length; i < ii; i++) - str += String.fromCharCode(bytesArray[i]); - return str; - } - - var stringArray = []; - for (var i = 0, ii = CFFStrings.length; i < ii; i++) - stringArray.push(CFFStrings[i]); - - for (var i = 0, ii = stringIndex.length; i < ii; i++) - stringArray.push(bytesToString(stringIndex.get(i).data)); - - return stringArray; - }, - parseHeader: function cff_parseHeader() { + parseHeader: function parseHeader() { var bytes = this.bytes; var offset = 0; @@ -3831,17 +3535,18 @@ var Type2CFF = (function Type2CFFClosure() { ++offset; if (offset != 0) { - warning('cff data is shifted'); + warn('cff data is shifted'); bytes = bytes.subarray(offset); this.bytes = bytes; } - - return { - endPos: bytes[2], - offsetSize: bytes[3] - }; + var major = bytes[0]; + var minor = bytes[1]; + var hdrSize = bytes[2]; + var offSize = bytes[3]; + var header = new CFFHeader(major, minor, hdrSize, offSize); + return {obj: header, endPos: hdrSize}; }, - parseDict: function cff_parseDict(dict) { + parseDict: function parseDict(dict) { var pos = 0; function parseOperand() { @@ -3858,11 +3563,11 @@ var Type2CFF = (function Type2CFFClosure() { value = (value << 8) | dict[pos++]; value = (value << 8) | dict[pos++]; return value; - } else if (value <= 246) { + } else if (value >= 32 && value <= 246) { return value - 139; - } else if (value <= 250) { + } else if (value >= 247 && value <= 250) { return ((value - 247) * 256) + dict[pos++] + 108; - } else if (value <= 254) { + } else if (value >= 251 && value <= 254) { return -((value - 251) * 256) - dict[pos++] - 108; } else { error('255 is not a valid DICT command'); @@ -3900,27 +3605,8 @@ var Type2CFF = (function Type2CFFClosure() { while (pos < end) { var b = dict[pos]; if (b <= 21) { - if (b === 12) { - ++pos; - var op = dict[pos]; - if ((op > 14 && op < 17) || - (op > 23 && op < 30) || op > 38) { - warn('Invalid CFF dictionary key: ' + op); - // trying to replace it with initialRandomSeed - // to pass sanitizer - dict[pos] = 19; - } - var b = (b << 8) | op; - } - if (!operands.length && b == 8 && - dict[pos + 1] == 9) { - // no operands for FamilyBlues, removing the key - // and next one is FamilyOtherBlues - skipping them - // also replacing FamilyBlues to pass sanitizer - dict[pos] = 139; - pos += 2; - continue; - } + if (b === 12) + b = (b << 8) | dict[++pos]; entries.push([b, operands]); operands = []; ++pos; @@ -3930,10 +3616,12 @@ var Type2CFF = (function Type2CFFClosure() { } return entries; }, - parseIndex: function cff_parseIndex(pos) { + parseIndex: function parseIndex(pos) { + var cffIndex = new CFFIndex(); var bytes = this.bytes; - var count = bytes[pos++] << 8 | bytes[pos++]; + var count = (bytes[pos++] << 8) | bytes[pos++]; var offsets = []; + var start = pos; var end = pos; if (count != 0) { @@ -3951,26 +3639,938 @@ var Type2CFF = (function Type2CFFClosure() { } end = offsets[count]; } + for (var i = 0, ii = offsets.length - 1; i < ii; ++i) { + var offsetStart = offsets[i]; + var offsetEnd = offsets[i + 1]; + cffIndex.add(bytes.subarray(offsetStart, offsetEnd)); + } + return {obj: cffIndex, endPos: end}; + }, + parseNameIndex: function parseNameIndex(index) { + var names = []; + for (var i = 0, ii = index.count; i < ii; ++i) { + var name = index.get(i); + // OTS doesn't allow names to be over 127 characters. + var length = Math.min(name.length, 127); + var data = new Array(length); + // OTS also only permits certain characters in the name. + for (var j = 0; j < length; ++j) { + var c = name[j]; + if (j === 0 && c === 0) { + data[j] = c; + continue; + } + if ((c < 33 || c > 126) || c === 91 /* [ */ || c === 93 /* ] */ || + c === 40 /* ( */ || c === 41 /* ) */ || c === 123 /* { */ || + c === 125 /* } */ || c === 60 /* < */ || c === 62 /* > */ || + c === 47 /* / */ || c === 37 /* % */) { + data[j] = 95; + continue; + } + data[j] = c; + } + names.push(String.fromCharCode.apply(null, data)); + } + return names; + }, + parseStringIndex: function parseStringIndex(index) { + var strings = new CFFStrings(); + for (var i = 0, ii = index.count; i < ii; ++i) { + var data = index.get(i); + strings.add(String.fromCharCode.apply(null, data)); + } + return strings; + }, + createDict: function createDict(type, dict, strings) { + var cffDict = new type(strings); + var types = cffDict.types; - return { - get: function index_get(index) { - if (index >= count) - return null; + for (var i = 0, ii = dict.length; i < ii; ++i) { + var pair = dict[i]; + var key = pair[0]; + var value = pair[1]; + cffDict.setByKey(key, value); + } + return cffDict; + }, + parseCharStrings: function parseCharStrings(charStringOffset) { + var charStrings = this.parseIndex(charStringOffset).obj; + // The CFF specification state that the 'dotsection' command + // (12, 0) is deprecated and treated as a no-op, but all Type2 + // charstrings processors should support them. Unfortunately + // the font sanitizer don't. As a workaround the sequence (12, 0) + // is replaced by a useless (0, hmoveto). + var count = charStrings.count; + for (var i = 0; i < count; i++) { + var charstring = charStrings.get(i); - var start = offsets[index]; - var end = offsets[index + 1]; - return { - start: start, - end: end, - data: bytes.subarray(start, end) - }; - }, - length: count, - endPos: end - }; + var data = charstring; + var length = data.length; + for (var j = 0; j <= length; j) { + var value = data[j++]; + if (value == 12 && data[j++] == 0) { + data[j - 2] = 139; + data[j - 1] = 22; + } else if (value === 28) { + j += 2; + } else if (value >= 247 && value <= 254) { + j++; + } else if (value == 255) { + j += 4; + } + } + } + return charStrings; + }, + parsePrivateDict: function parsePrivateDict(parentDict) { + // no private dict, do nothing + if (!parentDict.hasName('Private')) + return; + var privateOffset = parentDict.getByName('Private'); + // make sure the params are formatted correctly + if (!isArray(privateOffset) || privateOffset.length !== 2) { + parentDict.removeByName('Private'); + return; + } + var size = privateOffset[0]; + var offset = privateOffset[1]; + // remove empty dicts or ones that refer to invalid location + if (size === 0 || offset >= this.bytes.length) { + parentDict.removeByName('Private'); + return; + } + + var privateDictEnd = offset + size; + var dictData = this.bytes.subarray(offset, privateDictEnd); + var dict = this.parseDict(dictData); + var privateDict = this.createDict(CFFPrivateDict, dict, + parentDict.strings); + parentDict.privateDict = privateDict; + + // Parse the Subrs index also since it's relative to the private dict. + if (!privateDict.getByName('Subrs')) + return; + var subrsOffset = privateDict.getByName('Subrs'); + var relativeOffset = offset + subrsOffset; + // Validate the offset. + if (subrsOffset === 0 || relativeOffset >= this.bytes.length) { + privateDict.removeByName('Subrs'); + return; + } + var subrsIndex = this.parseIndex(relativeOffset); + privateDict.subrsIndex = subrsIndex.obj; + }, + parseCharsets: function parsecharsets(pos, length, strings, cid) { + if (pos == 0) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE, + ISOAdobeCharset); + } else if (pos == 1) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT, + ExpertCharset); + } else if (pos == 2) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET, + ExpertSubsetCharset); + } + + var bytes = this.bytes; + var start = pos; + var format = bytes[pos++]; + var charset = ['.notdef']; + + // subtract 1 for the .notdef glyph + length -= 1; + + switch (format) { + case 0: + for (var i = 0; i < length; i++) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + charset.push(cid ? id : strings.get(id)); + } + break; + case 1: + while (charset.length <= length) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + var count = bytes[pos++]; + for (var i = 0; i <= count; i++) + charset.push(cid ? id++ : strings.get(id++)); + } + break; + case 2: + while (charset.length <= length) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + var count = (bytes[pos++] << 8) | bytes[pos++]; + for (var i = 0; i <= count; i++) + charset.push(cid ? id++ : strings.get(id++)); + } + break; + default: + error('Unknown charset format'); + } + // Raw won't be needed if we actually compile the charset. + var end = pos; + var raw = bytes.subarray(start, end); + + return new CFFCharset(false, format, charset, raw); + }, + parseEncoding: function parseEncoding(pos, properties, strings, charset) { + var encoding = {}; + var bytes = this.bytes; + var predefined = false; + var hasSupplement = false; + var format; + var raw = null; + + function readSupplement() { + var supplementsCount = bytes[pos++]; + for (var i = 0; i < supplementsCount; i++) { + var code = bytes[pos++]; + var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff); + encoding[code] = properties.differences.indexOf(strings.get(sid)); + } + } + + if (pos == 0 || pos == 1) { + predefined = true; + format = pos; + var gid = 1; + var baseEncoding = pos ? Encodings.ExpertEncoding : + Encodings.StandardEncoding; + for (var i = 0, ii = charset.length; i < ii; i++) { + var index = baseEncoding.indexOf(charset[i]); + if (index != -1) + encoding[index] = gid++; + } + } else { + var dataStart = pos; + var format = bytes[pos++]; + switch (format & 0x7f) { + case 0: + var glyphsCount = bytes[pos++]; + for (var i = 1; i <= glyphsCount; i++) + encoding[bytes[pos++]] = i; + break; + + case 1: + var rangesCount = bytes[pos++]; + var gid = 1; + for (var i = 0; i < rangesCount; i++) { + var start = bytes[pos++]; + var left = bytes[pos++]; + for (var j = start; j <= start + left; j++) + encoding[j] = gid++; + } + break; + + default: + error('Unknow encoding format: ' + format + ' in CFF'); + break; + } + var dataEnd = pos; + if (format & 0x80) { + // The font sanitizer does not support CFF encoding with a + // supplement, since the encoding is not really used to map + // between gid to glyph, let's overwrite what is declared in + // the top dictionary to let the sanitizer think the font use + // StandardEncoding, that's a lie but that's ok. + bytes[dataStart] &= 0x7f; + readSupplement(); + hasSupplement = true; + } + raw = bytes.subarray(dataStart, dataEnd); + } + format = format & 0x7f; + return new CFFEncoding(predefined, format, encoding, raw); + }, + parseFDSelect: function parseFDSelect(pos, length) { + var start = pos; + var bytes = this.bytes; + var format = bytes[pos++]; + var fdSelect = []; + switch (format) { + case 0: + for (var i = 0; i < length; ++i) { + var id = bytes[pos++]; + fdSelect.push(id); + } + break; + case 3: + var rangesCount = (bytes[pos++] << 8) | bytes[pos++]; + for (var i = 0; i < rangesCount; ++i) { + var first = (bytes[pos++] << 8) | bytes[pos++]; + var fdIndex = bytes[pos++]; + var next = (bytes[pos] << 8) | bytes[pos + 1]; + for (var j = first; j < next; ++j) + fdSelect.push(fdIndex); + } + // Advance past the sentinel(next). + pos += 2; + break; + default: + error('Unknown fdselect format ' + format); + break; + } + var end = pos; + return new CFFFDSelect(fdSelect, bytes.subarray(start, end)); } }; - - return Type2CFF; + return CFFParser; +})(); + +// Compact Font Format +var CFF = (function CFFClosure() { + function CFF() { + this.header = null; + this.names = []; + this.topDict = null; + this.strings = new CFFStrings(); + this.globalSubrIndex = null; + + // The following could really be per font, but since we only have one font + // store them here. + this.encoding = null; + this.charset = null; + this.charStrings = null; + this.fdArray = []; + this.fdSelect = null; + + this.isCIDFont = false; + } + return CFF; +})(); + +var CFFHeader = (function CFFHeaderClosure() { + function CFFHeader(major, minor, hdrSize, offSize) { + this.major = major; + this.minor = minor; + this.hdrSize = hdrSize; + this.offSize = offSize; + } + return CFFHeader; +})(); + +var CFFStrings = (function CFFStringsClosure() { + function CFFStrings() { + this.strings = []; + } + CFFStrings.prototype = { + get: function get(index) { + if (index >= 0 && index <= 390) + return CFFStandardStrings[index]; + if (index - 391 <= this.strings.length) + return this.strings[index - 391]; + return CFFStandardStrings[0]; + }, + add: function add(value) { + this.strings.push(value); + }, + get count() { + return this.strings.length; + } + }; + return CFFStrings; +})(); + +var CFFIndex = (function CFFIndexClosure() { + function CFFIndex() { + this.objects = []; + this.length = 0; + } + CFFIndex.prototype = { + add: function add(data) { + this.length += data.length; + this.objects.push(data); + }, + get: function get(index) { + return this.objects[index]; + }, + get count() { + return this.objects.length; + } + }; + return CFFIndex; +})(); + +var CFFDict = (function CFFDictClosure() { + function CFFDict(tables, strings) { + this.keyToNameMap = tables.keyToNameMap; + this.nameToKeyMap = tables.nameToKeyMap; + this.defaults = tables.defaults; + this.types = tables.types; + this.opcodes = tables.opcodes; + this.order = tables.order; + this.strings = strings; + this.values = {}; + } + CFFDict.prototype = { + // value should always be an array + setByKey: function setByKey(key, value) { + if (!(key in this.keyToNameMap)) + return false; + // ignore empty values + if (value.length === 0) + return true; + var type = this.types[key]; + // remove the array wrapping these types of values + if (type === 'num' || type === 'sid' || type === 'offset') + value = value[0]; + this.values[key] = value; + return true; + }, + hasName: function hasName(name) { + return this.nameToKeyMap[name] in this.values; + }, + getByName: function getByName(name) { + if (!(name in this.nameToKeyMap)) + error('Invalid dictionary name "' + name + '"'); + var key = this.nameToKeyMap[name]; + if (!(key in this.values)) + return this.defaults[key]; + return this.values[key]; + }, + removeByName: function removeByName(name) { + delete this.values[this.nameToKeyMap[name]]; + } + }; + CFFDict.createTables = function createTables(layout) { + var tables = { + keyToNameMap: {}, + nameToKeyMap: {}, + defaults: {}, + types: {}, + opcodes: {}, + order: [] + }; + for (var i = 0, ii = layout.length; i < ii; ++i) { + var entry = layout[i]; + var key = isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0]; + tables.keyToNameMap[key] = entry[1]; + tables.nameToKeyMap[entry[1]] = key; + tables.types[key] = entry[2]; + tables.defaults[key] = entry[3]; + tables.opcodes[key] = isArray(entry[0]) ? entry[0] : [entry[0]]; + tables.order.push(key); + } + return tables; + }; + return CFFDict; +})(); + +var CFFTopDict = (function CFFTopDictClosure() { + var layout = [ + [[12, 30], 'ROS', ['sid', 'sid', 'num'], null], + [[12, 20], 'SyntheticBase', 'num', null], + [0, 'version', 'sid', null], + [1, 'Notice', 'sid', null], + [[12, 0], 'Copyright', 'sid', null], + [2, 'FullName', 'sid', null], + [3, 'FamilyName', 'sid', null], + [4, 'Weight', 'sid', null], + [[12, 1], 'isFixedPitch', 'num', 0], + [[12, 2], 'ItalicAngle', 'num', 0], + [[12, 3], 'UnderlinePosition', 'num', -100], + [[12, 4], 'UnderlineThickness', 'num', 50], + [[12, 5], 'PaintType', 'num', 0], + [[12, 6], 'CharstringType', 'num', 2], + [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'], + [.001, 0, 0, .001, 0, 0]], + [13, 'UniqueID', 'num', null], + [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]], + [[12, 8], 'StrokeWidth', 'num', 0], + [14, 'XUID', 'array', null], + [15, 'charset', 'offset', 0], + [16, 'Encoding', 'offset', 0], + [17, 'CharStrings', 'offset', 0], + [18, 'Private', ['offset', 'offset'], null], + [[12, 21], 'PostScript', 'sid', null], + [[12, 22], 'BaseFontName', 'sid', null], + [[12, 23], 'BaseFontBlend', 'delta', null], + [[12, 31], 'CIDFontVersion', 'num', 0], + [[12, 32], 'CIDFontRevision', 'num', 0], + [[12, 33], 'CIDFontType', 'num', 0], + [[12, 34], 'CIDCount', 'num', 8720], + [[12, 35], 'UIDBase', 'num', null], + [[12, 36], 'FDArray', 'offset', null], + [[12, 37], 'FDSelect', 'offset', null], + [[12, 38], 'FontName', 'sid', null]]; + var tables = null; + function CFFTopDict(strings) { + if (tables === null) + tables = CFFDict.createTables(layout); + CFFDict.call(this, tables, strings); + this.privateDict = null; + } + CFFTopDict.prototype = Object.create(CFFDict.prototype); + return CFFTopDict; +})(); + +var CFFPrivateDict = (function CFFPrivateDictClosure() { + var layout = [ + [6, 'BlueValues', 'delta', null], + [7, 'OtherBlues', 'delta', null], + [8, 'FamilyBlues', 'delta', null], + [9, 'FamilyOtherBlues', 'delta', null], + [[12, 9], 'BlueScale', 'num', 0.039625], + [[12, 10], 'BlueShift', 'num', 7], + [[12, 11], 'BlueFuzz', 'num', 1], + [10, 'StdHW', 'num', null], + [11, 'StdVW', 'num', null], + [[12, 12], 'StemSnapH', 'delta', null], + [[12, 13], 'StemSnapV', 'delta', null], + [[12, 14], 'ForceBold', 'num', 0], + [[12, 17], 'LanguageGroup', 'num', 0], + [[12, 18], 'ExpansionFactor', 'num', 0.06], + [[12, 19], 'initialRandomSeed', 'num', 0], + [19, 'Subrs', 'offset', null], + [20, 'defaultWidthX', 'num', 0], + [21, 'nominalWidthX', 'num', 0] + ]; + var tables = null; + function CFFPrivateDict(strings) { + if (tables === null) + tables = CFFDict.createTables(layout); + CFFDict.call(this, tables, strings); + this.subrsIndex = null; + } + CFFPrivateDict.prototype = Object.create(CFFDict.prototype); + return CFFPrivateDict; +})(); + +var CFFCharsetPredefinedTypes = { + ISO_ADOBE: 0, + EXPERT: 1, + EXPERT_SUBSET: 2 +}; +var CFFCharsetEmbeddedTypes = { + FORMAT0: 0, + FORMAT1: 1, + FORMAT2: 2 +}; +var CFFCharset = (function CFFCharsetClosure() { + function CFFCharset(predefined, format, charset, raw) { + this.predefined = predefined; + this.format = format; + this.charset = charset; + this.raw = raw; + } + return CFFCharset; +})(); + +var CFFEncodingPredefinedTypes = { + STANDARD: 0, + EXPERT: 1 +}; +var CFFCharsetEmbeddedTypes = { + FORMAT0: 0, + FORMAT1: 1 +}; +var CFFEncoding = (function CFFEncodingClosure() { + function CFFEncoding(predefined, format, encoding, raw) { + this.predefined = predefined; + this.format = format; + this.encoding = encoding; + this.raw = raw; + } + return CFFEncoding; +})(); + +var CFFFDSelect = (function CFFFDSelectClosure() { + function CFFFDSelect(fdSelect, raw) { + this.fdSelect = fdSelect; + this.raw = raw; + } + return CFFFDSelect; +})(); + +// Helper class to keep track of where an offset is within the data and helps +// filling in that offset once it's known. +var CFFOffsetTracker = (function CFFOffsetTracker() { + function CFFOffsetTracker() { + this.offsets = {}; + } + CFFOffsetTracker.prototype = { + isTracking: function isTracking(key) { + return key in this.offsets; + }, + track: function track(key, location) { + if (key in this.offsets) + error('Already tracking location of ' + key); + this.offsets[key] = location; + }, + offset: function offset(value) { + for (var key in this.offsets) { + this.offsets[key] += value; + } + }, + setEntryLocation: function setEntryLocation(key, values, output) { + if (!(key in this.offsets)) + error('Not tracking location of ' + key); + var data = output.data; + var dataOffset = this.offsets[key]; + var size = 5; + for (var i = 0, ii = values.length; i < ii; ++i) { + var offset0 = i * size + dataOffset; + var offset1 = offset0 + 1; + var offset2 = offset0 + 2; + var offset3 = offset0 + 3; + var offset4 = offset0 + 4; + // It's easy to screw up offsets so perform this sanity check. + if (data[offset0] !== 0x1d || data[offset1] !== 0 || + data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) + error('writing to an offset that is not empty'); + var value = values[i]; + data[offset0] = 0x1d; + data[offset1] = (value >> 24) & 0xFF; + data[offset2] = (value >> 16) & 0xFF; + data[offset3] = (value >> 8) & 0xFF; + data[offset4] = value & 0xFF; + } + } + }; + return CFFOffsetTracker; +})(); + +// Takes a CFF and converts it to the binary representation. +var CFFCompiler = (function CFFCompilerClosure() { + function stringToArray(str) { + var array = []; + for (var i = 0, ii = str.length; i < ii; ++i) + array[i] = str.charCodeAt(i); + + return array; + }; + function CFFCompiler(cff) { + this.cff = cff; + } + CFFCompiler.prototype = { + compile: function compile() { + var cff = this.cff; + var output = { + data: [], + length: 0, + add: function add(data) { + this.data = this.data.concat(data); + this.length = this.data.length; + } + }; + + // Compile the five entries that must be in order. + var header = this.compileHeader(cff.header); + output.add(header); + + var nameIndex = this.compileNameIndex(cff.names); + output.add(nameIndex); + + var compiled = this.compileTopDicts([cff.topDict], output.length); + output.add(compiled.output); + var topDictTracker = compiled.trackers[0]; + + var stringIndex = this.compileStringIndex(cff.strings.strings); + output.add(stringIndex); + + var globalSubrIndex = this.compileIndex(cff.globalSubrIndex); + output.add(globalSubrIndex); + + // Now start on the other entries that have no specfic order. + if (cff.encoding && cff.topDict.hasName('Encoding')) { + if (cff.encoding.predefined) { + topDictTracker.setEntryLocation('Encoding', [cff.encoding.format], + output); + } else { + var encoding = this.compileEncoding(cff.encoding); + topDictTracker.setEntryLocation('Encoding', [output.length], output); + output.add(encoding); + } + } + + if (cff.charset && cff.topDict.hasName('charset')) { + if (cff.charset.predefined) { + topDictTracker.setEntryLocation('charset', [cff.charset.format], + output); + } else { + var charset = this.compileCharset(cff.charset); + topDictTracker.setEntryLocation('charset', [output.length], output); + output.add(charset); + } + } + + var charStrings = this.compileCharStrings(cff.charStrings); + topDictTracker.setEntryLocation('CharStrings', [output.length], output); + output.add(charStrings); + + if (cff.isCIDFont) { + var compiled = this.compileTopDicts(cff.fdArray, output.length); + topDictTracker.setEntryLocation('FDArray', [output.length], output); + output.add(compiled.output); + var fontDictTrackers = compiled.trackers; + + this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output); + + topDictTracker.setEntryLocation('FDSelect', [output.length], output); + var fdSelect = this.compileFDSelect(cff.fdSelect.raw); + output.add(fdSelect); + } + + this.compilePrivateDicts([cff.topDict], [topDictTracker], output); + + return output.data; + }, + encodeNumber: function encodeNumber(value) { + if (parseFloat(value) == parseInt(value) && !isNaN(value)) // isInt + return this.encodeInteger(value); + else + return this.encodeFloat(value); + }, + encodeFloat: function encodeFloat(value) { + value = value.toString(); + // Strip off the any leading zeros. + if (value.substr(0, 2) === '0.') + value = value.substr(1); + else if (value.substr(0, 3) === '-0.') + value = '-' + value.substr(2); + var nibbles = []; + for (var i = 0, ii = value.length; i < ii; ++i) { + var a = value.charAt(i), b = value.charAt(i + 1); + var nibble; + if (a === 'e' && b === '-') { + nibble = 0xc; + ++i; + } else if (a === '.') { + nibble = 0xa; + } else if (a === 'E') { + nibble = 0xb; + } else if (a === '-') { + nibble = 0xe; + } else { + nibble = a; + } + nibbles.push(nibble); + } + nibbles.push(0xf); + if (nibbles.length % 2) + nibbles.push(0xf); + var out = [30]; + for (var i = 0, ii = nibbles.length; i < ii; i += 2) + out.push(nibbles[i] << 4 | nibbles[i + 1]); + return out; + }, + encodeInteger: function encodeInteger(value) { + var code; + if (value >= -107 && value <= 107) { + code = [value + 139]; + } else if (value >= 108 && value <= 1131) { + value = [value - 108]; + code = [(value >> 8) + 247, value & 0xFF]; + } else if (value >= -1131 && value <= -108) { + value = -value - 108; + code = [(value >> 8) + 251, value & 0xFF]; + } else if (value >= -32768 && value <= 32767) { + code = [0x1c, (value >> 8) & 0xFF, value & 0xFF]; + } else { + code = [0x1d, + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF]; + } + return code; + }, + compileHeader: function compileHeader(header) { + return [ + header.major, + header.minor, + header.hdrSize, + header.offSize + ]; + }, + compileNameIndex: function compileNameIndex(names) { + var nameIndex = new CFFIndex(); + for (var i = 0, ii = names.length; i < ii; ++i) + nameIndex.add(stringToArray(names[i])); + return this.compileIndex(nameIndex); + }, + compileTopDicts: function compileTopDicts(dicts, length) { + var fontDictTrackers = []; + var fdArrayIndex = new CFFIndex(); + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + var fontDictTracker = new CFFOffsetTracker(); + var fontDictData = this.compileDict(fontDict, fontDictTracker); + fontDictTrackers.push(fontDictTracker); + fdArrayIndex.add(fontDictData); + fontDictTracker.offset(length); + } + fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers); + return { + trackers: fontDictTrackers, + output: fdArrayIndex + }; + }, + compilePrivateDicts: function compilePrivateDicts(dicts, trackers, output) { + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + if (!fontDict.privateDict || !fontDict.hasName('Private')) + continue; + var privateDict = fontDict.privateDict; + var privateDictTracker = new CFFOffsetTracker(); + var privateDictData = this.compileDict(privateDict, privateDictTracker); + + privateDictTracker.offset(output.length); + trackers[i].setEntryLocation('Private', + [privateDictData.length, output.length], + output); + output.add(privateDictData); + + if (privateDict.subrsIndex && privateDict.hasName('Subrs')) { + var subrs = this.compileIndex(privateDict.subrsIndex); + privateDictTracker.setEntryLocation('Subrs', [privateDictData.length], + output); + output.add(subrs); + } + } + }, + compileDict: function compileDict(dict, offsetTracker) { + var out = []; + // The dictionary keys must be in a certain order. + var order = dict.order; + for (var i = 0; i < order.length; ++i) { + var key = order[i]; + if (!(key in dict.values)) + continue; + var values = dict.values[key]; + var types = dict.types[key]; + if (!isArray(types)) types = [types]; + if (!isArray(values)) values = [values]; + + // Remove any empty dict values. + if (values.length === 0) + continue; + + for (var j = 0, jj = types.length; j < jj; ++j) { + var type = types[j]; + var value = values[j]; + switch (type) { + case 'num': + case 'sid': + out = out.concat(this.encodeNumber(value)); + break; + case 'offset': + // For offsets we just insert a 32bit integer so we don't have to + // deal with figuring out the length of the offset when it gets + // replaced later on by the compiler. + var name = dict.keyToNameMap[key]; + // Some offsets have the offset and the length, so just record the + // position of the first one. + if (!offsetTracker.isTracking(name)) + offsetTracker.track(name, out.length); + out = out.concat([0x1d, 0, 0, 0, 0]); + break; + case 'array': + case 'delta': + out = out.concat(this.encodeNumber(value)); + for (var k = 1, kk = values.length; k < kk; ++k) + out = out.concat(this.encodeNumber(values[k])); + break; + default: + error('Unknown data type of ' + type); + break; + } + } + out = out.concat(dict.opcodes[key]); + } + return out; + }, + compileStringIndex: function compileStringIndex(strings) { + var stringIndex = new CFFIndex(); + for (var i = 0, ii = strings.length; i < ii; ++i) + stringIndex.add(stringToArray(strings[i])); + return this.compileIndex(stringIndex); + }, + compileGlobalSubrIndex: function compileGlobalSubrIndex() { + var globalSubrIndex = this.cff.globalSubrIndex; + this.out.writeByteArray(this.compileIndex(globalSubrIndex)); + }, + compileCharStrings: function compileCharStrings(charStrings) { + return this.compileIndex(charStrings); + }, + compileCharset: function compileCharset(charset) { + return this.compileTypedArray(charset.raw); + }, + compileEncoding: function compileEncoding(encoding) { + return this.compileTypedArray(encoding.raw); + }, + compileFDSelect: function compileFDSelect(fdSelect) { + return this.compileTypedArray(fdSelect); + }, + compileTypedArray: function compileTypedArray(data) { + var out = new Array(data.length); + for (var i = 0, ii = data.length; i < ii; ++i) + out[i] = data[i]; + return out; + }, + compileIndex: function compileIndex(index, trackers) { + trackers = trackers || []; + var objects = index.objects; + // First 2 bytes contains the number of objects contained into this index + var count = objects.length; + + // If there is no object, just create an index. This technically + // should just be [0, 0] but OTS has an issue with that. + if (count == 0) + return [0, 0, 0]; + + var data = [(count >> 8) & 0xFF, count & 0xff]; + + var lastOffset = 1; + for (var i = 0; i < count; ++i) + lastOffset += objects[i].length; + + var offsetSize; + if (lastOffset < 0x100) + offsetSize = 1; + else if (lastOffset < 0x10000) + offsetSize = 2; + else if (lastOffset < 0x1000000) + offsetSize = 3; + else + offsetSize = 4; + + // Next byte contains the offset size use to reference object in the file + data.push(offsetSize); + + // Add another offset after this one because we need a new offset + var relativeOffset = 1; + for (var i = 0; i < count + 1; i++) { + if (offsetSize === 1) { + data.push(relativeOffset & 0xFF); + } else if (offsetSize === 2) { + data.push((relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else if (offsetSize === 3) { + data.push((relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else { + data.push((relativeOffset >>> 24) & 0xFF, + (relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } + + if (objects[i]) + relativeOffset += objects[i].length; + } + var offset = data.length; + + for (var i = 0; i < count; i++) { + // Notify the tracker where the object will be offset in the data. + if (trackers[i]) + trackers[i].offset(data.length); + for (var j = 0, jj = objects[i].length; j < jj; j++) + data.push(objects[i][j]); + } + return data; + } + }; + return CFFCompiler; })(); diff --git a/src/obj.js b/src/obj.js index c38f922e5..3c649fb06 100644 --- a/src/obj.js +++ b/src/obj.js @@ -514,7 +514,6 @@ var XRef = (function XRefClosure() { return dict; // nothing helps error('Invalid PDF structure'); - return null; }, readXRef: function readXref(startXRef) { var stream = this.stream; @@ -723,12 +722,10 @@ var PDFObjects = (function PDFObjectsClosure() { // If there isn't an object yet or the object isn't resolved, then the // data isn't ready yet! - if (!obj || !obj.isResolved) { + if (!obj || !obj.isResolved) error('Requesting object that isn\'t resolved yet ' + objId); - return null; - } else { - return obj.data; - } + + return obj.data; }, /** diff --git a/src/parser.js b/src/parser.js index fc8f5bc66..fad8b2c03 100644 --- a/src/parser.js +++ b/src/parser.js @@ -53,15 +53,14 @@ var Parser = (function ParserClosure() { this.shift(); var dict = new Dict(); while (!isCmd(this.buf1, '>>') && !isEOF(this.buf1)) { - if (!isName(this.buf1)) { + if (!isName(this.buf1)) error('Dictionary key must be a name object'); - } else { - var key = this.buf1.name; - this.shift(); - if (isEOF(this.buf1)) - break; - dict.set(key, this.getObj(cipherTransform)); - } + + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) + break; + dict.set(key, this.getObj(cipherTransform)); } if (isEOF(this.buf1)) error('End of file inside dictionary'); @@ -106,15 +105,14 @@ var Parser = (function ParserClosure() { // parse dictionary var dict = new Dict(); while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { - if (!isName(this.buf1)) { + if (!isName(this.buf1)) error('Dictionary key must be a name object'); - } else { - var key = this.buf1.name; - this.shift(); - if (isEOF(this.buf1)) - break; - dict.set(key, this.getObj(cipherTransform)); - } + + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) + break; + dict.set(key, this.getObj(cipherTransform)); } // parse image stream @@ -176,10 +174,8 @@ var Parser = (function ParserClosure() { // get length var length = this.fetchIfRef(dict.get('Length')); - if (!isInt(length)) { + if (!isInt(length)) error('Bad ' + length + ' attribute in stream'); - length = 0; - } // skip over the stream data stream.pos = pos + length; @@ -208,14 +204,13 @@ var Parser = (function ParserClosure() { filter = filterArray[i]; if (!isName(filter)) error('Bad filter name: ' + filter); - else { - params = null; - if (isArray(paramsArray) && (i in paramsArray)) - params = paramsArray[i]; - stream = this.makeFilter(stream, filter.name, length, params); - // after the first stream the length variable is invalid - length = null; - } + + params = null; + if (isArray(paramsArray) && (i in paramsArray)) + params = paramsArray[i]; + stream = this.makeFilter(stream, filter.name, length, params); + // after the first stream the length variable is invalid + length = null; } } return stream; @@ -527,17 +522,15 @@ var Lexer = (function LexerClosure() { // fall through case ')': error('Illegal character: ' + ch); - return Error; } // command var str = ch; while (!!(ch = stream.lookChar()) && !specialChars[ch.charCodeAt(0)]) { stream.skip(); - if (str.length == 128) { + if (str.length == 128) error('Command token too long: ' + str.length); - break; - } + str += ch; } if (str == 'true') @@ -594,7 +587,6 @@ var Linearization = (function LinearizationClosure() { return obj; } error('"' + name + '" field in linearization table is invalid'); - return 0; }, getHint: function linearizationGetHint(index) { var linDict = this.linDict; @@ -607,7 +599,6 @@ var Linearization = (function LinearizationClosure() { return obj2; } error('Hints table in linearization table is invalid: ' + index); - return 0; }, get length() { if (!isDict(this.linDict)) diff --git a/src/pattern.js b/src/pattern.js index dff2a5b44..80ba159e7 100644 --- a/src/pattern.js +++ b/src/pattern.js @@ -82,7 +82,7 @@ Shadings.RadialAxial = (function RadialAxialClosure() { fnObj = xref.fetchIfRef(fnObj); if (isArray(fnObj)) error('No support for array of functions'); - else if (!isPDFFunction(fnObj)) + if (!isPDFFunction(fnObj)) error('Invalid function'); var fn = PDFFunction.parse(xref, fnObj); @@ -190,7 +190,7 @@ var TilingPattern = (function TilingPatternClosure() { var MAX_PATTERN_SIZE = 512; function TilingPattern(IR, color, ctx, objs) { - var IRQueue = IR[2]; + var operatorList = IR[2]; this.matrix = IR[3]; var bbox = IR[4]; var xstep = IR[5]; @@ -222,7 +222,7 @@ var TilingPattern = (function TilingPatternClosure() { width = height = MAX_PATTERN_SIZE; } - var tmpCanvas = new ScratchCanvas(width, height); + var tmpCanvas = createScratchCanvas(width, height); // set the new canvas element context as the graphics context var tmpCtx = tmpCanvas.getContext('2d'); @@ -259,12 +259,12 @@ var TilingPattern = (function TilingPatternClosure() { graphics.endPath(); } - graphics.executeIRQueue(IRQueue); + graphics.executeOperatorList(operatorList); this.canvas = tmpCanvas; } - TilingPattern.getIR = function tiling_getIR(codeIR, dict, args) { + TilingPattern.getIR = function tiling_getIR(operatorList, dict, args) { var matrix = dict.get('Matrix'); var bbox = dict.get('BBox'); var xstep = dict.get('XStep'); @@ -272,7 +272,7 @@ var TilingPattern = (function TilingPatternClosure() { var paintType = dict.get('PaintType'); return [ - 'TilingPattern', args, codeIR, matrix, bbox, xstep, ystep, paintType + 'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep, paintType ]; }; diff --git a/src/worker.js b/src/worker.js index 3122d741e..42bd61050 100644 --- a/src/worker.js +++ b/src/worker.js @@ -79,7 +79,7 @@ MessageHandler.prototype = { var WorkerMessageHandler = { setup: function wphSetup(handler) { - var pdfDoc = null; + var pdfModel = null; handler.on('test', function wphSetupTest(data) { handler.send('test', data instanceof Uint8Array); @@ -88,7 +88,7 @@ var WorkerMessageHandler = { handler.on('doc', function wphSetupDoc(data) { // Create only the model of the PDFDoc, which is enough for // processing the content of the pdf. - pdfDoc = new PDFDocModel(new Stream(data)); + pdfModel = new PDFDocModel(new Stream(data)); }); handler.on('page_request', function wphSetupPageRequest(pageNum) { @@ -103,14 +103,14 @@ var WorkerMessageHandler = { var start = Date.now(); var dependency = []; - var IRQueue = null; + var operatorList = null; try { - var page = pdfDoc.getPage(pageNum); + var page = pdfModel.getPage(pageNum); // Pre compile the pdf page and fetch the fonts/images. - IRQueue = page.getIRQueue(handler, dependency); + operatorList = page.getOperatorList(handler, dependency); } catch (e) { var minimumStackMessage = - 'worker.js: while trying to getPage() and getIRQueue()'; + 'worker.js: while trying to getPage() and getOperatorList()'; // Turn the error into an obj that can be serialized if (typeof e === 'string') { @@ -137,8 +137,8 @@ var WorkerMessageHandler = { return; } - console.log('page=%d - getIRQueue: time=%dms, len=%d', pageNum, - Date.now() - start, IRQueue.fnArray.length); + console.log('page=%d - getOperatorList: time=%dms, len=%d', pageNum, + Date.now() - start, operatorList.fnArray.length); // Filter the dependecies for fonts. var fonts = {}; @@ -151,59 +151,10 @@ var WorkerMessageHandler = { handler.send('page', { pageNum: pageNum, - IRQueue: IRQueue, + operatorList: operatorList, depFonts: Object.keys(fonts) }); }, this); - - handler.on('font', function wphSetupFont(data) { - var objId = data[0]; - var name = data[1]; - var file = data[2]; - var properties = data[3]; - - var font = { - name: name, - file: file, - properties: properties - }; - - // Some fonts don't have a file, e.g. the build in ones like Arial. - if (file) { - var fontFileDict = new Dict(); - fontFileDict.map = file.dict.map; - - var fontFile = new Stream(file.bytes, file.start, - file.end - file.start, fontFileDict); - - // Check if this is a FlateStream. Otherwise just use the created - // Stream one. This makes complex_ttf_font.pdf work. - var cmf = file.bytes[0]; - if ((cmf & 0x0f) == 0x08) { - font.file = new FlateStream(fontFile); - } else { - font.file = fontFile; - } - } - - var obj = new Font(font.name, font.file, font.properties); - - var str = ''; - var objData = obj.data; - if (objData) { - var length = objData.length; - for (var j = 0; j < length; ++j) - str += String.fromCharCode(objData[j]); - } - - obj.str = str; - - // Remove the data array form the font object, as it's not needed - // anymore as we sent over the ready str. - delete obj.data; - - handler.send('font_ready', [objId, obj]); - }); } }; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index f14236860..ef1f6835a 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -20,10 +20,12 @@ !scan-bad.pdf !freeculture.pdf !pdfkit_compressed.pdf +!TAMReview.pdf !issue918.pdf !issue1249.pdf !smaskdim.pdf !type4psfunc.pdf !S2.pdf !zerowidthline.pdf +!issue1002.pdf !issue925.pdf diff --git a/test/pdfs/TAMReview.pdf b/test/pdfs/TAMReview.pdf new file mode 100644 index 000000000..dc77653c5 Binary files /dev/null and b/test/pdfs/TAMReview.pdf differ diff --git a/test/pdfs/issue1002.pdf b/test/pdfs/issue1002.pdf new file mode 100644 index 000000000..b764c8841 Binary files /dev/null and b/test/pdfs/issue1002.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 16d924151..c3098da81 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -472,6 +472,14 @@ "rounds": 1, "type": "eq" }, + { "id": "tamreview", + "file": "pdfs/TAMReview.pdf", + "md5": "8039aba56790d3597d2bc8c794a51301", + "rounds": 1, + "pageLimit": 5, + "link": true, + "type": "eq" + }, { "id": "issue925", "file": "pdfs/issue925.pdf", "md5": "f58fe943090aff89dcc8e771bc0db4c2", @@ -500,6 +508,13 @@ "link": true, "type": "eq" }, + { "id": "issue1002", + "file": "pdfs/issue1002.pdf", + "md5": "af62d6cd95079322d4af18edd960d15c", + "rounds": 1, + "link": false, + "type": "eq" + }, { "id": "issue1243", "file": "pdfs/issue1243.pdf", "md5": "130c849b83513d5ac5e03c6421fc7489", diff --git a/test/unit/font_spec.js b/test/unit/font_spec.js new file mode 100644 index 000000000..9f0969324 --- /dev/null +++ b/test/unit/font_spec.js @@ -0,0 +1,223 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('font', function() { + function hexDump(bytes) { + var line = ''; + for (var i = 0, ii = bytes.length; i < ii; ++i) { + var b = bytes[i].toString(16); + if (b.length < 2) + b = '0' + b; + line += b.toString(16); + } + return line; + } + // This example font comes from the CFF spec: + // http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5176.CFF.pdf + var exampleFont = '0100040100010101134142434445462b' + + '54696d65732d526f6d616e000101011f' + + 'f81b00f81c02f81d03f819041c6f000d' + + 'fb3cfb6efa7cfa1605e911b8f1120003' + + '01010813183030312e30303754696d65' + + '7320526f6d616e54696d657300000002' + + '010102030e0e7d99f92a99fb7695f773' + + '8b06f79a93fc7c8c077d99f85695f75e' + + '9908fb6e8cf87393f7108b09a70adf0b' + + 'f78e14'; + var fontData = []; + for (var i = 0; i < exampleFont.length; i += 2) { + var hex = exampleFont.substr(i, 2); + fontData.push(parseInt(hex, 16)); + } + var bytes = new Uint8Array(fontData); + fontData = {getBytes: function() { return bytes}}; + + function bytesToString(bytesArray) { + var str = ''; + for (var i = 0, ii = bytesArray.length; i < ii; i++) + str += String.fromCharCode(bytesArray[i]); + return str; + } + + describe('CFFParser', function() { + var parser = new CFFParser(fontData); + var cff = parser.parse(); + + it('parses header', function() { + var header = cff.header; + expect(header.major).toEqual(1); + expect(header.minor).toEqual(0); + expect(header.hdrSize).toEqual(4); + expect(header.offSize).toEqual(1); + }); + + it('parses name index', function() { + var names = cff.names; + expect(names.length).toEqual(1); + expect(names[0]).toEqual('ABCDEF+Times-Roman'); + }); + + it('sanitizes name index', function() { + var index = new CFFIndex(); + index.add(['['.charCodeAt(0), 'a'.charCodeAt(0)]); + + var names = parser.parseNameIndex(index); + expect(names).toEqual(['_a']); + + index = new CFFIndex(); + var longName = []; + for (var i = 0; i < 129; i++) + longName.push(0); + index.add(longName); + names = parser.parseNameIndex(index); + expect(names[0].length).toEqual(127); + }); + + it('parses string index', function() { + var strings = cff.strings; + expect(strings.count).toEqual(3); + expect(strings.get(0)).toEqual('.notdef'); + expect(strings.get(391)).toEqual('001.007'); + }); + + it('parses top dict', function() { + var topDict = cff.topDict; + // 391 version 392 FullName 393 FamilyName 389 Weight 28416 UniqueID + // -168 -218 1000 898 FontBBox 94 CharStrings 45 102 Private + expect(topDict.getByName('version')).toEqual(391); + expect(topDict.getByName('FullName')).toEqual(392); + expect(topDict.getByName('FamilyName')).toEqual(393); + expect(topDict.getByName('Weight')).toEqual(389); + expect(topDict.getByName('UniqueID')).toEqual(28416); + expect(topDict.getByName('FontBBox')).toEqual([-168, -218, 1000, 898]); + expect(topDict.getByName('CharStrings')).toEqual(94); + expect(topDict.getByName('Private')).toEqual([45, 102]); + }); + + it('parses predefined charsets', function() { + var charset = parser.parseCharsets(0, 0, null, true); + expect(charset.predefined).toEqual(true); + }); + + it('parses charset format 0', function() { + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x00, // format + 0x00, 0x02 // sid/cid + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset[1]).toEqual('exclam'); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset[1]).toEqual(2); + }); + + it('parses charset format 1', function() { + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x01, // format + 0x00, 0x08, // sid/cid start + 0x01 // sid/cid left + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset).toEqual(['.notdef', 'quoteright', 'parenleft']); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset).toEqual(['.notdef', 8, 9]); + }); + + it('parses charset format 2', function() { + // format 2 is the same as format 1 but the left is card16 + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x02, // format + 0x00, 0x08, // sid/cid start + 0x00, 0x01 // sid/cid left + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset).toEqual(['.notdef', 'quoteright', 'parenleft']); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset).toEqual(['.notdef', 8, 9]); + }); + + it('parses encoding format 0', function() { + // The first two bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, + 0x00, // format + 0x01, // count + 0x08 // start + ]); + parser.bytes = bytes; + var encoding = parser.parseEncoding(2, {}, new CFFStrings(), null); + expect(encoding.encoding).toEqual({0x8: 1}); + }); + + it('parses encoding format 1', function() { + // The first two bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, + 0x01, // format + 0x01, // num ranges + 0x07, // range1 start + 0x01 // range2 left + ]); + parser.bytes = bytes; + var encoding = parser.parseEncoding(2, {}, new CFFStrings(), null); + expect(encoding.encoding).toEqual({0x7: 0x01, 0x08: 0x02}); + }); + + it('parses fdselect format 0', function() { + var bytes = new Uint8Array([0x00, // format + 0x00, // gid: 0 fd: 0 + 0x01 // gid: 1 fd: 1 + ]); + parser.bytes = bytes; + var fdSelect = parser.parseFDSelect(0, 2); + expect(fdSelect.fdSelect).toEqual([0, 1]); + }); + + it('parses fdselect format 3', function() { + var bytes = new Uint8Array([0x03, // format + 0x00, 0x02, // range count + 0x00, 0x00, // first gid + 0x09, // font dict 1 id + 0x00, 0x02, // nex gid + 0x0a, // font dict 2 gid + 0x00, 0x04 // sentinel (last gid) + ]); + parser.bytes = bytes; + var fdSelect = parser.parseFDSelect(0, 2); + expect(fdSelect.fdSelect).toEqual([9, 9, 0xa, 0xa]); + }); + // TODO fdArray + }); + describe('CFFCompiler', function() { + it('encodes integers', function() { + var c = new CFFCompiler(); + // all the examples from the spec + expect(c.encodeInteger(0)).toEqual([0x8b]); + expect(c.encodeInteger(100)).toEqual([0xef]); + expect(c.encodeInteger(-100)).toEqual([0x27]); + expect(c.encodeInteger(1000)).toEqual([0xfa, 0x7c]); + expect(c.encodeInteger(-1000)).toEqual([0xfe, 0x7c]); + expect(c.encodeInteger(10000)).toEqual([0x1c, 0x27, 0x10]); + expect(c.encodeInteger(-10000)).toEqual([0x1c, 0xd8, 0xf0]); + expect(c.encodeInteger(100000)).toEqual([0x1d, 0x00, 0x01, 0x86, 0xa0]); + expect(c.encodeInteger(-100000)).toEqual([0x1d, 0xff, 0xfe, 0x79, 0x60]); + }); + it('encodes floats', function() { + var c = new CFFCompiler(); + expect(c.encodeFloat(-2.25)).toEqual([0x1e, 0xe2, 0xa2, 0x5f]); + expect(c.encodeFloat(5e-11)).toEqual([0x1e, 0x5c, 0x11, 0xff]); + }); + // TODO a lot more compiler tests + }); +}); diff --git a/test/unit/jsTestDriver.conf b/test/unit/jsTestDriver.conf index b2d3e86f1..9a26df6a4 100644 --- a/test/unit/jsTestDriver.conf +++ b/test/unit/jsTestDriver.conf @@ -25,6 +25,7 @@ load: - ../../src/bidi.js - ../../external/jpgjs/jpg.js - ../unit/obj_spec.js + - ../unit/font_spec.js - ../unit/function_spec.js - ../unit/crypto_spec.js - ../unit/stream_spec.js