diff --git a/LICENSE b/LICENSE index 4d5a7b2dc..81658476c 100644 --- a/LICENSE +++ b/LICENSE @@ -5,6 +5,8 @@ Chris G Jones Shaon Barman Vivien Nicolas <21@vingtetun.org> + Justin D'Arcangelo + Yury Delendik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -22,4 +24,4 @@ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/fonts.js b/fonts.js index 273ef5ea6..53db045f0 100644 --- a/fonts.js +++ b/fonts.js @@ -8,11 +8,6 @@ */ 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 */ @@ -45,7 +40,6 @@ var Fonts = { }, set active(aName) { - fontName = aName; this._active = this[aName]; }, @@ -70,6 +64,10 @@ var Fonts = { var uc = encoding[ch]; if (uc instanceof Name) // we didn't convert the glyph yet uc = encoding[ch] = GlyphsUnicode[uc.name]; + if (uc > 0xffff) { // handle surrogate pairs + ret += String.fromCharCode(uc & 0xffff); + uc >>= 16; + } ret += String.fromCharCode(uc); } @@ -88,366 +86,368 @@ var Fonts = { * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); * type1Font.bind(); */ -var Font = function(aName, aFile, aProperties) { - this.name = aName; - this.encoding = aProperties.encoding; +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 - if (Fonts[aName]) { - this.font = Fonts[aName].data; - return; - } - fontCount++; + // If the font has already been decoded simply return it + if (Fonts[aName]) { + this.font = Fonts[aName].data; + return; + } + fontCount++; + fontName = aName; + + if (aProperties.ignore || kDisableFonts) { + Fonts[aName] = { + data: aFile, + loading: false, + properties: {}, + cache: Object.create(null) + } + 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": + // 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; + + default: + warn("Font " + aProperties.type + " is not supported"); + break; + } - if (aProperties.ignore || kDisableFonts) { Fonts[aName] = { - data: aFile, - loading: false, - properties: {}, + data: this.font, + properties: aProperties, + loading: true, cache: Object.create(null) } - return; - } - 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; + /** + * 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, - case "TrueType": - this.mimetype = "font/opentype"; - var ttf = new TrueType(aName, aFile, aProperties); - this.font = ttf.data; - break; + bind: function font_bind() { + var data = this.font; - default: - warn("Font " + aProperties.type + " is not supported"); - break; - } + // 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]); - Fonts[aName] = { - data: this.font, - properties: aProperties, - loading: true, - cache: Object.create(null) - } + var dataBase64 = window.btoa(str); + var fontName = this.name; - // Attach the font to the document - this.bind(); -}; + /** 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; -/** - * 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); - } - - 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); + 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); } - } - ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - var textWidth = ctx.measureText(testString).width; - if (debug) - ctx.fillText(testString, 20, 20); + 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); - var interval = window.setInterval(function canvasInterval(self) { - this.start = this.start || Date.now(); + // 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) { + for (var i = 0; i < charset.length; i++) { + var unicode = GlyphsUnicode[charset[i]]; + if (!unicode) + continue; + testString += String.fromCharCode(unicode); + } + } 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; - } + var textWidth = ctx.measureText(testString).width; if (debug) - ctx.fillText(testString, 20, 50); - }, 50, this); + ctx.fillText(testString, 20, 20); - /** Hack end */ + var interval = window.setInterval(function canvasInterval(self) { + this.start = this.start || Date.now(); + ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - // 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); - }, + // 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; + } - _createOpenTypeHeader: function font_createOpenTypeHeader(aFile, aOffsets, aNumTables) { - // sfnt version (4 bytes) - var version = [0x4F, 0x54, 0x54, 0X4F]; + if (debug) + ctx.fillText(testString, 20, 50); + }, 50, this); - // numTables (2 bytes) - var numTables = aNumTables; + /** Hack end */ - // searchRange (2 bytes) - var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); - var searchRange = tablesMaxPower2 * 16; + // 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); + }, - // entrySelector (2 bytes) - var entrySelector = Math.log(tablesMaxPower2) / Math.log(2); + cover: function font_cover(aName, aFont, aProperties) { + var otf = Uint8Array(kMaxFontFileSize); - // 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 Uint16Array(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 = []; - 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 s2a(s) { + var a = []; + for (var i = 0; i < s.length; ++i) + a[i] = s.charCodeAt(i); + return a; } - } - // 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); - }, - - _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 - ]; - - var name = [ - 0x00, 0x00, // format - 0x00, 0x0A, // Number of names Record - 0x00, 0x7E // Storage - ]; - - // Build the name records field - var strOffset = 0; - for (var i = 0; i < names.length; i++) { - var str = names[i]; - - var nameRecord = [ - 0x00, 0x01, // platform ID - 0x00, 0x00, // encoding ID - 0x00, 0x00, // language ID - 0x00, 0x00 // name ID - ]; - - nameRecord = nameRecord.concat( - FontsUtils.integerToBytes(str.length, 2), - FontsUtils.integerToBytes(strOffset, 2) - ); - name = name.concat(nameRecord); - - strOffset += str.length; - } - - // 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)); + function s16(value) { + return String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); } - name = name.concat(strBytes); - } - return name; - }, + function s32(value) { + return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) + + String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); + } - cover: function font_cover(aName, aFont, aProperties) { - var otf = new Uint8Array(kMaxFontFileSize); + function createOpenTypeHeader(aFile, aOffsets, numTables) { + var header = ""; - // Required Tables - var CFF = aFont.data, // PostScript Font Program + // sfnt version (4 bytes) + header += "\x4F\x54\x54\x4F"; + + // numTables (2 bytes) + header += s16(numTables); + + // searchRange (2 bytes) + var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); + var searchRange = tablesMaxPower2 * 16; + header += s16(searchRange); + + // entrySelector (2 bytes) + header += s16(Math.log(tablesMaxPower2) / Math.log(2)); + + // rangeShift (2 bytes) + header += s16(numTables * 16 - searchRange); + + aFile.set(s2a(header), aOffsets.currentOffset); + aOffsets.currentOffset += header.length; + aOffsets.virtualOffset += header.length; + } + + 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) + + 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; + } + + 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; + } + + name += names; + return name; + } + + 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; + } + + 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); + } + + startCount += "\xFF\xFF"; + endCount += "\xFF\xFF"; + idDeltas += "\x00\x01"; + idRangeOffsets += "\x00\x00"; + + return s2a(cmap + endCount + "\x00\x00" + startCount + + idDeltas + idRangeOffsets + glyphsIds); + } + + // Required Tables + var CFF = aFont.data, // PostScript Font Program OS2 = [], // OS/2 and Windows Specific metrics cmap = [], // Character to glyphs mapping head = [], // Font eader @@ -456,174 +456,171 @@ Font.prototype = { maxp = [], // Maximum profile name = [], // Naming tables post = []; // PostScript informations - var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, 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) - }; + // 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); + // For files with only one font the offset table is the first thing of the + // file + 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); + // 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 = [ - 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); + /** 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); + //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); + /** CMAP */ + cmap = createCMAPTable(charstrings); + 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); + /** 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 = [].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); + /** 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 = [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); + /** 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 = 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; + } + + var fontData = []; + for (var i = 0; i < offsets.currentOffset; i++) + fontData.push(otf[i]); + return fontData; } - 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); - - // 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; - } -}; + }; + return constructor; +})(); /** * FontsUtils is a static class dedicated to hold codes that are not related @@ -705,10 +702,8 @@ var TrueType = function(aName, aFile, aProperties) { if (table.tag == "cmap") originalCMAP = table; - requiredTables.splice(index, 1); + tables.push(table); } - - tables.push(table); } // If any tables are still in the array this means some required tables are @@ -787,7 +782,7 @@ var TrueType = function(aName, aFile, aProperties) { 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 @@ -830,7 +825,6 @@ var TrueType = function(aName, aFile, aProperties) { data: OS2 }); - // Tables needs to be written by ascendant alphabetic order tables.sort(function(a, b) { return a.tag > b.tag; @@ -866,9 +860,10 @@ var TrueType = function(aName, aFile, aProperties) { this.data = fontData; return; } 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,79 +896,8 @@ TrueType.prototype = { 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; @@ -1052,6 +976,77 @@ TrueType.prototype = { 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 + } } }; @@ -1245,8 +1240,8 @@ var Type1Parser = function() { this.extractFontProgram = function t1_extractFontProgram(aStream) { var eexecString = decrypt(aStream, kEexecEncryptionKey, 4); var subrs = [], glyphs = []; - var inSubrs = false; var inGlyphs = false; + var inSubrs = false; var glyph = ""; var token = ""; @@ -1641,6 +1636,7 @@ CFF.prototype = { } var charstringsIndex = this.createCFFIndexHeader([[0x40, 0x0E]].concat(glyphs), true); + charstringsIndex = charstringsIndex.join(" ").split(" "); // XXX why? //Top Dict Index var topDictIndex = [ @@ -1674,6 +1670,7 @@ CFF.prototype = { var privateOffset = charstringsOffset + charstringsIndex.length; topDictIndex = topDictIndex.concat(this.encodeNumber(privateOffset)); topDictIndex.push(18); // Private + topDictIndex = topDictIndex.join(" ").split(" "); var indexes = [ topDictIndex, stringsIndex, @@ -1703,6 +1700,7 @@ CFF.prototype = { 139, 12, 14, 28, 0, 55, 19 ]); + privateData = privateData.join(" ").split(" "); cff.set(privateData, currentOffset); currentOffset += privateData.length; diff --git a/glyphlist.js b/glyphlist.js index f638ff888..85ab876f9 100644 --- a/glyphlist.js +++ b/glyphlist.js @@ -1,3 +1,6 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + "use strict"; var GlyphsUnicode = { @@ -1502,27 +1505,27 @@ var GlyphsUnicode = { dalet: 0x05D3, daletdagesh: 0xFB33, daletdageshhebrew: 0xFB33, - dalethatafpatah: "05D3 05B2", - dalethatafpatahhebrew: "05D3 05B2", - dalethatafsegol: "05D3 05B1", - dalethatafsegolhebrew: "05D3 05B1", + dalethatafpatah: 0x05D305B2, + dalethatafpatahhebrew: 0x05D305B2, + dalethatafsegol: 0x05D305B1, + dalethatafsegolhebrew: 0x05D305B1, dalethebrew: 0x05D3, - dalethiriq: "05D3 05B4", - dalethiriqhebrew: "05D3 05B4", - daletholam: "05D3 05B9", - daletholamhebrew: "05D3 05B9", - daletpatah: "05D3 05B7", - daletpatahhebrew: "05D3 05B7", - daletqamats: "05D3 05B8", - daletqamatshebrew: "05D3 05B8", - daletqubuts: "05D3 05BB", - daletqubutshebrew: "05D3 05BB", - daletsegol: "05D3 05B6", - daletsegolhebrew: "05D3 05B6", - daletsheva: "05D3 05B0", - daletshevahebrew: "05D3 05B0", - dalettsere: "05D3 05B5", - dalettserehebrew: "05D3 05B5", + dalethiriq: 0x05D305B4, + dalethiriqhebrew: 0x05D305B4, + daletholam: 0x05D305B9, + daletholamhebrew: 0x05D305B9, + daletpatah: 0x05D305B7, + daletpatahhebrew: 0x05D305B7, + daletqamats: 0x05D305B8, + daletqamatshebrew: 0x05D305B8, + daletqubuts: 0x05D305BB, + daletqubutshebrew: 0x05D305BB, + daletsegol: 0x05D305B6, + daletsegolhebrew: 0x05D305B6, + daletsheva: 0x05D305B0, + daletshevahebrew: 0x05D305B0, + dalettsere: 0x05D305B5, + dalettserehebrew: 0x05D305B5, dalfinalarabic: 0xFEAA, dammaarabic: 0x064F, dammalowarabic: 0x064F, @@ -1839,10 +1842,10 @@ var GlyphsUnicode = { finalkafdagesh: 0xFB3A, finalkafdageshhebrew: 0xFB3A, finalkafhebrew: 0x05DA, - finalkafqamats: "05DA 05B8", - finalkafqamatshebrew: "05DA 05B8", - finalkafsheva: "05DA 05B0", - finalkafshevahebrew: "05DA 05B0", + finalkafqamats: 0x05DA05B8, + finalkafqamatshebrew: 0x05DA05B8, + finalkafsheva: 0x05DA05B0, + finalkafshevahebrew: 0x05DA05B0, finalmem: 0x05DD, finalmemhebrew: 0x05DD, finalnun: 0x05DF, @@ -2031,14 +2034,14 @@ var GlyphsUnicode = { hakatakanahalfwidth: 0xFF8A, halantgurmukhi: 0x0A4D, hamzaarabic: 0x0621, - hamzadammaarabic: "0621 064F", - hamzadammatanarabic: "0621 064C", - hamzafathaarabic: "0621 064E", - hamzafathatanarabic: "0621 064B", + hamzadammaarabic: 0x0621064F, + hamzadammatanarabic: 0x0621064C, + hamzafathaarabic: 0x0621064E, + hamzafathatanarabic: 0x0621064B, hamzalowarabic: 0x0621, - hamzalowkasraarabic: "0621 0650", - hamzalowkasratanarabic: "0621 064D", - hamzasukunarabic: "0621 0652", + hamzalowkasraarabic: 0x06210650, + hamzalowkasratanarabic: 0x0621064D, + hamzasukunarabic: 0x06210652, hangulfiller: 0x3164, hardsigncyrillic: 0x044A, harpoonleftbarbup: 0x21BC, @@ -2470,10 +2473,10 @@ var GlyphsUnicode = { lameddagesh: 0xFB3C, lameddageshhebrew: 0xFB3C, lamedhebrew: 0x05DC, - lamedholam: "05DC 05B9", + lamedholam: 0x05DC05B9, lamedholamdagesh: "05DC 05B9 05BC", lamedholamdageshhebrew: "05DC 05B9 05BC", - lamedholamhebrew: "05DC 05B9", + lamedholamhebrew: 0x05DC05B9, lamfinalarabic: 0xFEDE, lamhahinitialarabic: 0xFCCA, laminitialarabic: 0xFEDF, @@ -2781,7 +2784,7 @@ var GlyphsUnicode = { noonfinalarabic: 0xFEE6, noonghunnaarabic: 0x06BA, noonghunnafinalarabic: 0xFB9F, - noonhehinitialarabic: "FEE7 FEEC", + noonhehinitialarabic: 0xFEE7FEEC, nooninitialarabic: 0xFEE7, noonjeeminitialarabic: 0xFCD2, noonjeemisolatedarabic: 0xFC4B, @@ -3153,27 +3156,27 @@ var GlyphsUnicode = { qof: 0x05E7, qofdagesh: 0xFB47, qofdageshhebrew: 0xFB47, - qofhatafpatah: "05E7 05B2", - qofhatafpatahhebrew: "05E7 05B2", - qofhatafsegol: "05E7 05B1", - qofhatafsegolhebrew: "05E7 05B1", + qofhatafpatah: 0x05E705B2, + qofhatafpatahhebrew: 0x05E705B2, + qofhatafsegol: 0x05E705B1, + qofhatafsegolhebrew: 0x05E705B1, qofhebrew: 0x05E7, - qofhiriq: "05E7 05B4", - qofhiriqhebrew: "05E7 05B4", - qofholam: "05E7 05B9", - qofholamhebrew: "05E7 05B9", - qofpatah: "05E7 05B7", - qofpatahhebrew: "05E7 05B7", - qofqamats: "05E7 05B8", - qofqamatshebrew: "05E7 05B8", - qofqubuts: "05E7 05BB", - qofqubutshebrew: "05E7 05BB", - qofsegol: "05E7 05B6", - qofsegolhebrew: "05E7 05B6", - qofsheva: "05E7 05B0", - qofshevahebrew: "05E7 05B0", - qoftsere: "05E7 05B5", - qoftserehebrew: "05E7 05B5", + qofhiriq: 0x05E705B4, + qofhiriqhebrew: 0x05E705B4, + qofholam: 0x05E705B9, + qofholamhebrew: 0x05E705B9, + qofpatah: 0x05E705B7, + qofpatahhebrew: 0x05E705B7, + qofqamats: 0x05E705B8, + qofqamatshebrew: 0x05E705B8, + qofqubuts: 0x05E705BB, + qofqubutshebrew: 0x05E705BB, + qofsegol: 0x05E705B6, + qofsegolhebrew: 0x05E705B6, + qofsheva: 0x05E705B0, + qofshevahebrew: 0x05E705B0, + qoftsere: 0x05E705B5, + qoftserehebrew: 0x05E705B5, qparen: 0x24AC, quarternote: 0x2669, qubuts: 0x05BB, @@ -3252,27 +3255,27 @@ var GlyphsUnicode = { rekatakanahalfwidth: 0xFF9A, resh: 0x05E8, reshdageshhebrew: 0xFB48, - reshhatafpatah: "05E8 05B2", - reshhatafpatahhebrew: "05E8 05B2", - reshhatafsegol: "05E8 05B1", - reshhatafsegolhebrew: "05E8 05B1", + reshhatafpatah: 0x05E805B2, + reshhatafpatahhebrew: 0x05E805B2, + reshhatafsegol: 0x05E805B1, + reshhatafsegolhebrew: 0x05E805B1, reshhebrew: 0x05E8, - reshhiriq: "05E8 05B4", - reshhiriqhebrew: "05E8 05B4", - reshholam: "05E8 05B9", - reshholamhebrew: "05E8 05B9", - reshpatah: "05E8 05B7", - reshpatahhebrew: "05E8 05B7", - reshqamats: "05E8 05B8", - reshqamatshebrew: "05E8 05B8", - reshqubuts: "05E8 05BB", - reshqubutshebrew: "05E8 05BB", - reshsegol: "05E8 05B6", - reshsegolhebrew: "05E8 05B6", - reshsheva: "05E8 05B0", - reshshevahebrew: "05E8 05B0", - reshtsere: "05E8 05B5", - reshtserehebrew: "05E8 05B5", + reshhiriq: 0x05E805B4, + reshhiriqhebrew: 0x05E805B4, + reshholam: 0x05E805B9, + reshholamhebrew: 0x05E805B9, + reshpatah: 0x05E805B7, + reshpatahhebrew: 0x05E805B7, + reshqamats: 0x05E805B8, + reshqamatshebrew: 0x05E805B8, + reshqubuts: 0x05E805BB, + reshqubutshebrew: 0x05E805BB, + reshsegol: 0x05E805B6, + reshsegolhebrew: 0x05E805B6, + reshsheva: 0x05E805B0, + reshshevahebrew: 0x05E805B0, + reshtsere: 0x05E805B5, + reshtserehebrew: 0x05E805B5, reversedtilde: 0x223D, reviahebrew: 0x0597, reviamugrashhebrew: 0x0597, @@ -3471,7 +3474,7 @@ var GlyphsUnicode = { shaddadammaarabic: 0xFC61, shaddadammatanarabic: 0xFC5E, shaddafathaarabic: 0xFC60, - shaddafathatanarabic: "0651 064B", + shaddafathatanarabic: 0x0651064B, shaddakasraarabic: 0xFC62, shaddakasratanarabic: 0xFC5F, shade: 0x2592, @@ -3668,7 +3671,7 @@ var GlyphsUnicode = { tchehfinalarabic: 0xFB7B, tchehinitialarabic: 0xFB7C, tchehmedialarabic: 0xFB7D, - tchehmeeminitialarabic: "FB7C FEE4", + tchehmeeminitialarabic: 0xFB7CFEE4, tcircle: 0x24E3, tcircumflexbelow: 0x1E71, tcommaaccent: 0x0163, diff --git a/images/buttons.png b/images/buttons.png new file mode 100644 index 000000000..682212660 Binary files /dev/null and b/images/buttons.png differ diff --git a/images/combobox.png b/images/combobox.png new file mode 100644 index 000000000..f1527d6a2 Binary files /dev/null and b/images/combobox.png differ diff --git a/images/source/Buttons.psd.zip b/images/source/Buttons.psd.zip new file mode 100644 index 000000000..528e6ee3c Binary files /dev/null and b/images/source/Buttons.psd.zip differ diff --git a/images/source/ComboBox.psd.zip b/images/source/ComboBox.psd.zip new file mode 100644 index 000000000..232604c75 Binary files /dev/null and b/images/source/ComboBox.psd.zip differ diff --git a/multi-page-viewer.css b/multi-page-viewer.css new file mode 100644 index 000000000..c96567465 --- /dev/null +++ b/multi-page-viewer.css @@ -0,0 +1,184 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / +/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ + +body { + background-color: #929292; + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; + margin: 0px; + padding: 0px; +} + +canvas { + box-shadow: 0px 4px 10px #000; + -moz-box-shadow: 0px 4px 10px #000; + -webkit-box-shadow: 0px 4px 10px #000; +} + +span { + font-size: 0.8em; +} + +.control { + display: inline-block; + float: left; + margin: 0px 20px 0px 0px; + padding: 0px 4px 0px 0px; +} + +.control > input { + float: left; + margin: 0px 2px 0px 0px; +} + +.control > span { + cursor: default; + float: left; + height: 18px; + margin: 5px 2px 0px; + padding: 0px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.control .label { + clear: both; + float: left; + font-size: 0.65em; + margin: 2px 0px 0px; + position: relative; + text-align: center; + width: 100%; +} + +.page { + width: 816px; + height: 1056px; + margin: 10px auto; +} + +#controls { + background-color: #eee; + border-bottom: 1px solid #666; + padding: 4px 0px 0px 8px; + position:fixed; + left: 0px; + top: 0px; + height: 40px; + width: 100%; + box-shadow: 0px 2px 8px #000; + -moz-box-shadow: 0px 2px 8px #000; + -webkit-box-shadow: 0px 2px 8px #000; +} + +#controls input { + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; +} + +#previousPageButton { + background: url('images/buttons.png') no-repeat 0px -23px; + cursor: pointer; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#previousPageButton.down { + background: url('images/buttons.png') no-repeat 0px -46px; +} + +#previousPageButton.disabled { + background: url('images/buttons.png') no-repeat 0px 0px; +} + +#nextPageButton { + background: url('images/buttons.png') no-repeat -28px -23px; + cursor: pointer; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#nextPageButton.down { + background: url('images/buttons.png') no-repeat -28px -46px; +} + +#nextPageButton.disabled { + background: url('images/buttons.png') no-repeat -28px 0px; +} + +#scaleComboBoxInput { + background: url('images/combobox.png') no-repeat 0px -23px; + display: inline-block; + float: left; + margin: 0px; + width: 35px; + height: 23px; +} + +#scaleComboBoxInput input { + background: none; + border: 0px; + margin: 3px 2px 0px; + width: 31px; +} + +#scaleComboBoxButton { + background: url('images/combobox.png') no-repeat -41px -23px; + cursor: pointer; + display: inline-block; + float: left; + margin: 0px; + width: 21px; + height: 23px; +} + +#scaleComboBoxButton.down { + background: url('images/combobox.png') no-repeat -41px -46px; +} + +#scaleComboBoxButton.disabled { + background: url('images/combobox.png') no-repeat -41px 0px; +} + +#scaleComboBoxList { + background-color: #fff; + border: 1px solid #666; + clear: both; + position: relative; + display: none; + top: -20px; + width: 48px; +} + +#scaleComboBoxList > ul { + list-style: none; + padding: 0px; + margin: 0px; +} + +#scaleComboBoxList > ul > li { + display: inline-block; + cursor: pointer; + width: 100%; +} + +#scaleComboBoxList > ul > li:hover { + background-color: #09f; + color: #fff; +} + +#pageNumber, #scale { + text-align: right; +} + +#viewer { + margin: 44px 0px 0px; + padding: 8px 0px; +} diff --git a/multi-page-viewer.html b/multi-page-viewer.html new file mode 100644 index 000000000..692cfb1c4 --- /dev/null +++ b/multi-page-viewer.html @@ -0,0 +1,40 @@ + + + +pdf.js Multi-Page Viewer + + + + + + + +
+ + + Previous/Next + + + + / + -- + Page Number + + + + Zoom +
+
    +
  • 50%
  • +
  • 75%
  • +
  • 100%
  • +
  • 125%
  • +
  • 150%
  • +
  • 200%
  • +
+
+
+
+
+ + diff --git a/multi-page-viewer.js b/multi-page-viewer.js new file mode 100644 index 000000000..6cb46a08a --- /dev/null +++ b/multi-page-viewer.js @@ -0,0 +1,400 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / +/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ + +"use strict"; + +var PDFViewer = { + queryParams: {}, + + element: null, + + previousPageButton: null, + nextPageButton: null, + pageNumberInput: null, + scaleInput: null, + + willJumpToPage: false, + + pdf: null, + + url: 'compressed.tracemonkey-pldi-09.pdf', + pageNumber: 1, + numberOfPages: 1, + + scale: 1.0, + + pageWidth: function() { + return 816 * PDFViewer.scale; + }, + + pageHeight: function() { + return 1056 * PDFViewer.scale; + }, + + lastPagesDrawn: [], + + visiblePages: function() { + var pageHeight = PDFViewer.pageHeight() + 20; // Add 20 for the margins. + var windowTop = window.pageYOffset; + var windowBottom = window.pageYOffset + window.innerHeight; + var pageStartIndex = Math.floor(windowTop / pageHeight); + var pageStopIndex = Math.ceil(windowBottom / pageHeight); + + var pages = []; + + for (var i = pageStartIndex; i <= pageStopIndex; i++) { + pages.push(i + 1); + } + + return pages; + }, + + createPage: function(num) { + var anchor = document.createElement('a'); + anchor.name = '' + num; + + var div = document.createElement('div'); + div.id = 'pageContainer' + num; + div.className = 'page'; + div.style.width = PDFViewer.pageWidth() + 'px'; + div.style.height = PDFViewer.pageHeight() + 'px'; + + PDFViewer.element.appendChild(anchor); + PDFViewer.element.appendChild(div); + }, + + removePage: function(num) { + var div = document.getElementById('pageContainer' + num); + + if (div && div.hasChildNodes()) { + while (div.childNodes.length > 0) { + div.removeChild(div.firstChild); + } + } + }, + + drawPage: function(num) { + if (PDFViewer.pdf) { + var page = PDFViewer.pdf.getPage(num); + var div = document.getElementById('pageContainer' + num); + + if (div && !div.hasChildNodes()) { + var canvas = document.createElement('canvas'); + canvas.id = 'page' + num; + canvas.mozOpaque = true; + + // Canvas dimensions must be specified in CSS pixels. CSS pixels + // are always 96 dpi. These dimensions are 8.5in x 11in at 96dpi. + canvas.width = PDFViewer.pageWidth(); + canvas.height = PDFViewer.pageHeight(); + + 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); + var fonts = []; + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + page.compile(gfx, fonts); + + var fontsReady = true; + + // Inspect fonts and translate the missing one + var fontCount = fonts.length; + + for (var i = 0; i < fontCount; i++) { + var font = fonts[i]; + + if (Fonts[font.name]) { + fontsReady = fontsReady && !Fonts[font.name].loading; + continue; + } + + new Font(font.name, font.file, font.properties); + + fontsReady = false; + } + + var pageInterval; + var delayLoadFont = function() { + for (var i = 0; i < fontCount; i++) { + if (Fonts[font.name].loading) { + return; + } + } + + clearInterval(pageInterval); + + PDFViewer.drawPage(num); + } + + if (!fontsReady) { + pageInterval = setInterval(delayLoadFont, 10); + return; + } + + page.display(gfx); + div.appendChild(canvas); + } + } + }, + + changeScale: function(num) { + while (PDFViewer.element.childNodes.length > 0) { + PDFViewer.element.removeChild(PDFViewer.element.firstChild); + } + + PDFViewer.scale = num / 100; + + if (PDFViewer.pdf) { + for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + } + } + + PDFViewer.scaleInput.value = Math.floor(PDFViewer.scale * 100) + '%'; + }, + + goToPage: function(num) { + if (1 <= num && num <= PDFViewer.numberOfPages) { + PDFViewer.pageNumber = num; + PDFViewer.pageNumberInput.value = PDFViewer.pageNumber; + PDFViewer.willJumpToPage = true; + + document.location.hash = PDFViewer.pageNumber; + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? + 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? + 'disabled' : ''; + } + }, + + goToPreviousPage: function() { + if (PDFViewer.pageNumber > 1) { + PDFViewer.goToPage(--PDFViewer.pageNumber); + } + }, + + goToNextPage: function() { + if (PDFViewer.pageNumber < PDFViewer.numberOfPages) { + PDFViewer.goToPage(++PDFViewer.pageNumber); + } + }, + + open: function(url) { + PDFViewer.url = url; + document.title = url; + + var 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; + + PDFViewer.pdf = new PDFDoc(new Stream(data)); + PDFViewer.numberOfPages = PDFViewer.pdf.numPages; + document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); + + for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + } + } + }; + + req.send(null); + } +}; + +window.onload = function() { + + // Parse the URL query parameters into a cached object. + PDFViewer.queryParams = function() { + 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; + }(); + + PDFViewer.element = document.getElementById('viewer'); + + PDFViewer.pageNumberInput = document.getElementById('pageNumber'); + PDFViewer.pageNumberInput.onkeydown = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // Up arrow key. + if (charCode === 38) { + PDFViewer.goToNextPage(); + this.select(); + } + + // Down arrow key. + else if (charCode === 40) { + PDFViewer.goToPreviousPage(); + this.select(); + } + + // All other non-numeric keys (excluding Left arrow, Right arrow, + // Backspace, and Delete keys). + else if ((charCode < 48 || charCode > 57) && + charCode !== 8 && // Backspace + charCode !== 46 && // Delete + charCode !== 37 && // Left arrow + charCode !== 39 // Right arrow + ) { + return false; + } + + return true; + }; + PDFViewer.pageNumberInput.onkeyup = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // All numeric keys, Backspace, and Delete. + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || // Backspace + charCode === 46 // Delete + ) { + PDFViewer.goToPage(this.value); + } + + this.focus(); + }; + + PDFViewer.previousPageButton = document.getElementById('previousPageButton'); + PDFViewer.previousPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToPreviousPage(); + } + }; + PDFViewer.previousPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.previousPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.previousPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.nextPageButton = document.getElementById('nextPageButton'); + PDFViewer.nextPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToNextPage(); + } + }; + PDFViewer.nextPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.nextPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.nextPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.scaleInput = document.getElementById('scale'); + PDFViewer.scaleInput.buttonElement = document.getElementById('scaleComboBoxButton'); + PDFViewer.scaleInput.buttonElement.listElement = document.getElementById('scaleComboBoxList'); + PDFViewer.scaleInput.onchange = function(evt) { + PDFViewer.changeScale(parseInt(this.value)); + }; + + PDFViewer.scaleInput.buttonElement.onclick = function(evt) { + this.listElement.style.display = (this.listElement.style.display === 'block') ? 'none' : 'block'; + }; + PDFViewer.scaleInput.buttonElement.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.scaleInput.buttonElement.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.scaleInput.buttonElement.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + var listItems = PDFViewer.scaleInput.buttonElement.listElement.getElementsByTagName('LI'); + + for (var i = 0; i < listItems.length; i++) { + var listItem = listItems[i]; + listItem.onclick = function(evt) { + PDFViewer.changeScale(parseInt(this.innerHTML)); + PDFViewer.scaleInput.buttonElement.listElement.style.display = 'none'; + }; + } + + PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; + PDFViewer.scale = parseInt(PDFViewer.scaleInput.value) / 100 || 1.0; + PDFViewer.open(PDFViewer.queryParams.file || PDFViewer.url); + + window.onscroll = function(evt) { + var lastPagesDrawn = PDFViewer.lastPagesDrawn; + var visiblePages = PDFViewer.visiblePages(); + + var pagesToDraw = []; + var pagesToKeep = []; + var pagesToRemove = []; + + var i; + + // Determine which visible pages were not previously drawn. + for (i = 0; i < visiblePages.length; i++) { + if (lastPagesDrawn.indexOf(visiblePages[i]) === -1) { + pagesToDraw.push(visiblePages[i]); + PDFViewer.drawPage(visiblePages[i]); + } else { + pagesToKeep.push(visiblePages[i]); + } + } + + // Determine which previously drawn pages are no longer visible. + for (i = 0; i < lastPagesDrawn.length; i++) { + if (visiblePages.indexOf(lastPagesDrawn[i]) === -1) { + pagesToRemove.push(lastPagesDrawn[i]); + PDFViewer.removePage(lastPagesDrawn[i]); + } + } + + PDFViewer.lastPagesDrawn = pagesToDraw.concat(pagesToKeep); + + // Update the page number input with the current page number. + if (!PDFViewer.willJumpToPage && visiblePages.length > 0) { + PDFViewer.pageNumber = PDFViewer.pageNumberInput.value = visiblePages[0]; + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? + 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? + 'disabled' : ''; + } else { + PDFViewer.willJumpToPage = false; + } + }; +}; diff --git a/pdf.js b/pdf.js index d39579f74..2ce6b48a8 100644 --- a/pdf.js +++ b/pdf.js @@ -50,7 +50,7 @@ function shadow(obj, prop, value) { 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; @@ -115,7 +115,7 @@ var Stream = (function() { var StringStream = (function() { function constructor(str) { var length = str.length; - var bytes = new Uint8Array(length); + var bytes = Uint8Array(length); for (var n = 0; n < length; ++n) bytes[n] = str.charCodeAt(n); Stream.call(this, bytes); @@ -127,11 +127,11 @@ var StringStream = (function() { })(); var FlateStream = (function() { - const codeLenCodeMap = new Uint32Array([ + const codeLenCodeMap = Uint32Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]); - const lengthDecode = new Uint32Array([ + const lengthDecode = Uint32Array([ 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, @@ -139,7 +139,7 @@ var FlateStream = (function() { 0x00102, 0x00102, 0x00102 ]); - const distDecode = new Uint32Array([ + const distDecode = Uint32Array([ 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, @@ -147,7 +147,7 @@ var FlateStream = (function() { 0xd4001, 0xd6001 ]); - const fixedLitCodeTab = [new Uint32Array([ + const fixedLitCodeTab = [Uint32Array([ 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, @@ -224,7 +224,7 @@ var FlateStream = (function() { 0x900ff ]), 9]; - const fixedDistCodeTab = [new Uint32Array([ + const fixedDistCodeTab = [Uint32Array([ 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, @@ -300,7 +300,7 @@ var FlateStream = (function() { var size = 512; while (size < requested) size <<= 1; - var buffer2 = new Uint8Array(size); + var buffer2 = Uint8Array(size); for (var i = 0; i < current; ++i) buffer2[i] = buffer[i]; return this.buffer = buffer2; @@ -308,7 +308,7 @@ var FlateStream = (function() { getByte: function() { var bufferLength = this.bufferLength; var pos = this.pos; - if (bufferLength == pos) { + if (bufferLength <= pos) { if (this.eof) return; this.readBlock(); @@ -333,7 +333,7 @@ var FlateStream = (function() { lookChar: function() { var bufferLength = this.bufferLength; var pos = this.pos; - if (bufferLength == pos) { + if (bufferLength <= pos) { if (this.eof) return; this.readBlock(); @@ -365,7 +365,7 @@ var FlateStream = (function() { // build the table var size = 1 << maxLen; - var codes = new Uint32Array(size); + var codes = Uint32Array(size); for (var len = 1, code = 0, skip = 2; len <= maxLen; ++len, code <<= 1, skip <<= 1) { @@ -391,6 +391,12 @@ var FlateStream = (function() { 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 stream = this.stream; // read block header @@ -434,11 +440,6 @@ var FlateStream = (function() { litCodeTable = fixedLitCodeTab; distCodeTable = fixedDistCodeTab; } else if (hdr == 2) { // compressed block, dynamic codes - var repeat = function repeat(stream, array, len, offset, what) { - var repeat = stream.getBits(len) + offset; - while (repeat-- > 0) - array[i++] = what; - } var numLitCodes = this.getBits(5) + 257; var numDistCodes = this.getBits(5) + 1; var numCodeLenCodes = this.getBits(4) + 4; @@ -508,9 +509,97 @@ var FlateStream = (function() { return constructor; })(); +var PredictorStream = (function() { + function constructor(stream, params) { + this.stream = stream; + 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; @@ -727,7 +816,7 @@ var Lexer = (function() { var done = false; var str = ""; var stream = this.stream; - var ch = null; + var ch; do { switch (ch = stream.getChar()) { case undefined: @@ -1088,7 +1177,9 @@ var Parser = (function() { this.encAlgorithm, this.keyLength); } - return this.filter(stream, dict); + stream = this.filter(stream, dict); + stream.parameters = dict; + return stream; }, filter: function(stream, dict) { var filter = dict.get2("Filter", "F"); @@ -1113,8 +1204,9 @@ var Parser = (function() { }, 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 { error("filter '" + name + "' not supported yet"); @@ -1207,10 +1299,10 @@ var XRef = (function() { this.stream = stream; this.entries = []; this.xrefstms = {}; - this.readXRef(startXRef); + var trailerDict = this.readXRef(startXRef); // get the root dictionary (catalog) object - if (!IsRef(this.root = this.trailerDict.get("Root"))) + if (!IsRef(this.root = trailerDict.get("Root"))) error("Invalid root reference"); // prepare the XRef cache @@ -1265,18 +1357,18 @@ var XRef = (function() { error("Invalid XRef table"); // get the 'Prev' pointer - var more = false; + var prev; obj = dict.get("Prev"); if (IsInt(obj)) { - this.prev = obj; - more = true; + prev = obj; } 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.trailerDict = dict; // check for 'XRefStm' key if (IsInt(obj = dict.get("XRefStm"))) { @@ -1287,10 +1379,56 @@ var XRef = (function() { this.readXRef(pos); } - return more; + return dict; }, - readXRefStream: function(parser) { - error("Invalid XRef stream"); + 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; @@ -1330,11 +1468,12 @@ var XRef = (function() { e = this.getEntry(num); var gen = ref.gen; + var stream, parser; if (e.uncompressed) { if (e.gen != gen) throw("inconsistent generation in XRef"); - var stream = this.stream.makeSubStream(e.offset); - var parser = new Parser(new Lexer(stream), true, this); + stream = this.stream.makeSubStream(e.offset); + parser = new Parser(new Lexer(stream), true, this); var obj1 = parser.getObj(); var obj2 = parser.getObj(); var obj3 = parser.getObj(); @@ -1358,7 +1497,40 @@ var XRef = (function() { 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 +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); - this.code = gfx.compile(content, xref, resources, fonts); + 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] }); @@ -1574,7 +1766,7 @@ var PDFDoc = (function() { }, getPage: function(n) { var linearization = this.linearization; - assert(!linearization, "linearized page access not implemented"); + // assert(!linearization, "linearized page access not implemented"); return this.catalog.getPage(n); } }; @@ -1593,6 +1785,7 @@ var CanvasExtraState = (function() { this.fontSize = 0.0; this.textMatrix = IDENTITY_MATRIX; this.leading = 0.0; + this.colorSpace = null; // Current point (in user coordinates) this.x = 0.0; this.y = 0.0; @@ -1887,6 +2080,9 @@ var CanvasGraphics = (function() { const NORMAL_CLIP = {}; const EO_CLIP = {}; + // Used for tiling patterns + const PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; + constructor.prototype = { translateFont: function(fontDict, xref, resources) { var descriptor = xref.fetch(fontDict.get("FontDescriptor")); @@ -1922,7 +2118,8 @@ var CanvasGraphics = (function() { // Get the font charset if any var charset = descriptor.get("CharSet"); - assertWellFormed(IsString(charset), "invalid charset"); + if (charset) + assertWellFormed(IsString(charset), "invalid charset"); charset = charset.split("/"); } else if (IsName(encoding)) { @@ -1958,7 +2155,8 @@ var CanvasGraphics = (function() { 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) { @@ -2354,6 +2552,10 @@ var CanvasGraphics = (function() { }, setFillColorSpace: function(space) { // TODO real impl + if (space.name === "Pattern") + this.current.colorSpace = "Pattern"; + else + this.current.colorSpace = "DeviceRGB"; }, setStrokeColor: function(/*...*/) { // TODO real impl @@ -2377,7 +2579,125 @@ var CanvasGraphics = (function() { }, 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[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,18 +2785,24 @@ var CanvasGraphics = (function() { 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 + // The following bug should allow us to remove this. + // https://bugzilla.mozilla.org/show_bug.cgi?id=664884 this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); }, diff --git a/test.py b/test.py new file mode 100644 index 000000000..46d30fef5 --- /dev/null +++ b/test.py @@ -0,0 +1,175 @@ +import json, os, sys, subprocess +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +ANAL = True +VERBOSE = False + +MIMEs = { + '.css': 'text/css', + '.html': 'text/html', + '.js': 'application/json', + '.json': 'application/json', + '.pdf': 'application/pdf', + '.xhtml': 'application/xhtml+xml', +} + +class State: + browsers = [ ] + manifest = { } + taskResults = { } + remaining = 0 + results = { } + done = False + +class Result: + def __init__(self, snapshot, failure): + self.snapshot = snapshot + self.failure = failure + + +class PDFTestHandler(BaseHTTPRequestHandler): + # Disable annoying noise by default + def log_request(code=0, size=0): + if VERBOSE: + BaseHTTPRequestHandler.log_request(code, size) + + def do_GET(self): + cwd = os.getcwd() + path = os.path.abspath(os.path.realpath(cwd + os.sep + self.path)) + cwd = os.path.abspath(cwd) + prefix = os.path.commonprefix(( path, cwd )) + _, ext = os.path.splitext(path) + + if not (prefix == cwd + and os.path.isfile(path) + and ext in MIMEs): + self.send_error(404) + return + + if 'Range' in self.headers: + # TODO for fetch-as-you-go + self.send_error(501) + return + + self.send_response(200) + self.send_header("Content-Type", MIMEs[ext]) + self.end_headers() + + # Sigh, os.sendfile() plz + f = open(path) + self.wfile.write(f.read()) + f.close() + + + def do_POST(self): + numBytes = int(self.headers['Content-Length']) + + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + + result = json.loads(self.rfile.read(numBytes)) + browser = 'firefox4' + id, failure, round, page, snapshot = result['id'], result['failure'], result['round'], result['page'], result['snapshot'] + taskResults = State.taskResults[browser][id] + taskResults[round][page - 1] = Result(snapshot, failure) + + if result['taskDone']: + check(State.manifest[id], taskResults, browser) + State.remaining -= 1 + + State.done = (0 == State.remaining) + + +def set_up(): + # Only serve files from a pdf.js clone + assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git') + + testBrowsers = [ b for b in + ( 'firefox4', ) +#'chrome12', 'chrome13', 'firefox5', 'firefox6','opera11' ): + if os.access(b, os.R_OK | os.X_OK) ] + + mf = open('test_manifest.json') + manifestList = json.load(mf) + mf.close() + + for b in testBrowsers: + State.taskResults[b] = { } + for item in manifestList: + id, rounds = item['id'], int(item['rounds']) + State.manifest[id] = item + taskResults = [ ] + for r in xrange(rounds): + taskResults.append([ None ] * 100) + State.taskResults[b][id] = taskResults + + State.remaining = len(manifestList) + + for b in testBrowsers: + print 'Launching', b + subprocess.Popen(( os.path.abspath(os.path.realpath(b)), + 'http://localhost:8080/test_slave.html' )) + + +def check(task, results, browser): + failed = False + for r in xrange(len(results)): + pageResults = results[r] + for p in xrange(len(pageResults)): + pageResult = pageResults[p] + if pageResult is None: + continue + failure = pageResult.failure + if failure: + failed = True + print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure + + if failed: + return + + kind = task['type'] + if '==' == kind: + checkEq(task, results, browser) + elif 'fbf' == kind: + checkFBF(task, results, browser) + elif 'load' == kind: + checkLoad(task, results, browser) + else: + assert 0 and 'Unknown test type' + + +def checkEq(task, results, browser): + print ' !!! [TODO: == tests] !!!' + print 'TEST-PASS | == test', task['id'], '| in', browser + + +printed = [False] + +def checkFBF(task, results, browser): + round0, round1 = results[0], results[1] + assert len(round0) == len(round1) + + for page in xrange(len(round1)): + r0Page, r1Page = round0[page], round1[page] + if r0Page is None: + break + if r0Page.snapshot != r1Page.snapshot: + print 'TEST-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second' + print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser + + +def checkLoad(task, results, browser): + # Load just checks for absence of failure, so if we got here the + # test has passed + print 'TEST-PASS | load test', task['id'], '| in', browser + + +def main(): + set_up() + server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler) + while not State.done: + server.handle_request() + +if __name__ == '__main__': + main() diff --git a/test_manifest.json b/test_manifest.json new file mode 100644 index 000000000..2f45a026c --- /dev/null +++ b/test_manifest.json @@ -0,0 +1,17 @@ +[ + { "id": "tracemonkey-==", + "file": "tests/tracemonkey.pdf", + "rounds": 1, + "type": "==" + }, + { "id": "tracemonkey-fbf", + "file": "tests/tracemonkey.pdf", + "rounds": 2, + "type": "fbf" + }, + { "id": "html5-canvas-cheat-sheet-load", + "file": "tests/canvas.pdf", + "rounds": 1, + "type": "load" + } +] diff --git a/test_slave.html b/test_slave.html new file mode 100644 index 000000000..c560d90d0 --- /dev/null +++ b/test_slave.html @@ -0,0 +1,149 @@ + + + pdf.js test slave + + + + + + + +

+
+
+
diff --git a/tests/canvas.pdf b/tests/canvas.pdf
new file mode 100644
index 000000000..900d8af23
Binary files /dev/null and b/tests/canvas.pdf differ
diff --git a/tests/tracemonkey.pdf b/tests/tracemonkey.pdf
new file mode 100644
index 000000000..65570184a
Binary files /dev/null and b/tests/tracemonkey.pdf differ
diff --git a/utils/cffStandardStrings.js b/utils/cffStandardStrings.js
index 8977cd8f2..1b328a2da 100644
--- a/utils/cffStandardStrings.js
+++ b/utils/cffStandardStrings.js
@@ -1,5 +1,10 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+"use strict";
+
 var CFFStrings = [
-  ".notdef", 
+  ".notdef",
   "space",
   "exclam",
   "quotedbl",
@@ -490,7 +495,7 @@ var CFFDictDataMap = {
   },
   "10": {
     name: "StdHW"
-  }, 
+  },
   "11": {
     name: "StdVW"
   },
@@ -597,7 +602,7 @@ var CFFDictDataMap = {
   },
   "18": {
     name: "Private",
-    operand: "number number" 
+    operand: "number number"
   },
   "19": {
     name: "Subrs"
diff --git a/utils/fonts_utils.js b/utils/fonts_utils.js
index 45f92a28b..de4ceea72 100644
--- a/utils/fonts_utils.js
+++ b/utils/fonts_utils.js
@@ -1,3 +1,8 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+"use strict";
+
 /**
  * The Type2 reader code below is only used for debugging purpose since Type2
  * is only a CharString format and is never used directly as a Font file.