From: Vivien Nicolas <21@vingtetun.org> Date: Tue, 21 Jun 2011 00:35:14 +0000 (+0200) Subject: Merge with master X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=c4056aa99a27ae15b0b02496df439ebf4ebf386d;p=pdf.js.git Merge with master --- c4056aa99a27ae15b0b02496df439ebf4ebf386d diff --cc fonts.js index 273ef5e,b8a4903..53db045 --- a/fonts.js +++ b/fonts.js @@@ -88,542 -79,489 +86,541 @@@ var Fonts = * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); * type1Font.bind(); */ - var Font = function(aName, aFile, aProperties) { - this.name = aName; - this.encoding = aProperties.encoding; - - // If the font has already been decoded simply return - if (Fonts[aName]) { - this.font = Fonts[aName].data; - return; - } - fontCount++; - - if (aProperties.ignore || kDisableFonts) { - Fonts[aName] = { - data: aFile, - loading: false, - properties: {}, - cache: Object.create(null) + var Font = (function () { + var constructor = function(aName, aFile, aProperties) { + this.name = aName; ++ this.encoding = aProperties.encoding; + + // If the font has already been decoded simply return it + if (Fonts[aName]) { + this.font = Fonts[aName].data; + return; } - return; - } - - switch (aProperties.type) { - case "Type1": - var cff = new CFF(aName, aFile, aProperties); - this.mimetype = "font/opentype"; - - // Wrap the CFF data inside an OTF font file - this.font = this.cover(aName, cff, aProperties); - break; - - case "TrueType": - this.mimetype = "font/opentype"; - var ttf = new TrueType(aName, aFile, aProperties); - this.font = ttf.data; - break; + fontCount++; ++ fontName = aName; - default: - warn("Font " + aProperties.type + " is not supported"); - switch (aProperties.type) { - case "Type1": - var cff = new CFF(aName, aFile, aProperties); - this.mimetype = "font/otf"; - - // Wrap the CFF data inside an OTF font file - this.font = this.convert(cff, aProperties); -- break; - } - - case "TrueType": - // TrueType is disabled for the moment since the sanitizer prevent it - // from loading because of an overdated cmap table - return Fonts[aName] = { - data: null, - properties: { - encoding: {}, - charset: null - }, ++ if (aProperties.ignore || kDisableFonts) { ++ Fonts[aName] = { ++ data: aFile, + loading: false, ++ properties: {}, + cache: Object.create(null) - }; - - this.mimetype = "font/ttf"; - var ttf = new TrueType(aFile); - this.font = ttf.data; - break; ++ } ++ return; ++ } - Fonts[aName] = { - data: this.font, - properties: aProperties, - loading: true, - cache: Object.create(null) - } - default: - warn("Font " + aProperties.type + " is not supported"); - break; ++ switch (aProperties.type) { ++ case "Type1": ++ var cff = new CFF(aName, aFile, aProperties); ++ this.mimetype = "font/opentype"; + - // Attach the font to the document - this.bind(); - }; ++ // Wrap the CFF data inside an OTF font file ++ this.font = this.cover(aName, cff, aProperties); ++ break; + ++ case "TrueType": ++ // TrueType is disabled for the moment since the sanitizer prevent it ++ // from loading due to missing tables ++ return Fonts[aName] = { ++ data: null, ++ properties: { ++ encoding: {}, ++ charset: null ++ }, ++ loading: false, ++ cache: Object.create(null) ++ }; ++ ++ this.mimetype = "font/opentype"; ++ var ttf = new TrueType(aName, aFile, aProperties); ++ this.font = ttf.data; ++ break; + - /** - * A bunch of the OpenType code is duplicate between this class and the - * TrueType code, this is intentional and will merge in a future version - * where all the code relative to OpenType will probably have its own - * class and will take decision without the Fonts consent. - * But at the moment it allows to develop around the TrueType rewriting - * on the fly without messing up with the 'regular' Type1 to OTF conversion. - */ - Font.prototype = { - name: null, - font: null, - mimetype: null, - encoding: 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); ++ default: ++ warn("Font " + aProperties.type + " is not supported"); ++ break; } - 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].properties.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) { - var encoding = this.encoding; - for (var i = 0; i < charset.length; i++) { - var unicode = GlyphsUnicode[charset[i]]; - if (!unicode) - continue; - testString += String.fromCharCode(unicode); - } + Fonts[aName] = { + data: this.font, + properties: aProperties, + loading: true, + cache: Object.create(null) } - ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - var textWidth = ctx.measureText(testString).width; - if (debug) - ctx.fillText(testString, 20, 20); + // Attach the font to the document + this.bind(); + }; - var interval = window.setInterval(function canvasInterval(self) { - this.start = this.start || Date.now(); - ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; + /** + * A bunch of the OpenType code is duplicate between this class and the + * TrueType code, this is intentional and will merge in a future version + * where all the code relative to OpenType will probably have its own + * class and will take decision without the Fonts consent. + * But at the moment it allows to develop around the TrueType rewriting + * on the fly without messing up with the 'regular' Type1 to OTF conversion. + */ + constructor.prototype = { + name: null, + font: null, + mimetype: null, ++ encoding: null, + + bind: function font_bind() { + var data = this.font; + + // Get the base64 encoding of the binary font data + var str = ""; + var length = data.length; + for (var i = 0; i < length; ++i) + str += String.fromCharCode(data[i]); + + var dataBase64 = window.btoa(str); + 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); + } - // For some reasons the font has not loaded, so mark it loaded for the - // page to proceed but cry - if ((Date.now() - this.start) >= kMaxWaitForFontFace) { - window.clearInterval(interval); - Fonts[fontName].loading = false; - warn("Is " + fontName + " for charset: " + charset + " loaded?"); - this.start = 0; - } else if (textWidth != ctx.measureText(testString).width) { - window.clearInterval(interval); - Fonts[fontName].loading = false; - this.start = 0; + 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 || []; ++ var charset = Fonts[fontName].properties.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 = GlyphsUnicode[charset[i]]; + if (!unicode) - error("Unicode for " + charset[i] + " is has not been found in the glyphs list"); ++ continue; + testString += String.fromCharCode(unicode); + } } + ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; + var textWidth = ctx.measureText(testString).width; if (debug) - ctx.fillText(testString, 20, 50); - }, 50, 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; + ctx.fillText(testString, 20, 20); + - var start = Date.now(); + var interval = window.setInterval(function canvasInterval(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.measureText(testString).width) { - window.clearInterval(interval); - Fonts[fontName].loading = false; - } ++ this.start = this.start || Date.now(); ++ 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() - this.start) >= kMaxWaitForFontFace) { ++ window.clearInterval(interval); ++ Fonts[fontName].loading = false; ++ warn("Is " + fontName + " for charset: " + charset + " loaded?"); ++ this.start = 0; ++ } else if (textWidth != ctx.measureText(testString).width) { ++ window.clearInterval(interval); ++ Fonts[fontName].loading = false; ++ this.start = 0; ++ } - // Per spec tables must be 4-bytes align so add some 0x00 if needed - while (aData.length & 3) - aData.push(0x00); - if (debug) - ctx.fillText(testString, 20, 50); - }, 50, this); ++ if (debug) ++ ctx.fillText(testString, 20, 50); ++ }, 50, this); - // length - var length = aData.length; + /** Hack end */ - // checksum - var checksum = FontsUtils.bytesToInteger(tag) + offset + length; + // 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); + }, - 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; - }, - convert: function font_convert(aFont, aProperties) { - var otf = new Uint8Array(kMaxFontFileSize); ++ cover: function font_cover(aName, aFont, aProperties) { ++ var otf = Uint8Array(kMaxFontFileSize); - _createCMAPTable: function font_createCMAPTable(aGlyphs) { - var characters = new Uint16Array(kMaxGlyphsCount); - for (var i = 0; i < aGlyphs.length; i++) - characters[aGlyphs[i].unicode] = i + 1; + function s2a(s) { + var a = []; + for (var i = 0; i < s.length; ++i) + a[i] = s.charCodeAt(i); + return a; + } - // Separate the glyphs into continuous range of codes, aka segment. - var ranges = []; - var range = []; - var count = characters.length; - for (var i = 0; i < count; i++) { - if (characters[i]) { - range.push(i); - } else if (range.length) { - ranges.push(range.slice()); - range = []; + function s16(value) { + return String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); } - } - // 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)); + function s32(value) { + return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) + + String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); + } - 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) - ); + function createOpenTypeHeader(aFile, aOffsets, numTables) { + var header = ""; - // 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); + // sfnt version (4 bytes) + header += "\x4F\x54\x54\x4F"; - var delta = FontsUtils.integerToBytes(((range[0] - 1) - bias) % 65536, 2); - bias += range.length; + // numTables (2 bytes) + header += s16(numTables); - // deltas are signed shorts - delta[0] ^= 0xFF; - delta[1] ^= 0xFF; - delta[1] += 1; + // searchRange (2 bytes) + var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); + var searchRange = tablesMaxPower2 * 16; + header += s16(searchRange); - startCount.push(start[0], start[1]); - endCount.push(end[0], end[1]); - idDeltas.push(delta[0], delta[1]); - idRangeOffsets.push(0x00, 0x00); + // entrySelector (2 bytes) + header += s16(Math.log(tablesMaxPower2) / Math.log(2)); - 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); + // rangeShift (2 bytes) + header += s16(numTables * 16 - searchRange); - return cmap.concat(endCount, [0x00, 0x00], startCount, - idDeltas, idRangeOffsets, glyphsIdsArray); - }, + aFile.set(s2a(header), aOffsets.currentOffset); + aOffsets.currentOffset += header.length; + aOffsets.virtualOffset += header.length; + } - _createNameTable: function font_createNameTable(aName) { - var names = [ - "See original licence", // Copyright - aName, // Font family - "undefined", // Font subfamily (font weight) - "uniqueID", // Unique ID - aName, // Full font name - "0.1", // Version - "undefined", // Postscript name - "undefined", // Trademark - "undefined", // Manufacturer - "undefined" // Designer - ]; + function createTableEntry(aFile, aOffsets, aTag, aData) { + // offset + var offset = aOffsets.virtualOffset; + + // Per spec tables must be 4-bytes align so add padding as needed + while (aData.length & 3) + aData.push(0x00); + + // length + var length = aData.length; + + // checksum + var checksum = aTag.charCodeAt(0) + + aTag.charCodeAt(1) + - aTag.charCodeAt(2) + - aTag.charCodeAt(3) + ++ aTag.charCodeAt(2) + ++ aTag.charCodeAt(3) + + offset + + length; + + var tableEntry = aTag + s32(checksum) + s32(offset) + s32(length); + tableEntry = s2a(tableEntry); + aFile.set(tableEntry, aOffsets.currentOffset); + aOffsets.currentOffset += tableEntry.length; + aOffsets.virtualOffset += aData.length; + } - var name = [ - 0x00, 0x00, // format - 0x00, 0x0A, // Number of names Record - 0x00, 0x7E // Storage - ]; ++ function createNameTable(aName) { ++ var names = ++ "See original licence" + // Copyright ++ aName + // Font family ++ "undefined" + // Font subfamily (font weight) ++ "uniqueID" + // Unique ID ++ aName + // Full font name ++ "0.1" + // Version ++ "undefined" + // Postscript name ++ "undefined" + // Trademark ++ "undefined" + // Manufacturer ++ "undefined"; // Designer ++ ++ var name = ++ "\x00\x00" + // format ++ "\x00\x0A" + // Number of names Record ++ "\x00\x7E"; // Storage ++ ++ // Build the name records field ++ var strOffset = 0; ++ for (var i = 0; i < names.length; i++) { ++ var str = names[i]; ++ ++ var nameRecord = ++ "\x00\x01" + // platform ID ++ "\x00\x00" + // encoding ID ++ "\x00\x00" + // language ID ++ "\x00\x00" + // name ID ++ s16(str.length) + ++ s16(strOffset); ++ name += nameRecord; ++ ++ strOffset += str.length; ++ } + - // Build the name records field - var strOffset = 0; - for (var i = 0; i < names.length; i++) { - var str = names[i]; ++ name += names; ++ return name; ++ } + - var nameRecord = [ - 0x00, 0x01, // platform ID - 0x00, 0x00, // encoding ID - 0x00, 0x00, // language ID - 0x00, 0x00 // name ID - ]; + function getRanges(glyphs) { + // Array.sort() sorts by characters, not numerically, so convert to an + // array of characters. + var codes = []; + var length = glyphs.length; + for (var n = 0; n < length; ++n) + codes.push(String.fromCharCode(glyphs[n].unicode)) + codes.sort(); + + // Split the sorted codes into ranges. + var ranges = []; + for (var n = 0; n < length; ) { + var start = codes[n++].charCodeAt(0); + var end = start; + while (n < length && end + 1 == codes[n].charCodeAt(0)) { + ++end; + ++n; + } + ranges.push([start, end]); + } + return ranges; + } - nameRecord = nameRecord.concat( - FontsUtils.integerToBytes(str.length, 2), - FontsUtils.integerToBytes(strOffset, 2) - ); - name = name.concat(nameRecord); + function createCMAPTable(aGlyphs) { + var ranges = getRanges(aGlyphs); + + 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 = "\x00\x00" + // version + "\x00\x01" + // numTables + "\x00\x03" + // platformID + "\x00\x01" + // encodingID + "\x00\x00\x00\x0C" + // start of the table record + "\x00\x04" + // format + s16(headerSize) + // length + "\x00\x00" + // languages + s16(segCount2) + + s16(searchRange) + + s16(searchEntry) + + s16(rangeShift); + + // Fill up the 4 parallel arrays describing the segments. + var startCount = ""; + var endCount = ""; + var idDeltas = ""; + var idRangeOffsets = ""; + var glyphsIds = ""; + var bias = 0; + for (var i = 0; i < segCount - 1; i++) { + var range = ranges[i]; + var start = range[0]; + var end = range[1]; + var delta = (((start - 1) - bias) ^ 0xffff) + 1; + bias += (end - start + 1); + + startCount += s16(start); + endCount += s16(end); + idDeltas += s16(delta); + idRangeOffsets += s16(0); + + for (var j = start; j <= end; j++) + glyphsIds += String.fromCharCode(j); + } - strOffset += str.length; - } + startCount += "\xFF\xFF"; + endCount += "\xFF\xFF"; + idDeltas += "\x00\x01"; + idRangeOffsets += "\x00\x00"; - // Add the name records data - for (var i = 0; i < names.length; i++) { - var str = names[i]; - var strBytes = []; - for (var j = 0; j < str.length; j++) { - strBytes.push(str.charCodeAt(j)); + return s2a(cmap + endCount + "\x00\x00" + startCount + + idDeltas + idRangeOffsets + glyphsIds); } - name = name.concat(strBytes); - } - - return name; - }, - cover: function font_cover(aName, aFont, aProperties) { - var otf = new Uint8Array(kMaxFontFileSize); - - // Required Tables - var CFF = aFont.data, // PostScript Font Program + // 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 + 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(aProperties.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++) { - 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 */ - var name = this._createNameTable(aName); - this._createTableEntry(otf, offsets, "name", name); - - /** POST */ - // FIXME Get those informations from the FontInfo structure - 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); + 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 + createOpenTypeHeader(otf, offsets, tables.length); + + // TODO: 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. + createTableEntry(otf, offsets, "CFF ", CFF); + + /** OS/2 */ + OS2 = s2a( + "\x00\x03" + // version + "\x02\x24" + // xAvgCharWidth + "\x01\xF4" + // usWeightClass + "\x00\x05" + // usWidthClass + "\x00\x00" + // fstype + "\x02\x8A" + // ySubscriptXSize + "\x02\xBB" + // ySubscriptYSize + "\x00\x00" + // ySubscriptXOffset + "\x00\x8C" + // ySubscriptYOffset + "\x02\x8A" + // ySuperScriptXSize + "\x02\xBB" + // ySuperScriptYSize + "\x00\x00" + // ySuperScriptXOffset + "\x01\xDF" + // ySuperScriptYOffset + "\x00\x31" + // yStrikeOutSize + "\x01\x02" + // yStrikeOutPosition + "\x00\x00" + // sFamilyClass + "\x02\x00\x06\x03\x00\x00\x00\x00\x00\x00" + // Panose + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 0-31) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 32-63) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 64-95) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 96-127) + "\x2A\x32\x31\x2A" + // achVendID + "\x00\x20" + // fsSelection + "\x00\x2D" + // usFirstCharIndex + "\x00\x7A" + // usLastCharIndex + "\x00\x03" + // sTypoAscender + "\x00\x20" + // sTypeDescender + "\x00\x38" + // sTypoLineGap + "\x00\x5A" + // usWinAscent + "\x02\xB4" + // usWinDescent + "\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31) + "\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63) + "\x00\x00" + // sxHeight + "\x00\x00" + // sCapHeight + "\x00\x01" + // usDefaultChar + "\x00\xCD" + // usBreakChar + "\x00\x02" // usMaxContext + ); + createTableEntry(otf, offsets, "OS/2", OS2); + + //XXX Getting charstrings here seems wrong since this is another CFF glue + var charstrings = aFont.getOrderedCharStrings(aProperties.glyphs); + + /** CMAP */ + cmap = createCMAPTable(charstrings); + createTableEntry(otf, offsets, "cmap", cmap); + + /** HEAD */ + head = s2a( + "\x00\x01\x00\x00" + // Version number + "\x00\x00\x50\x00" + // fontRevision + "\x00\x00\x00\x00" + // checksumAdjustement + "\x5F\x0F\x3C\xF5" + // magicNumber + "\x00\x00" + // Flags + "\x03\xE8" + // unitsPerEM (defaulting to 1000) + "\x00\x00\x00\x00\x00\x00\x00\x00" + // creation date + "\x00\x00\x00\x00\x00\x00\x00\x00" + // modifification date + "\x00\x00" + // xMin + "\x00\x00" + // yMin + "\x00\x00" + // xMax + "\x00\x00" + // yMax + "\x00\x00" + // macStyle + "\x00\x00" + // lowestRecPPEM + "\x00\x00" + // fontDirectionHint + "\x00\x00" + // indexToLocFormat + "\x00\x00" // glyphDataFormat + ); + createTableEntry(otf, offsets, "head", head); + + /** HHEA */ + hhea = s2a( + "\x00\x01\x00\x00" + // Version number + "\x00\x00" + // Typographic Ascent + "\x00\x00" + // Typographic Descent + "\x00\x00" + // Line Gap + "\xFF\xFF" + // advanceWidthMax + "\x00\x00" + // minLeftSidebearing + "\x00\x00" + // minRightSidebearing + "\x00\x00" + // xMaxExtent + "\x00\x00" + // caretSlopeRise + "\x00\x00" + // caretSlopeRun + "\x00\x00" + // caretOffset + "\x00\x00" + // -reserved- + "\x00\x00" + // -reserved- + "\x00\x00" + // -reserved- + "\x00\x00" + // -reserved- + "\x00\x00" + // metricDataFormat + s16(charstrings.length) + ); + createTableEntry(otf, offsets, "hhea", hhea); + + /** HMTX */ + hmtx = "\x01\xF4\x00\x00"; + for (var i = 0; i < charstrings.length; i++) { + var charstring = charstrings[i].charstring; + var width = charstring[1]; + var lsb = charstring[0]; + hmtx += s16(width) + s16(lsb); + } + hmtx = s2a(hmtx); + createTableEntry(otf, offsets, "hmtx", hmtx); + + /** MAXP */ + maxp = "\x00\x00\x50\x00" + // Version number + s16(charstrings.length + 1); // Num of glyphs (+1 to pass the sanitizer...) + maxp = s2a(maxp); + createTableEntry(otf, offsets, "maxp", maxp); + + /** NAME */ - name = "\x00\x00" + // Format - "\x00\x00" + // Number of name records - "\x00\x00"; // Storage - name = s2a(name); ++ name = s2a(createNameTable(aName)); + createTableEntry(otf, offsets, "name", name); + + /** POST */ + // TODO: get those informations from the FontInfo structure + post = "\x00\x03\x00\x00" + // Version number + "\x00\x00\x01\x00" + // italicAngle + "\x00\x00" + // underlinePosition + "\x00\x00" + // underlineThickness + "\x00\x00\x00\x00" + // isFixedPitch + "\x00\x00\x00\x00" + // minMemType42 + "\x00\x00\x00\x00" + // maxMemType42 + "\x00\x00\x00\x00" + // minMemType1 + "\x00\x00\x00\x00"; // maxMemType1 + post = s2a(post); + 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; + } - // 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]); + return fontData; } + }; - var fontData = []; - for (var i = 0; i < offsets.currentOffset; i++) - fontData.push(otf[i]); - return fontData; - } - }; - + return constructor; + })(); /** * FontsUtils is a static class dedicated to hold codes that are not related @@@ -701,16 -637,17 +698,14 @@@ var TrueType = function(aName, aFile, a 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); + if (index != -1) { + if (table.tag == "cmap") + originalCMAP = table; - requiredTables.splice(index, 1); - tables.push(table); ++ tables.push(table); + } - - tables.push(table); } - // Tables needs to be written by ascendant alphabetic order - 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. @@@ -755,54 -692,6 +750,54 @@@ 0x00, 0x02 // usMaxContext ]; + // If the font is missing a OS/2 table it's could be an old mac font + // without a 3-1-4 Unicode BMP table, so let's rewrite it. + var charset = aProperties.charset; + var glyphs = []; + for (var i = 0; i < charset.length; i++) { + glyphs.push({ + unicode: GlyphsUnicode[charset[i]] + }); + } + + + var offsetDelta = 0; + + // Replace the old CMAP table + var rewrittedCMAP = this._createCMAPTable(glyphs); + offsetDelta = rewrittedCMAP.length - originalCMAP.data.length; + originalCMAP.data = rewrittedCMAP; + + // Rewrite the 'post' table if needed + var postTable = null; + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + if (table.tag == "post") { + postTable = table; + break; + } + } + + if (!postTable) { + var post = [ + 0x00, 0x03, 0x00, 0x00, // Version number + 0x00, 0x00, 0x01, 0x00, // italicAngle - 0x00, 0x00, // underlinePosition ++ 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 + ]; + + offsetDelta += post.length; + tables.unshift({ + tag: "post", + data: post + }); + } + // Create a new file to hold the new version of our truetype with a new // header and new offsets var stream = aFile.stream || aFile; @@@ -830,12 -719,6 +825,11 @@@ data: OS2 }); - + // Tables needs to be written by ascendant alphabetic order + tables.sort(function(a, b) { + return a.tag > b.tag; + }); + // rewrite the tables but tweak offsets for (var i = 0; i < tables.length; i++) { var table = tables[i]; @@@ -859,16 -742,13 +853,17 @@@ offsets.currentOffset++; } - this.data = ttf; + var fontData = []; + for (var i = 0; i < offsets.currentOffset; i++) + fontData.push(ttf[i]); + + this.data = fontData; return; - } else if (requiredTables.lenght) { + } else if (requiredTables.length) { - warn("Missing " + requiredTables + " in the TrueType font"); + error("Table " + requiredTables[0] + " is missing from the TruType font"); + } else { + this.data = aFile.getBytes(); } - this.data = aFile; }; TrueType.prototype = { @@@ -901,157 -781,75 +896,157 @@@ 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 - } - }, - + _createCMAPTable: function font_createCMAPTable(aGlyphs) { - var characters = new Uint16Array(kMaxGlyphsCount); ++ var characters = Uint16Array(65535); + 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 = []; + var count = characters.length; + for (var i = 0; i < count; 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); ++ }, ++ + _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 + } } }; diff --cc pdf.js index d39579f,4004430..2ce6b48 --- a/pdf.js +++ b/pdf.js @@@ -48,9 -48,17 +48,9 @@@ function shadow(obj, prop, value) return value; } -function bytesToString(bytes) { - var str = ""; - var length = bytes.length; - for (var n = 0; n < length; ++n) - str += String.fromCharCode(bytes[n]); - return str; -} - var Stream = (function() { function constructor(arrayBuffer, start, length, dict) { - this.bytes = new Uint8Array(arrayBuffer); + this.bytes = Uint8Array(arrayBuffer); this.start = start || 0; this.pos = this.start; this.end = (start + length) || this.bytes.byteLength; @@@ -306,9 -325,8 +306,9 @@@ var FlateStream = (function() return this.buffer = buffer2; }, getByte: function() { + var bufferLength = this.bufferLength; var pos = this.pos; - if (bufferLength == pos) { - while (this.bufferLength <= pos) { ++ if (bufferLength <= pos) { if (this.eof) return; this.readBlock(); @@@ -331,9 -349,8 +331,9 @@@ return this.buffer.subarray(pos, end) }, lookChar: function() { + var bufferLength = this.bufferLength; var pos = this.pos; - if (bufferLength == pos) { - while (this.bufferLength <= pos) { ++ if (bufferLength <= pos) { if (this.eof) return; this.readBlock(); @@@ -391,7 -407,14 +391,13 @@@ return [codes, maxLen]; }, readBlock: function() { + function repeat(stream, array, len, offset, what) { + var repeat = stream.getBits(len) + offset; + while (repeat-- > 0) + array[i++] = what; + } + - var bytes = this.bytes; - var bytesPos = this.bytesPos; + var stream = this.stream; // read block header var hdr = this.getBits(3); @@@ -508,9 -526,120 +509,97 @@@ return constructor; })(); -// A JpegStream can't be read directly. We use the platform to render the underlying -// JPEG data for us. -var JpegStream = (function() { - function constructor(bytes, dict) { - // TODO: per poppler, some images may have "junk" before that need to be removed - this.dict = dict; - - // create DOM image - var img = new Image(); - img.src = "data:image/jpeg;base64," + window.btoa(bytesToString(bytes)); - this.domImage = img; - } - - constructor.prototype = { - getImage: function() { - return this.domImage; - } - }; - - return constructor; -})(); - + var PredictorStream = (function() { + function constructor(stream, params) { + this.stream = stream; - this.dict = stream.dict; + this.predictor = params.get("Predictor") || 1; + if (this.predictor <= 1) { + return stream; // no prediction + } + if (params.has("EarlyChange")) { + error("EarlyChange predictor parameter is not supported"); + } + this.colors = params.get("Colors") || 1; + this.bitsPerComponent = params.get("BitsPerComponent") || 8; + this.columns = params.get("Columns") || 1; + if (this.colors !== 1 || this.bitsPerComponent !== 8) { + error("Multi-color and multi-byte predictors are not supported"); + } + if (this.predictor < 10 || this.predictor > 15) { + error("Unsupported predictor"); + } + this.currentRow = new Uint8Array(this.columns); + this.pos = 0; + this.bufferLength = 0; + } + + constructor.prototype = { + readRow : function() { + var lastRow = this.currentRow; + var predictor = this.stream.getByte(); + var currentRow = this.stream.getBytes(this.columns), i; + switch (predictor) { + default: + error("Unsupported predictor"); + break; + case 0: + break; + case 2: + for (i = 0; i < currentRow.length; ++i) { + currentRow[i] = (lastRow[i] + currentRow[i]) & 0xFF; + } + break; + } + this.pos = 0; + this.bufferLength = currentRow.length; + this.currentRow = currentRow; + }, + getByte : function() { + if (this.pos >= this.bufferLength) { + this.readRow(); + } + return this.currentRow[this.pos++]; + }, + getBytes : function(n) { + var i, bytes; + bytes = new Uint8Array(n); + for (i = 0; i < n; ++i) { + if (this.pos >= this.bufferLength) { + this.readRow(); + } + bytes[i] = this.currentRow[this.pos++]; + } + return bytes; + }, + getChar : function() { + return String.formCharCode(this.getByte()); + }, + lookChar : function() { + if (this.pos >= this.bufferLength) { + this.readRow(); + } + return String.formCharCode(this.currentRow[this.pos]); + }, + skip : function(n) { + var i; + if (!n) { + n = 1; + } + while (n > this.bufferLength - this.pos) { + n -= this.bufferLength - this.pos; + this.readRow(); + if (this.bufferLength === 0) break; + } + this.pos += n; + } + }; + + return constructor; + })(); + var DecryptStream = (function() { function constructor(str, fileKey, encAlgorithm, keyLength) { - // TODO + TODO("decrypt stream is not implemented"); } constructor.prototype = Stream.prototype; @@@ -1088,9 -1217,11 +1177,11 @@@ var Parser = (function() this.encAlgorithm, this.keyLength); } - return this.filter(stream, dict); - stream = this.filter(stream, dict, length); - stream.parameters = dict; ++ stream = this.filter(stream, dict); ++ stream.parameters = dict; + return stream; }, - filter: function(stream, dict, length) { + filter: function(stream, dict) { var filter = dict.get2("Filter", "F"); var params = dict.get2("DecodeParms", "DP"); if (IsName(filter)) @@@ -1111,11 -1242,15 +1202,12 @@@ } return stream; }, - makeFilter: function(stream, name, length, params) { + makeFilter: function(stream, name, params) { if (name == "FlateDecode" || name == "Fl") { - if (params) - error("params not supported yet for FlateDecode"); + if (params) { + return new PredictorStream(new FlateStream(stream), params); + } return new FlateStream(stream); - } else if (name == "DCTDecode") { - var bytes = stream.getBytes(length); - return new JpegStream(bytes, stream.dict); } else { error("filter '" + name + "' not supported yet"); } @@@ -1273,10 -1407,11 +1364,11 @@@ var XRef = (function() } else if (IsRef(obj)) { // certain buggy PDF generators generate "/Prev NNN 0 R" instead // of "/Prev NNN" - this.prev = obj.num; - more = true; + prev = obj.num; + } + if (prev) { - this.readXRef(prev); ++ this.readXRef(prev); } - this.trailerDict = dict; // check for 'XRefStm' key if (IsInt(obj = dict.get("XRefStm"))) { @@@ -1286,11 -1421,56 +1378,57 @@@ this.xrefstms[pos] = 1; // avoid infinite recursion this.readXRef(pos); } + - return more; - }, - readXRefStream: function(parser) { - error("Invalid XRef stream"); + return dict; + }, + readXRefStream: function(stream) { + var streamParameters = stream.parameters; + var length = streamParameters.get("Length"); + var byteWidths = streamParameters.get("W"); + var range = streamParameters.get("Index"); + if (!range) + range = [0, streamParameters.get("Size")]; + var i, j; + while (range.length > 0) { + var first = range[0], n = range[1]; + if (!IsInt(first) || !IsInt(n)) + error("Invalid XRef range fields"); + var typeFieldWidth = byteWidths[0], offsetFieldWidth = byteWidths[1], generationFieldWidth = byteWidths[2]; + if (!IsInt(typeFieldWidth) || !IsInt(offsetFieldWidth) || !IsInt(generationFieldWidth)) + error("Invalid XRef entry fields length"); + for (i = 0; i < n; ++i) { + var type = 0, offset = 0, generation = 0; + for (j = 0; j < typeFieldWidth; ++j) + type = (type << 8) | stream.getByte(); + for (j = 0; j < offsetFieldWidth; ++j) + offset = (offset << 8) | stream.getByte(); + for (j = 0; j < generationFieldWidth; ++j) + generation = (generation << 8) | stream.getByte(); + var entry = new Ref(offset, generation); + if (typeFieldWidth > 0) { + switch (type) { + case 0: + entry.free = true; + break; + case 1: + entry.uncompressed = true; + break; + case 2: + break; + default: + error("Invalid XRef entry type"); + break; + } + } + if (!this.entries[first + i]) + this.entries[first + i] = entry; + } + range.splice(0, 2); + } + var prev = streamParameters.get("Prev"); + if (IsInt(prev)) + this.readXRef(prev); + return streamParameters; }, readXRef: function(startXRef) { var stream = this.stream; @@@ -1358,7 -1539,39 +1497,40 @@@ this.cache[num] = e; return e; } - error("compressed entry"); ++ + // compressed entry + stream = this.fetch(new Ref(e.offset, 0)); + if (!IsStream(stream)) + error("bad ObjStm stream"); + var first = stream.parameters.get("First"); + var n = stream.parameters.get("N"); + if (!IsInt(first) || !IsInt(n)) { + error("invalid first and n parameters for ObjStm stream"); + } + parser = new Parser(new Lexer(stream), false); + var i, entries = [], nums = []; + // read the object numbers to populate cache + for (i = 0; i < n; ++i) { + var num = parser.getObj(); + if (!IsInt(num)) { + error("invalid object number in the ObjStm stream"); + } + nums.push(num); + var offset = parser.getObj(); + if (!IsInt(offset)) { + error("invalid object offset in the ObjStm stream"); + } + } + // read stream objects for cache + for (i = 0; i < n; ++i) { + entries.push(parser.getObj()); + this.cache[nums[i]] = entries[i]; + } + e = entries[e.gen]; + if (!e) { + error("bad XRef entry for compressed object"); + } + return e; }, getCatalogObj: function() { return this.fetch(this.root); @@@ -1389,20 -1602,39 +1561,40 @@@ var Page = (function() : null)); }, compile: function(gfx, fonts) { - if (!this.code) { - var xref = this.xref; - var content = xref.fetchIfRef(this.content); - var resources = xref.fetchIfRef(this.resources); + if (this.code) { + // content was compiled + return; + } ++ + var xref = this.xref; + var content; + var resources = xref.fetchIfRef(this.resources); + if (!IsArray(this.content)) { + // content is not an array, shortcut + content = xref.fetchIfRef(this.content); this.code = gfx.compile(content, xref, resources, fonts); + return; + } + // the content is an array, compiling all items + var i, n = this.content.length, compiledItems = []; + for (i = 0; i < n; ++i) { + content = xref.fetchIfRef(this.content[i]); + compiledItems.push(gfx.compile(content, xref, resources, fonts)); } + // creating the function that executes all compiled items + this.code = function(gfx) { + var i, n = compiledItems.length; + for (i = 0; i < n; ++i) { + compiledItems[i](gfx); + } + }; }, display: function(gfx) { + assert(this.code instanceof Function, "page content must be compiled first"); var xref = this.xref; - var content = xref.fetchIfRef(this.content); var resources = xref.fetchIfRef(this.resources); var mediaBox = xref.fetchIfRef(this.mediaBox); - assertWellFormed(IsStream(content) && IsDict(resources), - "invalid page content or resources"); + assertWellFormed(IsDict(resources), "invalid page resources"); gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], width: mediaBox[2] - mediaBox[0], height: mediaBox[3] - mediaBox[1] }); @@@ -1922,9 -2151,11 +2118,10 @@@ var CanvasGraphics = (function() // Get the font charset if any var charset = descriptor.get("CharSet"); - assertWellFormed(IsString(charset), "invalid charset"); - if (charset) { ++ if (charset) + assertWellFormed(IsString(charset), "invalid charset"); - charset = charset.split("/"); - } + charset = charset.split("/"); } else if (IsName(encoding)) { var encoding = Encodings[encoding.name]; if (!encoding) @@@ -1945,71 -2171,6 +2142,72 @@@ charset.push(encoding[j + firstChar]); } } + } else if (fontDict.has("ToUnicode")) { + var cmapObj = xref.fetchIfRef(fontDict.get("ToUnicode")); + if (IsName(cmapObj)) { + error("ToUnicode file cmap translation not implemented"); + } else if (IsStream(cmapObj)) { + var encoding = Encodings["WinAnsiEncoding"]; + var firstChar = xref.fetchIfRef(fontDict.get("FirstChar")); + for (var i = firstChar; i < encoding.length; i++) + encodingMap[i] = new Name(encoding[i]); + + var tokens = []; + var token = ""; + - var cmap = cmapObj.getBytes(cmapObj.length); ++ var buffer = cmapObj.ensureBuffer(); ++ var cmap = cmapObj.getBytes(buffer.byteLength); + for (var i =0; i < cmap.length; i++) { + var byte = cmap[i]; + if (byte == 0x20 || byte == 0x0A || byte == 0x3C || byte == 0x3E) { + switch (token) { + case "useCMap": + error("useCMap is not implemented"); + break; + + case "beginbfrange": + ignoreFont = false; + case "begincodespacerange": + token = ""; + tokens = []; + break; + + case "endcodespacerange": + TODO("Support CMap ranges"); + break; + + case "endbfrange": + for (var j = 0; j < tokens.length; j+=3) { + var startRange = parseInt("0x" + tokens[j]); + var endRange = parseInt("0x" + tokens[j+1]); + var code = parseInt("0x" + tokens[j+2]); + + for (var k = startRange; k <= endRange; k++) { + encodingMap[k] = GlyphsUnicode[encoding[code]]; + charset.push(encoding[code++]); + } + } + break; + + case "beginfbchar": + case "endfbchar": + error("fbchar parsing is not implemented"); + break; + + default: + if (token.length) { + tokens.push(token); + token = ""; + } + break; + } + } else if (byte == 0x5B || byte == 0x5D) { + error("CMAP list parsing is not implemented"); + } else { + token += String.fromCharCode(byte); + } + } + } } var subType = fontDict.get("Subtype"); @@@ -2377,7 -2547,125 +2579,125 @@@ }, setFillColorN: function(/*...*/) { // TODO real impl - this.setFillColor.apply(this, arguments); + var colorSpace = this.current.colorSpace; + if (!colorSpace) { + var stateStack = this.stateStack; + var i = stateStack.length - 1; + while (!colorSpace && i >= 0) { + colorSpace = stateStack[i--].colorSpace; + } + } + + if (this.current.colorSpace == "Pattern") { + var patternName = arguments[0]; + if (IsName(patternName)) { + var xref = this.xref; + var patternRes = xref.fetchIfRef(this.res.get("Pattern")); + if (!patternRes) + error("Unable to find pattern resource"); + + var pattern = xref.fetchIfRef(patternRes.get(patternName.name)); - ++ + const types = [null, this.tilingFill]; + var typeNum = pattern.dict.get("PatternType"); + var patternFn = types[typeNum]; + if (!patternFn) + error("Unhandled pattern type"); + patternFn.call(this, pattern); + } + } else { + // TODO real impl + this.setFillColor.apply(this, arguments); + } + }, + tilingFill: function(pattern) { + function applyMatrix(point, m) { + var x = point[0] * m[0] + point[1] * m[2] + m[4]; + var y = point[0] * m[1] + point[1] * m[3] + m[5]; + return [x,y]; + }; + + function multiply(m, tm) { + var a = m[0] * tm[0] + m[1] * tm[2]; + var b = m[0] * tm[1] + m[1] * tm[3]; + var c = m[2] * tm[0] + m[3] * tm[2]; + var d = m[2] * tm[1] + m[3] * tm[3]; + var e = m[4] * tm[0] + m[5] * tm[2] + tm[4]; + var f = m[4] * tm[1] + m[5] * tm[3] + tm[5]; + return [a, b, c, d, e, f] + }; + + this.save(); + var dict = pattern.dict; + var ctx = this.ctx; + + var paintType = dict.get("PaintType"); + switch (paintType) { + case PAINT_TYPE_COLORED: + // should go to default for color space + ctx.fillStyle = this.makeCssRgb(1, 1, 1); + ctx.strokeStyle = this.makeCssRgb(0, 0, 0); + break; + case PAINT_TYPE_UNCOLORED: + default: + error("Unsupported paint type"); + } + + TODO("TilingType"); + + var matrix = dict.get("Matrix") || IDENTITY_MATRIX; + + var bbox = dict.get("BBox"); + var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3]; + + var xstep = dict.get("XStep"); + var ystep = dict.get("YStep"); + + // top left corner should correspond to the top left of the bbox + var topLeft = applyMatrix([x0,y0], matrix); + // we want the canvas to be as large as the step size + var botRight = applyMatrix([x0 + xstep, y0 + ystep], matrix); - ++ + var tmpCanvas = document.createElement("canvas"); + tmpCanvas.width = Math.ceil(botRight[0] - topLeft[0]); + tmpCanvas.height = Math.ceil(botRight[1] - topLeft[1]); - ++ + // set the new canvas element context as the graphics context + var tmpCtx = tmpCanvas.getContext("2d"); + var savedCtx = ctx; + this.ctx = tmpCtx; + + // normalize transform matrix so each step + // takes up the entire tmpCanvas (need to remove white borders) + if (matrix[1] === 0 && matrix[2] === 0) { - matrix[0] = tmpCanvas.width / xstep; ++ matrix[0] = tmpCanvas.width / xstep; + matrix[3] = tmpCanvas.height / ystep; + topLeft = applyMatrix([x0,y0], matrix); + } + + // move the top left corner of bounding box to [0,0] + matrix = multiply(matrix, [1, 0, 0, 1, -topLeft[0], -topLeft[1]]); - ++ + this.transform.apply(this, matrix); - ++ + if (bbox && IsArray(bbox) && 4 == bbox.length) { + this.rectangle.apply(this, bbox); + this.clip(); + this.endPath(); + } + + var xref = this.xref; + var res = xref.fetchIfRef(dict.get("Resources")); + if (!pattern.code) + pattern.code = this.compile(pattern, xref, res, []); + this.execute(pattern.code, xref, res); - ++ + this.ctx = savedCtx; + this.restore(); + + TODO("Inverse pattern is painted"); + var pattern = this.ctx.createPattern(tmpCanvas, "repeat"); + this.ctx.fillStyle = pattern; }, setStrokeGray: function(gray) { this.setStrokeRGBColor(gray, gray, gray); @@@ -2465,15 -2753,19 +2785,19 @@@ var fn = new PDFFunction(this.xref, fnObj); var gradient = this.ctx.createLinearGradient(x0, y0, x1, y1); - ++ + // 10 samples seems good enough for now, but probably won't work + // if there are sharp color changes. Ideally, we would implement + // the spec faithfully and add lossless optimizations. var step = (t1 - t0) / 10; -- ++ for (var i = t0; i <= t1; i += step) { var c = fn.func([i]); gradient.addColorStop(i, this.makeCssRgb.apply(this, c)); } this.ctx.fillStyle = gradient; -- ++ // HACK to draw the gradient onto an infinite rectangle. // PDF gradients are drawn across the entire image while // Canvas only allows gradients to be drawn in a rectangle