From: Vivien Nicolas <21@vingtetun.org> Date: Thu, 16 Jun 2011 01:05:55 +0000 (+0200) Subject: Merge from gal's master branch (got a regression on the mapping between char->glyph) X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=a57b53b3b3c58f79ad3d0d786910f2ff73a65bf3;p=pdf.js.git Merge from gal's master branch (got a regression on the mapping between char->glyph) --- a57b53b3b3c58f79ad3d0d786910f2ff73a65bf3 diff --cc PDFFont.js index 8c3abc7,0000000..d106e0b mode 100644,000000..100644 --- a/PDFFont.js +++ b/PDFFont.js @@@ -1,1551 -1,0 +1,1551 @@@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +/** + * Maximum file size of the font. + */ +var kMaxFontFileSize = 40000; + +/** + * Maximum number of glyphs per font. +*/ +var kMaxGlyphsCount = 65526; + +/** + * Maximum time to wait for a font to be loaded by @font-face + */ +var kMaxWaitForFontFace = 1000; + + /* + * Useful for debugging when you want to certains operations depending on how + * many fonts are loaded. + */ +var fontCount = 0; + +/** + * Hold a map of decoded fonts and of the standard fourteen Type1 fonts and + * their acronyms. + * TODO Add the standard fourteen Type1 fonts list by default + * http://cgit.freedesktop.org/poppler/poppler/tree/poppler/GfxFont.cc#n65 + */ +var Fonts = { + _active: null, + get active() { + return this._active || { encoding: {} }; + }, + - set active(aFontName) { - this._active = this[aFontName]; ++ set active(aName) { ++ this._active = this[aName]; + }, + + unicodeFromCode: function fonts_unicodeFromCode(aCode) { + var unicode = GlyphsUnicode[this.active.encoding[aCode]]; + return unicode ? "0x" + unicode : aCode; + } +}; + +/** + * 'Font' is the class the outside world should use, it encapsulate all the font + * decoding logics whatever type it is (assuming the font type is supported). + * + * For example to read a Type1 font and to attach it to the document: + * var type1Font = new Font("MyFontName", binaryData, aFontEncoding, "Type1"); + * type1Font.bind(); + * + * As an improvment the last parameter can be replaced by an automatic guess + * of the font type based on the first byte of the file. + * + * XXX There is now too many parameters, this should be turned into an + * object containing all the required informations about the font + */ +var Font = function(aName, aFile, aEncoding, aCharset, aBBox, aType) { + this.name = aName; + + // If the font has already been decoded simply return + if (Fonts[aName]) { + this.font = Fonts[aName].data; + return; + } + fontCount++; + + var start = Date.now(); + switch (aType) { + case "Type1": + var cff = new CFF(aName, aBBox, aFile); + this.mimetype = "font/otf"; + + // Wrap the CFF data inside an OTF font file + this.font = this.cover(cff); + break; + + case "TrueType": + return Fonts[aName] = { + data: null, + encoding: {}, + charset: null, + loading: false + }; + + // TrueType is disabled for the moment since the sanitizer prevent it + // from loading + this.mimetype = "font/ttf"; + var ttf = new TrueType(aFile); + this.font = ttf.data; + break; + + default: + warn("Font " + aType + " is not supported"); + break; + } + var end = Date.now(); + + Fonts[aName] = { + data: this.font, + encoding: aEncoding, + charset: aCharset ? aCharset.slice() : null, + loading: true + } + + // Attach the font to the document + this.bind(); +}; + +Font.prototype = { + name: null, + font: null, + mimetype: null, + + bind: function font_bind() { + var data = this.font; + + // Compute the binary data to base 64 + var str = []; + var count = data.length; + for (var i = 0; i < count; i++) + str.push(data.getChar ? data.getChar() + : String.fromCharCode(data[i])); + + var dataBase64 = window.btoa(str.join("")); + var fontName = this.name; + + /** Hack begin */ + + // Actually there is not event when a font has finished downloading so + // the following tons of code are a dirty hack to 'guess' when a font is + // ready + var debug = false; + + if (debug) { + var name = document.createElement("font"); + name.setAttribute("style", "position: absolute; left: 20px; top: " + + (100 * fontCount + 60) + "px"); + name.innerHTML = fontName; + document.body.appendChild(name); + } + + var canvas = document.createElement("canvas"); + var style = "border: 1px solid black; position:absolute; top: " + + (debug ? (100 * fontCount) : "-200") + "px; left: 2px; width: 340px; height: 100px"; + canvas.setAttribute("style", style); + canvas.setAttribute("width", 340); + canvas.setAttribute("heigth", 100); + document.body.appendChild(canvas); + + // Retrieve font charset + var charset = Fonts[fontName].charset || []; + // if the charset is too small make it repeat a few times + var count = 30; + while (count-- && charset.length <= 30) + charset = charset.concat(charset.slice()); + + // Get the font size canvas think it will be for 'spaces' + var ctx = canvas.getContext("2d"); + var testString = " "; + + // When debugging use the characters provided by the charsets to visually + // see what's happening + if (debug) { + for (var i = 0; i < charset.length; i++) { + var unicode = new Number("0x" + GlyphsUnicode[charset[i]]); + if (!unicode) + error("Unicode for " + charset[i] + " is has not been found in the glyphs list"); + testString += String.fromCharCode(unicode); + } + } + ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; + var textWidth = ctx.mozMeasureText(testString); + + if (debug) + ctx.fillText(testString, 20, 20); + + var start = Date.now(); + var interval = window.setInterval(function(self) { + ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; + + // For some reasons the font has not loaded, so mark it loaded for the + // page to proceed but cry + if ((Date.now() - start) >= kMaxWaitForFontFace) { + window.clearInterval(interval); + Fonts[fontName].loading = false; + warn("Is " + fontName + " for charset: " + charset + " loaded?"); + } else if (textWidth != ctx.mozMeasureText(testString)) { + window.clearInterval(interval); + Fonts[fontName].loading = false; + } + + if (debug) + ctx.fillText(testString, 20, 50); + }, 20, this); + + /** Hack end */ + + // Add the @font-face rule to the document + var url = "url(data:" + this.mimetype + ";base64," + dataBase64 + ");"; + var rule = "@font-face { font-family:'" + fontName + "';src:" + url + "}"; + var styleSheet = document.styleSheets[0]; + styleSheet.insertRule(rule, styleSheet.length); + }, + + _createOpenTypeHeader: function font_createOpenTypeHeader(aFile, aOffsets, aNumTables) { + // sfnt version (4 bytes) + var version = [0x4F, 0x54, 0x54, 0X4F]; + + // numTables (2 bytes) + var numTables = aNumTables; + + // searchRange (2 bytes) + var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); + var searchRange = tablesMaxPower2 * 16; + + // entrySelector (2 bytes) + var entrySelector = Math.log(tablesMaxPower2) / Math.log(2); + + // rangeShift (2 bytes) + var rangeShift = numTables * 16 - searchRange; + + var header = [].concat(version, + FontsUtils.integerToBytes(numTables, 2), + FontsUtils.integerToBytes(searchRange, 2), + FontsUtils.integerToBytes(entrySelector, 2), + FontsUtils.integerToBytes(rangeShift, 2)); + aFile.set(header, aOffsets.currentOffset); + aOffsets.currentOffset += header.length; + aOffsets.virtualOffset += header.length; + }, + + _createTableEntry: function font_createTableEntry(aFile, aOffsets, aTag, aData) { + // tag + var tag = [ + aTag.charCodeAt(0), + aTag.charCodeAt(1), + aTag.charCodeAt(2), + aTag.charCodeAt(3) + ]; + + // offset + var offset = aOffsets.virtualOffset; + + // Per spec tables must be 4-bytes align so add some 0x00 if needed + while (aData.length & 3) + aData.push(0x00); + + // length + var length = aData.length; + + // checksum + var checksum = FontsUtils.bytesToInteger(tag) + offset + length; + + var tableEntry = [].concat(tag, + FontsUtils.integerToBytes(checksum, 4), + FontsUtils.integerToBytes(offset, 4), + FontsUtils.integerToBytes(length, 4)); + aFile.set(tableEntry, aOffsets.currentOffset); + aOffsets.currentOffset += tableEntry.length; + aOffsets.virtualOffset += aData.length; + }, + + _createCMAPTable: function font_createCMAPTable(aGlyphs) { + var characters = new Array(kMaxGlyphsCount); + for (var i = 0; i < aGlyphs.length; i++) { + characters[aGlyphs[i].unicode] = i + 1; + } + + // Separate the glyphs into continuous range of codes, aka segment. + var ranges = []; + var range = []; + for (var i = 0; i < characters.length; i++) { + if (characters[i]) { + range.push(i); + } else if (range.length) { + ranges.push(range.slice()); + range = []; + } + } + + // The size in bytes of the header is equal to the size of the + // different fields * length of a short + (size of the 4 parallels arrays + // describing segments * length of a short). + var headerSize = (12 * 2 + (ranges.length * 4 * 2)); + + var segCount = ranges.length + 1; + var segCount2 = segCount * 2; + var searchRange = FontsUtils.getMaxPower2(segCount) * 2; + var searchEntry = Math.log(segCount) / Math.log(2); + var rangeShift = 2 * segCount - searchRange; + var cmap = [].concat( + [ + 0x00, 0x00, // version + 0x00, 0x01, // numTables + 0x00, 0x03, // platformID + 0x00, 0x01, // encodingID + 0x00, 0x00, 0x00, 0x0C, // start of the table record + 0x00, 0x04 // format + ], + FontsUtils.integerToBytes(headerSize, 2), // length + [0x00, 0x00], // language + FontsUtils.integerToBytes(segCount2, 2), + FontsUtils.integerToBytes(searchRange, 2), + FontsUtils.integerToBytes(searchEntry, 2), + FontsUtils.integerToBytes(rangeShift, 2) + ); + + // Fill up the 4 parallel arrays describing the segments. + var startCount = []; + var endCount = []; + var idDeltas = []; + var idRangeOffsets = []; + var glyphsIdsArray = []; + var bias = 0; + for (var i = 0; i < segCount - 1; i++) { + var range = ranges[i]; + var start = FontsUtils.integerToBytes(range[0], 2); + var end = FontsUtils.integerToBytes(range[range.length - 1], 2); + + var delta = FontsUtils.integerToBytes(((range[0] - 1) - bias) % 65536, 2); + bias += range.length; + + // deltas are signed shorts + delta[0] ^= 0xFF; + delta[1] ^= 0xFF; + delta[1] += 1; + + startCount.push(start[0], start[1]); + endCount.push(end[0], end[1]); + idDeltas.push(delta[0], delta[1]); + idRangeOffsets.push(0x00, 0x00); + + for (var j = 0; j < range.length; j++) + glyphsIdsArray.push(range[j]); + } + startCount.push(0xFF, 0xFF); + endCount.push(0xFF, 0xFF); + idDeltas.push(0x00, 0x01); + idRangeOffsets.push(0x00, 0x00); + + return cmap.concat(endCount, [0x00, 0x00], startCount, + idDeltas, idRangeOffsets, glyphsIdsArray); + }, + + cover: function font_cover(aFont) { + var otf = new Uint8Array(kMaxFontFileSize); + + // Required Tables + var CFF = aFont.data, // PostScript Font Program + OS2 = [], // OS/2 and Windows Specific metrics + cmap = [], // Character to glyphs mapping + head = [], // Font eader + hhea = [], // Horizontal header + hmtx = [], // Horizontal metrics + maxp = [], // Maximum profile + name = [], // Naming tables + post = []; // PostScript informations + var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post]; + + // The offsets object holds at the same time a representation of where + // to write the table entry information about a table and another offset + // representing the offset where to draw the actual data of a particular + // table + var offsets = { + currentOffset: 0, + virtualOffset: tables.length * (4 * 4) + }; + + // For files with only one font the offset table is the first thing of the + // file + this._createOpenTypeHeader(otf, offsets, tables.length); + + // XXX It is probable that in a future we want to get rid of this glue + // between the CFF and the OTF format in order to be able to embed TrueType + // data. + this._createTableEntry(otf, offsets, "CFF ", CFF); + + /** OS/2 */ + OS2 = [ + 0x00, 0x03, // version + 0x02, 0x24, // xAvgCharWidth + 0x01, 0xF4, // usWeightClass + 0x00, 0x05, // usWidthClass + 0x00, 0x00, // fstype + 0x02, 0x8A, // ySubscriptXSize + 0x02, 0xBB, // ySubscriptYSize + 0x00, 0x00, // ySubscriptXOffset + 0x00, 0x8C, // ySubscriptYOffset + 0x02, 0x8A, // ySuperScriptXSize + 0x02, 0xBB, // ySuperScriptYSize + 0x00, 0x00, // ySuperScriptXOffset + 0x01, 0xDF, // ySuperScriptYOffset + 0x00, 0x31, // yStrikeOutSize + 0x01, 0x02, // yStrikeOutPosition + 0x00, 0x00, // sFamilyClass + 0x02, 0x00, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Panose + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 0-31) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 32-63) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 64-95) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 96-127) + 0x2A, 0x32, 0x31, 0x2A, // achVendID + 0x00, 0x20, // fsSelection + 0x00, 0x2D, // usFirstCharIndex + 0x00, 0x7A, // usLastCharIndex + 0x00, 0x03, // sTypoAscender + 0x00, 0x20, // sTypeDescender + 0x00, 0x38, // sTypoLineGap + 0x00, 0x5A, // usWinAscent + 0x02, 0xB4, // usWinDescent + 0x00, 0xCE, 0x00, 0x00, // ulCodePageRange1 (Bits 0-31) + 0x00, 0x01, 0x00, 0x00, // ulCodePageRange2 (Bits 32-63) + 0x00, 0x00, // sxHeight + 0x00, 0x00, // sCapHeight + 0x00, 0x01, // usDefaultChar + 0x00, 0xCD, // usBreakChar + 0x00, 0x02 // usMaxContext + ]; + this._createTableEntry(otf, offsets, "OS/2", OS2); + + //XXX Getting charstrings here seems wrong since this is another CFF glue + var charstrings = aFont.getOrderedCharStrings(aFont.glyphs); + + /** CMAP */ + cmap = this._createCMAPTable(charstrings); + this._createTableEntry(otf, offsets, "cmap", cmap); + + /** HEAD */ + head = [ + 0x00, 0x01, 0x00, 0x00, // Version number + 0x00, 0x00, 0x50, 0x00, // fontRevision + 0x00, 0x00, 0x00, 0x00, // checksumAdjustement + 0x5F, 0x0F, 0x3C, 0xF5, // magicNumber + 0x00, 0x00, // Flags + 0x03, 0xE8, // unitsPerEM (defaulting to 1000) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // creation date + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // modifification date + 0x00, 0x00, // xMin + 0x00, 0x00, // yMin + 0x00, 0x00, // xMax + 0x00, 0x00, // yMax + 0x00, 0x00, // macStyle + 0x00, 0x00, // lowestRecPPEM + 0x00, 0x00, // fontDirectionHint + 0x00, 0x00, // indexToLocFormat + 0x00, 0x00 // glyphDataFormat + ]; + this._createTableEntry(otf, offsets, "head", head); + + /** HHEA */ + hhea = [].concat( + [ + 0x00, 0x01, 0x00, 0x00, // Version number + 0x00, 0x00, // Typographic Ascent + 0x00, 0x00, // Typographic Descent + 0x00, 0x00, // Line Gap + 0xFF, 0xFF, // advanceWidthMax + 0x00, 0x00, // minLeftSidebearing + 0x00, 0x00, // minRightSidebearing + 0x00, 0x00, // xMaxExtent + 0x00, 0x00, // caretSlopeRise + 0x00, 0x00, // caretSlopeRun + 0x00, 0x00, // caretOffset + 0x00, 0x00, // -reserved- + 0x00, 0x00, // -reserved- + 0x00, 0x00, // -reserved- + 0x00, 0x00, // -reserved- + 0x00, 0x00 // metricDataFormat + ], + FontsUtils.integerToBytes(charstrings.length, 2) // numberOfHMetrics + ); + this._createTableEntry(otf, offsets, "hhea", hhea); + + /** HMTX */ + hmtx = [0x01, 0xF4, 0x00, 0x00]; + for (var i = 0; i < charstrings.length; i++) { + // XXX this can easily broke + var charstring = charstrings[i].charstring; + var width = FontsUtils.integerToBytes(charstring[1], 2); + var lsb = FontsUtils.integerToBytes(charstring[0], 2); + hmtx = hmtx.concat(width, lsb); + } + this._createTableEntry(otf, offsets, "hmtx", hmtx); + + /** MAXP */ + maxp = [].concat( + [ + 0x00, 0x00, 0x50, 0x00, // Version number + ], + FontsUtils.integerToBytes(charstrings.length + 1, 2) // Num of glyphs (+1 to pass the sanitizer...) + ); + this._createTableEntry(otf, offsets, "maxp", maxp); + + /** NAME */ + name = [ + 0x00, 0x00, // format + 0x00, 0x00, // Number of names Record + 0x00, 0x00 // Storage + ]; + this._createTableEntry(otf, offsets, "name", name); + + /** POST */ + // XXX get those info from the Font dict! + post = [ + 0x00, 0x03, 0x00, 0x00, // Version number + 0x00, 0x00, 0x01, 0x00, // italicAngle + 0x00, 0x00, // underlinePosition + 0x00, 0x00, // underlineThickness + 0x00, 0x00, 0x00, 0x00, // isFixedPitch + 0x00, 0x00, 0x00, 0x00, // minMemType42 + 0x00, 0x00, 0x00, 0x00, // maxMemType42 + 0x00, 0x00, 0x00, 0x00, // minMemType1 + 0x00, 0x00, 0x00, 0x00 // maxMemType1 + ]; + this._createTableEntry(otf, offsets, "post", post); + + // Once all the table entries header are written, dump the data! + var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post]; + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + otf.set(table, offsets.currentOffset); + offsets.currentOffset += table.length; + } + + var fontData = []; + for (var i = 0; i < offsets.currentOffset; i++) + fontData.push(otf[i]); + + //writeToFile(fontData, "/tmp/pdf.js." + fontCount + ".otf"); + return fontData; + } +}; + + +var FontsUtils = { + integerToBytes: function fu_integerToBytes(aValue, aBytesCount) { + var bytes = []; + for (var i = 0; i < aBytesCount; i++) + bytes[i] = 0x00; + + do { + bytes[--aBytesCount] = (aValue & 0xFF); + aValue = aValue >> 8; + } while (aBytesCount && aValue > 0); + + return bytes; + }, + + bytesToInteger: function(aBytesArray) { + var value = 0; + for (var i = 0; i < aBytesArray.length; i++) + value = (value << 8) + aBytesArray[i]; + return value; + }, + + getMaxPower2: function fu_getMaxPower2(aNumber) { + var maxPower = 0; + var value = aNumber; + while (value >= 2) { + value /= 2; + maxPower++; + } + + value = 2; + for (var i = 1; i < maxPower; i++) + value *= 2; + return value; + } +}; + + +/** Implementation dirty logic starts here */ + +/** + * At the moment TrueType is just a stub that does mostly nothing but in a + * (near?) future this class will rewrite the font to ensure it is well formed + * and valid in the point of view of the sanitizer. + */ +var TrueType = function(aFile) { + var header = this._readOpenTypeHeader(aFile); + var numTables = header.numTables; + + // Check that required tables are present + var requiredTables = [ + "OS/2", + "cmap", + "head", + "hhea", + "hmtx", + "maxp", + "name", + "post" + ]; + + var tables = []; + for (var i = 0; i < numTables; i++) { + var table = this._readTableEntry(aFile); + var index = requiredTables.indexOf(table.tag); + if (index != -1) + requiredTables.splice(index, 1); + + tables.push(table); + } + tables.sort(function(a, b) { + return a.tag > b.tag; + }); + + // If any tables are still in the array this means some required tables are + // missing, which means that we need to rebuild the font in order to pass + // the sanitizer. + if (requiredTables.length && requiredTables[0] == "OS/2") { + OS2 = [ + 0x00, 0x03, // version + 0x02, 0x24, // xAvgCharWidth + 0x01, 0xF4, // usWeightClass + 0x00, 0x05, // usWidthClass + 0x00, 0x00, // fstype + 0x02, 0x8A, // ySubscriptXSize + 0x02, 0xBB, // ySubscriptYSize + 0x00, 0x00, // ySubscriptXOffset + 0x00, 0x8C, // ySubscriptYOffset + 0x02, 0x8A, // ySuperScriptXSize + 0x02, 0xBB, // ySuperScriptYSize + 0x00, 0x00, // ySuperScriptXOffset + 0x01, 0xDF, // ySuperScriptYOffset + 0x00, 0x31, // yStrikeOutSize + 0x01, 0x02, // yStrikeOutPosition + 0x00, 0x00, // sFamilyClass + 0x02, 0x00, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Panose + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 0-31) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 32-63) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 64-95) + 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 96-127) + 0x2A, 0x32, 0x31, 0x2A, // achVendID + 0x00, 0x20, // fsSelection + 0x00, 0x2D, // usFirstCharIndex + 0x00, 0x7A, // usLastCharIndex + 0x00, 0x03, // sTypoAscender + 0x00, 0x20, // sTypeDescender + 0x00, 0x38, // sTypoLineGap + 0x00, 0x5A, // usWinAscent + 0x02, 0xB4, // usWinDescent + 0x00, 0xCE, 0x00, 0x00, // ulCodePageRange1 (Bits 0-31) + 0x00, 0x01, 0x00, 0x00, // ulCodePageRange2 (Bits 32-63) + 0x00, 0x00, // sxHeight + 0x00, 0x00, // sCapHeight + 0x00, 0x01, // usDefaultChar + 0x00, 0xCD, // usBreakChar + 0x00, 0x02 // usMaxContext + ]; + + // Create a new file to hold the new version of our truetype with a new + // header and new offsets + var stream = aFile.stream || aFile; + var ttf = new Uint8Array(stream.length + 16 + OS2.length); + + // The new numbers of tables will be the last one plus the num of missing + // tables + var numTables = header.numTables + 1; + + // The offsets object holds at the same time a representation of where + // to write the table entry information about a table and another offset + // representing the offset where to draw the actual data of a particular + // table + var offsets = { + currentOffset: 0, + virtualOffset: numTables * (4 * 4) + }; + + // Write the sfnt header with one more table + this._createOpenTypeHeader(ttf, offsets, numTables); + + // Insert the missing table + tables.unshift({ + tag: "OS/2", + data: OS2 + }); + + // rewrite the tables but tweak offsets + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + var data = []; + + var tableData = table.data; + for (var j = 0; j < tableData.length; j++) + data.push(tableData[j]); + this._createTableEntry(ttf, offsets, table.tag, data); + } + + // Add the table datas + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + var tableData = table.data; + ttf.set(tableData, offsets.currentOffset); + offsets.currentOffset += tableData.length; + + if (0) { + var data = []; + for (var j = 0; j < tableData.length; j++) + d.push(tableData[j]); + log("data for table: " + table.tag + ": " + data); + } + + // 4-byte aligned data + while (offsets.currentOffset & 3) + offsets.currentOffset++; + } + + var fontData = []; + for (var i = 0; i < ttf.length; i++) + fontData.push(ttf[i]); + + this.data = ttf; + //writeToFile(fontData, "/tmp/pdf.js." + fontCount + ".ttf"); + return; + } else if (requiredTables.lenght) { + error("Table " + requiredTables[0] + " is missing from the TruType font"); + } else { + this.data = aFile; + } +}; + +TrueType.prototype = { + _createOpenTypeHeader: function tt_createOpenTypeHeader(aFile, aOffsets, aNumTables) { + // sfnt version (4 bytes) + // XXX if we want to merge this function and the one from the Font class + // XXX this need to be adapted + var version = [0x00, 0x01, 0x00, 0X00]; + + // numTables (2 bytes) + var numTables = aNumTables; + + // searchRange (2 bytes) + var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); + var searchRange = tablesMaxPower2 * 16; + + // entrySelector (2 bytes) + var entrySelector = Math.log(tablesMaxPower2) / Math.log(2); + + // rangeShift (2 bytes) + var rangeShift = numTables * 16 - searchRange; + + var header = [].concat(version, + FontsUtils.integerToBytes(numTables, 2), + FontsUtils.integerToBytes(searchRange, 2), + FontsUtils.integerToBytes(entrySelector, 2), + FontsUtils.integerToBytes(rangeShift, 2)); + aFile.set(header, aOffsets.currentOffset); + aOffsets.currentOffset += header.length; + aOffsets.virtualOffset += header.length; + }, + + _createTableEntry: function font_createTableEntry(aFile, aOffsets, aTag, aData) { + // tag + var tag = [ + aTag.charCodeAt(0), + aTag.charCodeAt(1), + aTag.charCodeAt(2), + aTag.charCodeAt(3) + ]; + + // Per spec tables must be 4-bytes align so add some 0x00 if needed + while (aData.length & 3) + aData.push(0x00); + + while (aOffsets.virtualOffset & 3) + aOffsets.virtualOffset++; + + // offset + var offset = aOffsets.virtualOffset; + + // length + var length = aData.length; + + // checksum + var checksum = FontsUtils.bytesToInteger(tag) + offset + length; + + var tableEntry = [].concat(tag, + FontsUtils.integerToBytes(checksum, 4), + FontsUtils.integerToBytes(offset, 4), + FontsUtils.integerToBytes(length, 4)); + aFile.set(tableEntry, aOffsets.currentOffset); + aOffsets.currentOffset += tableEntry.length; + aOffsets.virtualOffset += aData.length; + }, + + _readOpenTypeHeader: function tt_readOpenTypeHeader(aFile) { + return { + version: aFile.getBytes(4), + numTables: FontsUtils.bytesToInteger(aFile.getBytes(2)), + searchRange: FontsUtils.bytesToInteger(aFile.getBytes(2)), + entrySelector: FontsUtils.bytesToInteger(aFile.getBytes(2)), + rangeShift: FontsUtils.bytesToInteger(aFile.getBytes(2)) + } + }, + + _readTableEntry: function tt_readTableEntry(aFile) { + // tag + var tag = aFile.getBytes(4); + tag = String.fromCharCode(tag[0]) + + String.fromCharCode(tag[1]) + + String.fromCharCode(tag[2]) + + String.fromCharCode(tag[3]); + + var checksum = FontsUtils.bytesToInteger(aFile.getBytes(4)); + var offset = FontsUtils.bytesToInteger(aFile.getBytes(4)); + var length = FontsUtils.bytesToInteger(aFile.getBytes(4)); + + // Read the table associated data + var currentPosition = aFile.pos; + aFile.pos = aFile.start + offset; + var data = aFile.getBytes(length); + aFile.pos = currentPosition; + + return { + tag: tag, + checksum: checksum, + length: offset, + offset: length, + data: data + } + } +}; + +/** + * This dictionary holds decoded fonts data. + */ +var PSFonts = new Dict(); + +var Stack = function(aStackSize) { + var innerStack = new Array(aStackSize || 0); + + this.push = function(aOperand) { + innerStack.push(aOperand); + }; + + this.pop = function() { + if (!this.count()) + throw new Error("stackunderflow"); + return innerStack.pop(); + }; + + this.peek = function() { + if (!this.count()) + return null; + return innerStack[innerStack.length - 1]; + }; + + this.get = function(aIndex) { + return innerStack[aIndex]; + }; + + this.clear = function() { + innerStack = []; + }; + + this.count = function() { + return innerStack.length; + }; + + this.dump = function() { + for (var i = 0; i < this.length; i++) + log(innerStack[i]); + }; + + this.clone = function() { + return innerStack.slice(); + }; +}; + +var Type1Parser = function() { + // Turn on this flag for additional debugging logs + var debug = false; + + var dump = function(aData) { + if (debug) + log(aData); + }; + + /* + * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence + * of Plaintext Bytes. The function took a key as a parameter which can be + * for decrypting the eexec block of for decoding charStrings. + */ + var kEexecEncryptionKey = 55665; + var kCharStringsEncryptionKey = 4330; + + function decrypt(aStream, aKey, aDiscardNumber, aByteArray) { + var start = Date.now(); + var r = aKey, c1 = 52845, c2 = 22719; + var decryptedString = []; + + var value = ""; + var count = aStream.length; + for (var i = 0; i < count; i++) { + value = aStream.getByte(); + if (aByteArray) + decryptedString[i] = value ^ (r >> 8); + else + decryptedString[i] = String.fromCharCode(value ^ (r >> 8)); + r = ((value + r) * c1 + c2) & ((1 << 16) - 1); + } + var end = Date.now(); + dump("Time to decrypt string of length " + count + " is " + (end - start)); + return decryptedString.slice(aDiscardNumber); + }; + + /* + * CharStrings are encoded following the the CharString Encoding sequence + * describe in Chapter 6 of the "Adobe Type1 Font Format" specification. + * The value in a byte indicates a command, a number, or subsequent bytes + * that are to be interpreted in a special way. + * + * CharString Number Encoding: + * A CharString byte containing the values from 32 through 255 inclusive + * indicate an integer. These values are decoded in four ranges. + * + * 1. A CharString byte containing a value, v, between 32 and 246 inclusive, + * indicate the integer v - 139. Thus, the integer values from -107 through + * 107 inclusive may be encoded in single byte. + * + * 2. A CharString byte containing a value, v, between 247 and 250 inclusive, + * indicates an integer involving the next byte, w, according to the formula: + * [(v - 247) x 256] + w + 108 + * + * 3. A CharString byte containing a value, v, between 251 and 254 inclusive, + * indicates an integer involving the next byte, w, according to the formula: + * -[(v - 251) * 256] - w - 108 + * + * 4. A CharString containing the value 255 indicates that the next 4 bytes + * are a two complement signed integer. The first of these bytes contains the + * highest order bits, the second byte contains the next higher order bits + * and the fourth byte contain the lowest order bits. + * + * + * CharString Command Encoding: + * CharStrings commands are encoded in 1 or 2 bytes. + * + * Single byte commands are encoded in 1 byte that contains a value between + * 0 and 31 inclusive. + * If a command byte contains the value 12, then the value in the next byte + * indicates a command. This "escape" mechanism allows many extra commands + * to be encoded and this encoding technique helps to minimize the length of + * the charStrings. + */ + var charStringDictionary = { + "1": "hstem", + "3": "vstem", + "4": "vmoveto", + "5": "rlineto", + "6": "hlineto", + "7": "vlineto", + "8": "rrcurveto", + "9": "closepath", + "10": "callsubr", + "11": "return", + "12": { + "0": "dotsection", + "1": "vstem3", + "3": "hstem3", + "6": "seac", + "7": "sbw", + "12": "div", + "16": "callothersubr", + "17": "pop", + "33": "setcurrentpoint" + }, + "13": "hsbw", + "14": "endchar", + "21": "rmoveto", + "22": "hmoveto", + "30": "vhcurveto", + "31": "hvcurveto" + }; + + function decodeCharString(aStream) { + var start = Date.now(); + var charString = []; + + var value = ""; + var count = aStream.length; + for (var i = 0; i < count; i++) { + value = aStream.getByte(); + + if (value < 32) { + if (value == 12) { + value = charStringDictionary["12"][aStream.getByte()]; + i++; + } else { + value = charStringDictionary[value]; + } + } else if (value <= 246) { + value = parseInt(value) - 139; + } else if (value <= 250) { + value = ((value - 247) * 256) + parseInt(aStream.getByte()) + 108; + i++; + } else if (value <= 254) { + value = -((value - 251) * 256) - parseInt(aStream.getByte()) - 108; + i++; + } else { + var byte = aStream.getByte(); + var high = (byte >> 1); + value = (byte - high) << 24 | aStream.getByte() << 16 | + aStream.getByte() << 8 | aStream.getByte(); + i += 4; + } + + charString.push(value); + } + + var end = Date.now(); + dump("Time to decode charString of length " + count + " is " + (end - start)); + return charString; + }; + + /* + * The operand stack holds arbitrary PostScript objects that are the operands + * and results of PostScript operators being executed. The interpreter pushes + * objects on the operand stack when it encounters them as literal data in a + * program being executed. When an operator requires one or more operands, it + * obtains them by popping them off the top of the operand stack. When an + * operator returns one or more results, it does so by pushing them on the + * operand stack. + */ + var operandStack = new Stack(40); + + // Flag indicating if the topmost operand of the operandStack is an array + var operandIsArray = 0; + + /* + * The dictionary stack holds only dictionary objects. The current set of + * dictionaries on the dictionary stack defines the environment for all + * implicit name searches, such as those that occur when the interpreter + * encounters an executable name. The role of the dictionary stack is + * introduced in Section 3.3, “Data Types and Objects,” and is further + * explained in Section 3.5, “Execution.” of the PostScript Language + * Reference. + */ + var systemDict = new Dict(), + globalDict = new Dict(), + userDict = new Dict(); + + var dictionaryStack = new Stack(); + dictionaryStack.push(systemDict); + dictionaryStack.push(globalDict); + dictionaryStack.push(userDict); + + /* + * The execution stack holds executable objects (mainly procedures and files) + * that are in intermediate stages of execution. At any point in the + * execution of a PostScript program, this stack represents the program’s + * call stack. Whenever the interpreter suspends execution of an object to + * execute some other object, it pushes the new object on the execution + * stack. When the interpreter finishes executing an object, it pops that + * object off the execution stack and resumes executing the suspended object + * beneath it. + */ + var executionStack = new Stack(); + + /* + * Return the next token in the execution stack + */ + function nextInStack() { + var currentProcedure = executionStack.peek(); + var command = currentProcedure.shift(); + if (!currentProcedure.length) + executionStack.pop(); + return command; + }; + + /** + * Returns an object containing a Subrs array and a CharStrings array + * extracted from and eexec encrypted block of data + */ + this.extractFontInfo = function(aStream) { + var eexecString = decrypt(new Stream(aStream), kEexecEncryptionKey, 4, true); + var subrs = [], glyphs = []; + var inSubrs = inGlyphs = false; + var glyph = ""; + + var token = ""; + var index = 0; + var length = 0; + + var count = eexecString.length; + var c = ""; + for (var i = 0; i < count; i++) { + var c = eexecString[i]; + + if (inSubrs && c == 0x52) { + length = parseInt(length); + var stream = new Stream(eexecString.slice(i + 3, i + 3 + length)); + var encodedSubr = decrypt(stream, kCharStringsEncryptionKey, 4).join(""); + var subr = decodeCharString(new StringStream(encodedSubr)); + + subrs.push(subr); + i += 3 + length; + } else if (inGlyphs && c == 0x52) { + length = parseInt(length); + var stream = new Stream(eexecString.slice(i + 3, i + 3 + length)); + var encodedCharstring = decrypt(stream, kCharStringsEncryptionKey, 4).join(""); + var subr = decodeCharString(new StringStream(encodedCharstring)); + + glyphs.push({ + glyph: glyph, + data: subr + }); + i += 3 + length; + } else if (inGlyphs && c == 0x2F) { + token = ""; + glyph = ""; + + while ((c = eexecString[++i]) != 0x20) + glyph += String.fromCharCode(c); + } else if (!inSubrs && !inGlyphs && c == 0x2F && eexecString[i+1] == 0x53) { + while ((c = eexecString[++i]) != 0x20) {}; + inSubrs = true; + } else if (c == 0x20) { + index = length; + length = token; + token = ""; + } else if (c == 0x2F && eexecString[i+1] == 0x43 && eexecString[i+2] == 0x68) { + while ((c = eexecString[++i]) != 0x20) {}; + inSubrs = false; + inGlyphs = true; + } else { + token += String.fromCharCode(c); + } + } + return { + subrs: subrs, + charstrings: glyphs + } + }; + + /* + * Flatten the commands by interpreting the postscript code and replacing + * every 'callsubr', 'callothersubr' by the real commands. + * At the moment OtherSubrs are not fully supported and only otherSubrs 0-4 + * as descrived in 'Using Subroutines' of 'Adobe Type 1 Font Format', + * chapter 8. + */ + this.flattenCharstring = function(aCharstring, aSubrs) { + operandStack.clear(); + executionStack.clear(); + executionStack.push(aCharstring.slice()); + + var leftSidebearing = 0; + var lastPoint = 0; + while (true) { + var obj = nextInStack(); + if (IsInt(obj) || IsBool(obj)) { + operandStack.push(obj); + } else { + switch (obj) { + case "vstem3": + case "hstem3": + operandStack.push(obj.slice(0, 5)); + break; + + case "callsubr": + var index = operandStack.pop(); + executionStack.push(aSubrs[index].slice()); + break; + + case "callothersubr": + var index = operandStack.pop(); + var count = operandStack.pop(); + var data = operandStack.pop(); + // XXX The callothersubr needs to support at least the 3 defaults + // otherSubrs of the spec + if (index != 3) + error("callothersubr for index: " + index); + operandStack.push(3); + operandStack.push("callothersubr"); + break; + + case "div": + var num2 = operandStack.pop(); + var num1 = operandStack.pop(); + operandStack.push(num2 / num1); + break; + + case "pop": + operandStack.pop(); + break; + + case "closepath": + case "return": + break; + + case "hsbw": + var charWidthVector = operandStack.pop(); + var leftSidebearing = operandStack.pop(); + operandStack.push(charWidthVector); + + if (leftSidebearing) { + operandStack.push(leftSidebearing); + operandStack.push("hmoveto"); + } + break; + + case "endchar": + operandStack.push("endchar"); + return operandStack.clone(); + + case "setcurrentpoint": + case "dotsection": + case "seac": + case "sbw": + error(obj + " parsing is not implemented (yet)"); + break; + + default: + operandStack.push(obj); + break; + } + } + } + } +}; + +var CFF = function(aFontName, aFontBBox, aFontFile) { + // Get the data block containing glyphs and subrs informations + var length1 = aFontFile.dict.get("Length1"); + var length2 = aFontFile.dict.get("Length2"); + aFontFile.skip(length1); + var eexecBlock = aFontFile.getBytes(length2); + + // Extract informations from it + var start = Date.now(); + var parser = new Type1Parser(); + var fontInfo = parser.extractFontInfo(eexecBlock); + fontInfo.name = aFontName; + fontInfo.bbox = aFontBBox; + + // XXX + this.glyphs = fontInfo.charstrings; + + this.data = this.convertToCFF(fontInfo); + var end = Date.now(); + //log("Time to parse font is:" + (end - start)); +}; + +CFF.prototype = { + createCFFIndexHeader: function(aObjects, aIsByte) { + var data = []; + + // First 2 bytes contains the number of objects contained into this index + var count = aObjects.length; + if (count ==0) + return [0x00, 0x00, 0x00]; + + var bytes = FontsUtils.integerToBytes(count, 2); + for (var i = 0; i < bytes.length; i++) + data.push(bytes[i]); + + // Next byte contains the offset size use to reference object in the file + // Actually we're using 0x04 to be sure to be able to store everything + // without thinking of it while coding. + data.push(0x04); + + // Add another offset after this one because we need a new offset + var relativeOffset = 1; + for (var i = 0; i < count + 1; i++) { + var bytes = FontsUtils.integerToBytes(relativeOffset, 4); + for (var j = 0; j < bytes.length; j++) + data.push(bytes[j]); + + if (aObjects[i]) + relativeOffset += aObjects[i].length; + } + + for (var i =0; i < count; i++) { + for (var j = 0; j < aObjects[i].length; j++) + data.push(aIsByte ? aObjects[i][j] : aObjects[i].charCodeAt(j)); + } + return data; + }, + + encodeNumber: function(aValue) { + var x = 0; + // XXX we don't really care about Type2 optimization here... + if (aValue >= -32768 && aValue <= 32767) { + return [ + 28, + FontsUtils.integerToBytes(aValue >> 8, 1), + FontsUtils.integerToBytes(aValue, 1) + ]; + } else if (aValue >= (-2147483647-1) && aValue <= 2147483647) { + return [ + 0xFF, + FontsUtils.integerToBytes(aValue >> 24, 1), + FontsUtils.integerToBytes(aValue >> 16, 1), + FontsUtils.integerToBytes(aValue >> 8, 1), + FontsUtils.integerToBytes(aValue, 1) + ]; + } else { + error("Value: " + aValue + " is not allowed"); + } + }, + + getOrderedCharStrings: function(aGlyphs) { + var charstrings = []; + + for (var i = 0; i < aGlyphs.length; i++) { + var glyph = aGlyphs[i].glyph; + var unicode = GlyphsUnicode[glyph]; + if (!unicode) { + if (glyph != ".notdef") + warn(glyph + " does not have an entry in the glyphs unicode dictionary"); + } else { + var b1 = parseInt("0x" + unicode[0] + unicode[1]); + var b2 = parseInt("0x" + unicode[2] + unicode[3]); + unicode = FontsUtils.bytesToInteger([b1, b2]); + + charstrings.push({ + glyph: glyph, + unicode: unicode, + charstring: aGlyphs[i].data.slice() + }); + } + }; + + charstrings.sort(function(a, b) { + return a.unicode > b.unicode; + }); + return charstrings; + }, + + convertToCFF: function(aFontInfo) { + var debug = false; + function dump(aMsg) { + if (debug) + log(aMsg); + }; + + var charstrings = this.getOrderedCharStrings(aFontInfo.charstrings); + + var charstringsCount = 0; + var charstringsDataLength = 0; + var glyphs = []; + var glyphsChecker = {}; + var subrs = aFontInfo.subrs; + var parser = new Type1Parser(); + for (var i = 0; i < charstrings.length; i++) { + var charstring = charstrings[i].charstring.slice(); + var glyph = charstrings[i].glyph; + if (glyphsChecker[glyph]) + error("glyphs already exists!"); + glyphsChecker[glyph] = true; + + var flattened = parser.flattenCharstring(charstring, subrs); + glyphs.push(flattened); + charstringsCount++; + charstringsDataLength += flattened.length; + } + dump("There is " + charstringsCount + " glyphs (size: " + charstringsDataLength + ")"); + + // Create a CFF font data + var cff = new Uint8Array(kMaxFontFileSize); + var currentOffset = 0; + + // Font header (major version, minor version, header size, offset size) + var header = [0x01, 0x00, 0x04, 0x04]; + currentOffset += header.length; + cff.set(header); + + // Names Index + var nameIndex = this.createCFFIndexHeader([aFontInfo.name]); + cff.set(nameIndex, currentOffset); + currentOffset += nameIndex.length; + + // Calculate strings before writing the TopDICT index in order + // to calculate correct relative offsets for storing 'charset' + // and 'charstrings' data + var version = ""; + var notice = ""; + var fullName = ""; + var familyName = ""; + var weight = ""; + var strings = [version, notice, fullName, + familyName, weight]; + var stringsIndex = this.createCFFIndexHeader(strings); + var stringsDataLength = stringsIndex.length; + + // Create the global subroutines index + var globalSubrsIndex = this.createCFFIndexHeader([]); + + // Fill the charset header (first byte is the encoding) + var charset = [0x00]; + for (var i = 0; i < glyphs.length; i++) { + var index = CFFStrings.indexOf(charstrings[i].glyph); + if (index == -1) + index = CFFStrings.length + strings.indexOf(glyph); + var bytes = FontsUtils.integerToBytes(index, 2); + charset.push(bytes[0]); + charset.push(bytes[1]); + } + + // Convert charstrings + var getNumFor = { + "hstem": 1, + "vstem": 3, + "vmoveto": 4, + "rlineto": 5, + "hlineto": 6, + "vlineto": 7, + "rrcurveto": 8, + "endchar": 14, + "rmoveto": 21, + "hmoveto": 22, + "vhcurveto": 30, + "hvcurveto": 31, + }; + + // Encode the glyph and add it to the FUX + var r = [[0x40, 0x0E]]; + for (var i = 0; i < glyphs.length; i++) { + var data = glyphs[i].slice(); + var charstring = []; + for (var j = 0; j < data.length; j++) { + var c = data[j]; + if (!IsNum(c)) { + var token = getNumFor[c]; + if (!token) + error(c); + charstring.push(token); + } else { + try { + var bytes = this.encodeNumber(c); + } catch(e) { + log("Glyph " + i + " has a wrong value: " + c + " in charstring: " + data); + log("the default value is glyph " + charstrings[i].glyph + " and is supposed to be: " + charstrings[i].charstring); + } + charstring = charstring.concat(bytes); + } + } + r.push(charstring); + } + + var charstringsIndex = this.createCFFIndexHeader(r, true); + charstringsIndex = charstringsIndex.join(" ").split(" "); // XXX why? + + + //Top Dict Index + var topDictIndex = [ + 0x00, 0x01, 0x01, 0x01, 0x30, + 248, 27, 0, // version + 248, 28, 1, // Notice + 248, 29, 2, // FullName + 248, 30, 3, // FamilyName + 248, 31, 4 // Weight + ]; + + var fontBBox = aFontInfo.bbox; + for (var i = 0; i < fontBBox.length; i++) + topDictIndex = topDictIndex.concat(this.encodeNumber(fontBBox[i])); + topDictIndex.push(5) // FontBBox; + + var charsetOffset = currentOffset + + (topDictIndex.length + (4 + 4 + 4 + 7)) + + stringsIndex.length + + globalSubrsIndex.length; + topDictIndex = topDictIndex.concat(this.encodeNumber(charsetOffset)); + topDictIndex.push(15); // charset + + topDictIndex = topDictIndex.concat([28, 0, 0, 16]) // Encoding + + var charstringsOffset = charsetOffset + (charstringsCount * 2) + 1; + topDictIndex = topDictIndex.concat(this.encodeNumber(charstringsOffset)); + topDictIndex.push(17); // charstrings + + topDictIndex = topDictIndex.concat([28, 0, 55]) + var privateOffset = charstringsOffset + charstringsIndex.length; + topDictIndex = topDictIndex.concat(this.encodeNumber(privateOffset)); + topDictIndex.push(18); // Private + topDictIndex = topDictIndex.join(" ").split(" "); + + // Top Dict Index + cff.set(topDictIndex, currentOffset); + currentOffset += topDictIndex.length; + + // Strings Index + cff.set(stringsIndex, currentOffset); + currentOffset += stringsIndex.length; + + // Global Subrs Index + cff.set(globalSubrsIndex, currentOffset); + currentOffset += globalSubrsIndex.length; + + // Charset Index + cff.set(charset, currentOffset); + currentOffset += charset.length; + + // Fill charstrings data + cff.set(charstringsIndex, currentOffset); + currentOffset += charstringsIndex.length; + + // Private Data + var defaultWidth = this.encodeNumber(0); + var privateData = [].concat( + defaultWidth, [20], + [139, 21], // nominalWidth + [ + 119, 159, 248, 97, 159, 247, 87, 159, 6, + 30, 10, 3, 150, 37, 255, 12, 9, + 139, 12, + 10, 172, 10, + 172, 150, 143, 146, 150, 146, 12, 12, + 247, 32, 11, + 247, 10, 161, 147, 154, 150, 143, 12, 13, + 139, 12, 14, + 28, 0, 55, 19 + ]); + privateData = privateData.join(" ").split(" "); + cff.set(privateData, currentOffset); + currentOffset += privateData.length; + + // Dump shit at the end of the file + var shit = [ + 0x00, 0x01, 0x01, 0x01, + 0x13, 0x5D, 0x65, 0x64, + 0x5E, 0x5B, 0xAF, 0x66, + 0xBA, 0xBB, 0xB1, 0xB0, + 0xB9, 0xBA, 0x65, 0xB2, + 0x5C, 0x1F, 0x0B + ]; + cff.set(shit, currentOffset); + currentOffset += shit.length; + + + dump("==================== debug ===================="); + //var file = new Uint8Array(cff, 0, currentOffset); + //var parser = new Type2Parser(); + //parser.parse(new Stream(file)); + + var fontData = []; + for (var i = 0; i < currentOffset; i++) + fontData.push(cff[i]); + + //log("== write to file"); + //writeToFile(fontData, "/tmp/pdf.js." + fontCount + ".cff"); + + return fontData; + } +}; + diff --cc pdf.js index f46362f,17537d2..2b7eb1e --- a/pdf.js +++ b/pdf.js @@@ -1726,6 -1705,10 +1726,52 @@@ var CanvasGraphics = (function() const EO_CLIP = {}; constructor.prototype = { + translateFont: function(fontDict, xref, resources) { - return "translated"; ++ var descriptor = xref.fetch(fontDict.get("FontDescriptor")); ++ var fontName = descriptor.get("FontName").name; ++ fontName = fontName.replace("+", "_"); ++ ++ var font = Fonts[fontName]; ++ if (!font) { ++ var fontFile = descriptor.get2("FontFile", "FontFile2"); ++ fontFile = xref.fetchIfRef(fontFile); ++ ++ // Generate the custom cmap of the font if needed ++ var encodingMap = {}; ++ if (fontDict.has("Encoding")) { ++ var encoding = xref.fetchIfRef(fontDict.get("Encoding")); ++ if (IsDict(encoding)) { ++ // Build an map between codes and glyphs ++ var differences = encoding.get("Differences"); ++ var index = 0; ++ for (var j = 0; j < differences.length; j++) { ++ var data = differences[j]; ++ IsNum(data) ? index = data : encodingMap[index++] = data; ++ } ++ ++ // Get the font charset ++ var charset = descriptor.get("CharSet").split("/"); ++ } else if (IsName(encoding)) { ++ var encoding = Encodings[encoding]; ++ var widths = xref.fetchIfRef(fontDict.get("Widths")); ++ var firstchar = xref.fetchIfRef(fontDict.get("FirstChar")); ++ ++ var charset = []; ++ for (var j = 0; j < widths.length; j++) { ++ var index = widths[j]; ++ if (index) ++ charset.push(encoding[j + firstchar]); ++ } ++ } ++ } ++ ++ var fontBBox = descriptor.get("FontBBox"); ++ var subtype = fontDict.get("Subtype").name; ++ new Font(fontName, fontFile, encodingMap, charset, fontBBox, subtype); ++ } ++ return Fonts[fontName]; + }, + beginDrawing: function(mediaBox) { var cw = this.ctx.canvas.width, ch = this.ctx.canvas.height; this.ctx.save(); @@@ -1866,26 -1943,23 +2006,32 @@@ this.current.leading = leading; }, setFont: function(fontRef, size) { - var fontRes = this.res.get("Font"); - if (!fontRes) - return; - fontRes = this.xref.fetchIfRef(fontRes); - var font = fontRes.get(fontRef.name); + var font = this.res.get("Font"); + if (!IsDict(font)) + return; + + font = font.get(fontRef.name); + font = this.xref.fetchIfRef(font); if (!font) return; + + var fontName = ""; + var fontDescriptor = font.get("FontDescriptor"); + if (fontDescriptor.num) { + var fontDescriptor = this.xref.fetchIfRef(fontDescriptor); + fontName = fontDescriptor.get("FontName").name.replace("+", "_"); + Fonts.active = fontName; + } + this.current.fontSize = size; - TODO("using hard-coded font for testing"); - this.ctx.font = this.current.fontSize +'px "Nimbus Roman No9 L"'; + this.ctx.font = this.current.fontSize +'px "' + fontName + '"'; }, + setTextRenderingMode: function(mode) { + TODO("text rendering mode"); + }, + setTextRise: function(rise) { + TODO("text rise"); + }, moveText: function (x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; diff --cc test.js index 0fdb2aa,0000000..75b7200 mode 100644,000000..100644 --- a/test.js +++ b/test.js @@@ -1,156 -1,0 +1,104 @@@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / +/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ + +var pdfDocument, canvas, pageDisplay, pageNum, pageTimeout; +function load() { + canvas = document.getElementById("canvas"); + canvas.mozOpaque = true; + pageNum = parseInt(queryParams().page) || 1; + fileName = queryParams().file || "compressed.tracemonkey-pldi-09.pdf"; + open(fileName); +} + +function queryParams() { + var qs = window.location.search.substring(1); + var kvs = qs.split("&"); + var params = { }; + for (var i = 0; i < kvs.length; ++i) { + var kv = kvs[i].split("="); + params[unescape(kv[0])] = unescape(kv[1]); + } + return params; +} + +function open(url) { + document.title = url; + req = new XMLHttpRequest(); + req.open("GET", url); + req.mozResponseType = req.responseType = "arraybuffer"; + req.expected = (document.URL.indexOf("file:") == 0) ? 0 : 200; + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == req.expected) { + var data = req.mozResponseArrayBuffer || req.mozResponse || + req.responseArrayBuffer || req.response; + pdfDocument = new PDFDoc(new Stream(data)); + numPages = pdfDocument.numPages; + document.getElementById("numPages").innerHTML = numPages.toString(); + goToPage(pageNum); + } + }; + req.send(null); +} + +function gotoPage(num) { + if (0 <= num && num <= numPages) + pageNum = num; + displayPage(pageNum); +} + +function displayPage(num) { + if (pageNum != num) + window.clearTimeout(pageTimeout); + + document.getElementById("pageNumber").value = num; + + var t0 = Date.now(); + + var page = pdfDocument.getPage(pageNum = num); + - function display() { - var t1 = Date.now(); - var ctx = canvas.getContext("2d"); - ctx.save(); - ctx.fillStyle = "rgb(255, 255, 255)"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - - var gfx = new CanvasGraphics(ctx); - page.display(gfx); - - var t2 = Date.now(); - var infoDisplay = document.getElementById("info"); - infoDisplay.innerHTML = "Time to render: "+ (t1 - t0) + "/" + (t2 - t1) + " ms"; - } - - // Loading a font via data uri is asynchronous, so wait for all font - // of the page to be fully loaded before loading the page - var fontsReady = true; - var fonts = page.fonts; - var xref = page.xref; - fonts.forEach(function(fontKey, fontDict) { - var descriptor = xref.fetch(fontDict.get("FontDescriptor")); - var fontName = descriptor.get("FontName").name; - fontName = fontName.replace("+", "_"); - - // Check if the font has been loaded or is still loading - var font = Fonts[fontName]; - if (!font) { - var fontFile = descriptor.get2("FontFile", "FontFile2"); - fontFile = xref.fetchIfRef(fontFile); - - // Generate the custom cmap of the font if needed - var encodingMap = {}; - if (fontDict.has("Encoding")) { - var encoding = xref.fetchIfRef(fontDict.get("Encoding")); - if (IsDict(encoding)) { - - // Build an map between codes and glyphs - var differences = encoding.get("Differences"); - var index = 0; - for (var j = 0; j < differences.length; j++) { - var data = differences[j]; - IsNum(data) ? index = data : encodingMap[index++] = data; - } - - // Get the font charset - var charset = descriptor.get("CharSet").split("/"); - - } else if (IsName(encoding)) { - var encoding = Encodings[encoding]; - var widths = xref.fetchIfRef(fontDict.get("Widths")); - var firstchar = xref.fetchIfRef(fontDict.get("FirstChar")); - - var charset = []; - for (var j = 0; j < widths.length; j++) { - var index = widths[j]; - if (index) - charset.push(encoding[j + firstchar]); - } - } - } - - var fontBBox = descriptor.get("FontBBox"); - - var subtype = fontDict.get("Subtype").name; - new Font(fontName, fontFile, encodingMap, charset, fontBBox, subtype); - return fontsReady = false; - } else if (font.loading) { - return fontsReady = false; ++ var t1 = Date.now(); ++ ++ var ctx = canvas.getContext("2d"); ++ ctx.save(); ++ ctx.fillStyle = "rgb(255, 255, 255)"; ++ ctx.fillRect(0, 0, canvas.width, canvas.height); ++ ctx.restore(); ++ ++ var gfx = new CanvasGraphics(ctx); ++ ++ // page.compile will collect all fonts for us, once we have loaded them ++ // we can trigger the actual page rendering with page.display ++ var fonts = []; ++ ++ page.compile(gfx, fonts); ++ var t2 = Date.now(); ++ ++ var interval = setInterval(function() { ++ for (var i = 0; i < fonts.length; i++) { ++ if (fonts[i].loading) ++ return; + } - }); - - // If everything is ready do not delayed the page loading any more - if (fontsReady) - display(); - else { - // FIXME Relying on an event seems much more cleaner here instead - // of a setTimeout... - pageTimeout = window.setTimeout(displayPage, 150, num); - } ++ ++ page.display(gfx); ++ var t3 = Date.now(); ++ var infoDisplay = document.getElementById("info"); ++ infoDisplay.innerHTML = "Time to load/compile/render: "+ (t1 - t0) + "/" + (t2 - t1) + "/" + (t3 - t2) + " ms"; ++ clearInterval(interval); ++ }, 10); +} + +function nextPage() { + if (pageNum < pdfDocument.numPages) + displayPage(++pageNum); +} + +function prevPage() { + if (pageNum > 1) + displayPage(--pageNum); +} + +function goToPage(num) { + if (0 <= num && num <= numPages) + displayPage(pageNum = num); +} +