diff --git a/LICENSE b/LICENSE index 6b2bc4d61..81658476c 100644 --- a/LICENSE +++ b/LICENSE @@ -4,6 +4,9 @@ Contributors: Andreas Gal 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"), diff --git a/fonts.js b/fonts.js index be4007d24..b8a490369 100644 --- a/fonts.js +++ b/fonts.js @@ -1,16 +1,13 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +"use strict"; + /** * Maximum file size of the font. */ var kMaxFontFileSize = 40000; -/** - * Maximum number of glyphs per font. -*/ -var kMaxGlyphsCount = 65526; - /** * Maximum time to wait for a font to be loaded by @font-face */ @@ -30,6 +27,7 @@ var fontCount = 0; */ var Fonts = { _active: null, + get active() { return this._active; }, @@ -38,12 +36,38 @@ var Fonts = { this._active = this[aName]; }, - unicodeFromCode: function fonts_unicodeFromCode(aCode) { + chars2Unicode: function(chars) { var active = this._active; - if (!active || !active.properties.encoding) - return aCode; + if (!active) + return chars; - return GlyphsUnicode[active.properties.encoding[aCode]]; + // if we translated this string before, just grab it from the cache + var ret = active.cache[chars]; + if (ret) + return ret; + + // translate the string using the font's encoding + var encoding = active.properties.encoding; + if (!encoding) + return chars; + + var ret = ""; + for (var i = 0; i < chars.length; ++i) { + var ch = chars.charCodeAt(i); + 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); + } + + // enter the translated string into the cache + active.cache[chars] = ret; + + return ret; } }; @@ -55,23 +79,24 @@ var Fonts = { * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); * type1Font.bind(); */ -var Font = function(aName, aFile, aProperties) { - this.name = aName; +var Font = (function () { + var constructor = function(aName, aFile, aProperties) { + this.name = aName; - // 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++; - switch (aProperties.type) { + 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.cover(cff, aProperties); + this.font = this.convert(cff, aProperties); break; case "TrueType": @@ -83,7 +108,8 @@ var Font = function(aName, aFile, aProperties) { encoding: {}, charset: null }, - loading: false + loading: false, + cache: Object.create(null) }; this.mimetype = "font/ttf"; @@ -94,448 +120,448 @@ var Font = function(aName, aFile, aProperties) { default: warn("Font " + aProperties.type + " is not supported"); break; - } - - Fonts[aName] = { - data: this.font, - properties: aProperties, - loading: true - } - - // Attach the font to the document - this.bind(); -}; - - -/** - * 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, - - 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); + Fonts[aName] = { + data: this.font, + properties: aProperties, + loading: true, + cache: Object.create(null) + } - // Retrieve font charset - var charset = Fonts[fontName].charset || []; - // if the charset is too small make it repeat a few times - var count = 30; - while (count-- && charset.length <= 30) - charset = charset.concat(charset.slice()); + // Attach the font to the document + this.bind(); + }; - // Get the font size canvas think it will be for 'spaces' - var ctx = canvas.getContext("2d"); - var testString = " "; + /** + * 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, - // 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"); - testString += String.fromCharCode(unicode); + 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); } - } - ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - var textWidth = ctx.mozMeasureText(testString); - 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 start = Date.now(); - var interval = window.setInterval(function canvasInterval(self) { + // Retrieve font charset + var charset = Fonts[fontName].charset || []; + // if the charset is too small make it repeat a few times + var count = 30; + while (count-- && charset.length <= 30) + charset = charset.concat(charset.slice()); + + // Get the font size canvas think it will be for 'spaces' + var ctx = canvas.getContext("2d"); + var testString = " "; + + // When debugging use the characters provided by the charsets to visually + // see what's happening + if (debug) { + for (var i = 0; i < charset.length; i++) { + var unicode = GlyphsUnicode[charset[i]]; + if (!unicode) + error("Unicode for " + charset[i] + " is has not been found in the glyphs list"); + testString += String.fromCharCode(unicode); + } + } ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - - // For some reasons the font has not loaded, so mark it loaded for the - // page to proceed but cry - if ((Date.now() - start) >= kMaxWaitForFontFace) { - window.clearInterval(interval); - Fonts[fontName].loading = false; - warn("Is " + fontName + " for charset: " + charset + " loaded?"); - } else if (textWidth != ctx.mozMeasureText(testString)) { - window.clearInterval(interval); - Fonts[fontName].loading = false; - } + var textWidth = ctx.measureText(testString).width; if (debug) - ctx.fillText(testString, 20, 50); - }, 50, this); + ctx.fillText(testString, 20, 20); - /** Hack end */ + var start = Date.now(); + var interval = window.setInterval(function canvasInterval(self) { + 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() - 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; + } - _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); + convert: function font_convert(aFont, aProperties) { + var otf = new 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; } + + function s16(value) { + return String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); + } + + function s32(value) { + return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) + + String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); + } + + function createOpenTypeHeader(aFile, aOffsets, numTables) { + var header = ""; + + // 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 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 + 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 + 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); + 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; } + }; - // The size in bytes of the header is equal to the size of the - // different fields * length of a short + (size of the 4 parallels arrays - // describing segments * length of a short). - var headerSize = (12 * 2 + (ranges.length * 4 * 2)); - - var segCount = ranges.length + 1; - var segCount2 = segCount * 2; - var searchRange = FontsUtils.getMaxPower2(segCount) * 2; - var searchEntry = Math.log(segCount) / Math.log(2); - var rangeShift = 2 * segCount - searchRange; - var cmap = [].concat( - [ - 0x00, 0x00, // version - 0x00, 0x01, // numTables - 0x00, 0x03, // platformID - 0x00, 0x01, // encodingID - 0x00, 0x00, 0x00, 0x0C, // start of the table record - 0x00, 0x04 // format - ], - FontsUtils.integerToBytes(headerSize, 2), // length - [0x00, 0x00], // language - FontsUtils.integerToBytes(segCount2, 2), - FontsUtils.integerToBytes(searchRange, 2), - FontsUtils.integerToBytes(searchEntry, 2), - FontsUtils.integerToBytes(rangeShift, 2) - ); - - // Fill up the 4 parallel arrays describing the segments. - var startCount = []; - var endCount = []; - var idDeltas = []; - var idRangeOffsets = []; - var glyphsIdsArray = []; - var bias = 0; - for (var i = 0; i < segCount - 1; i++) { - var range = ranges[i]; - var start = FontsUtils.integerToBytes(range[0], 2); - var end = FontsUtils.integerToBytes(range[range.length - 1], 2); - - var delta = FontsUtils.integerToBytes(((range[0] - 1) - bias) % 65536, 2); - bias += range.length; - - // deltas are signed shorts - delta[0] ^= 0xFF; - delta[1] ^= 0xFF; - delta[1] += 1; - - startCount.push(start[0], start[1]); - endCount.push(end[0], end[1]); - idDeltas.push(delta[0], delta[1]); - idRangeOffsets.push(0x00, 0x00); - - for (var j = 0; j < range.length; j++) - glyphsIdsArray.push(range[j]); - } - startCount.push(0xFF, 0xFF); - endCount.push(0xFF, 0xFF); - idDeltas.push(0x00, 0x01); - idRangeOffsets.push(0x00, 0x00); - - return cmap.concat(endCount, [0x00, 0x00], startCount, - idDeltas, idRangeOffsets, glyphsIdsArray); - }, - - cover: function font_cover(aFont, aProperties) { - var otf = new Uint8Array(kMaxFontFileSize); - - // Required Tables - var CFF = aFont.data, // PostScript Font Program - OS2 = [], // OS/2 and Windows Specific metrics - cmap = [], // Character to glyphs mapping - head = [], // Font eader - hhea = [], // Horizontal header - hmtx = [], // Horizontal metrics - maxp = [], // Maximum profile - name = [], // Naming tables - post = []; // PostScript informations - var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post]; - - // The offsets object holds at the same time a representation of where - // to write the table entry information about a table and another offset - // representing the offset where to draw the actual data of a particular - // table - var offsets = { - currentOffset: 0, - virtualOffset: tables.length * (4 * 4) - }; - - // For files with only one font the offset table is the first thing of the - // file - this._createOpenTypeHeader(otf, offsets, tables.length); - - // XXX It is probable that in a future we want to get rid of this glue - // between the CFF and the OTF format in order to be able to embed TrueType - // data. - this._createTableEntry(otf, offsets, "CFF ", CFF); - - /** OS/2 */ - OS2 = [ - 0x00, 0x03, // version - 0x02, 0x24, // xAvgCharWidth - 0x01, 0xF4, // usWeightClass - 0x00, 0x05, // usWidthClass - 0x00, 0x00, // fstype - 0x02, 0x8A, // ySubscriptXSize - 0x02, 0xBB, // ySubscriptYSize - 0x00, 0x00, // ySubscriptXOffset - 0x00, 0x8C, // ySubscriptYOffset - 0x02, 0x8A, // ySuperScriptXSize - 0x02, 0xBB, // ySuperScriptYSize - 0x00, 0x00, // ySuperScriptXOffset - 0x01, 0xDF, // ySuperScriptYOffset - 0x00, 0x31, // yStrikeOutSize - 0x01, 0x02, // yStrikeOutPosition - 0x00, 0x00, // sFamilyClass - 0x02, 0x00, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Panose - 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 0-31) - 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 32-63) - 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 64-95) - 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 96-127) - 0x2A, 0x32, 0x31, 0x2A, // achVendID - 0x00, 0x20, // fsSelection - 0x00, 0x2D, // usFirstCharIndex - 0x00, 0x7A, // usLastCharIndex - 0x00, 0x03, // sTypoAscender - 0x00, 0x20, // sTypeDescender - 0x00, 0x38, // sTypoLineGap - 0x00, 0x5A, // usWinAscent - 0x02, 0xB4, // usWinDescent - 0x00, 0xCE, 0x00, 0x00, // ulCodePageRange1 (Bits 0-31) - 0x00, 0x01, 0x00, 0x00, // ulCodePageRange2 (Bits 32-63) - 0x00, 0x00, // sxHeight - 0x00, 0x00, // sCapHeight - 0x00, 0x01, // usDefaultChar - 0x00, 0xCD, // usBreakChar - 0x00, 0x02 // usMaxContext - ]; - this._createTableEntry(otf, offsets, "OS/2", OS2); - - //XXX Getting charstrings here seems wrong since this is another CFF glue - var charstrings = aFont.getOrderedCharStrings(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 */ - name = [ - 0x00, 0x00, // format - 0x00, 0x00, // Number of names Record - 0x00, 0x00 // Storage - ]; - 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 @@ -721,7 +747,7 @@ var TrueType = function(aFile) { } else if (requiredTables.lenght) { error("Table " + requiredTables[0] + " is missing from the TruType font"); } else { - this.data = aFile; + this.data = aFile.getBytes(); } }; @@ -1017,7 +1043,8 @@ var Type1Parser = function() { this.extractFontProgram = function t1_extractFontProgram(aStream) { var eexecString = decrypt(aStream, kEexecEncryptionKey, 4); var subrs = [], glyphs = []; - var inSubrs = inGlyphs = false; + var inGlyphs = false; + var inSubrs = false; var glyph = ""; var token = ""; diff --git a/glyphlist.js b/glyphlist.js index 1a0190133..85ab876f9 100644 --- a/glyphlist.js +++ b/glyphlist.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"; + var GlyphsUnicode = { A: 0x0041, AE: 0x00C6, @@ -1500,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, @@ -1837,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, @@ -2029,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, @@ -2468,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, @@ -2779,7 +2784,7 @@ var GlyphsUnicode = { noonfinalarabic: 0xFEE6, noonghunnaarabic: 0x06BA, noonghunnafinalarabic: 0xFB9F, - noonhehinitialarabic: "FEE7 FEEC", + noonhehinitialarabic: 0xFEE7FEEC, nooninitialarabic: 0xFEE7, noonjeeminitialarabic: 0xFCD2, noonjeemisolatedarabic: 0xFC4B, @@ -3151,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, @@ -3250,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, @@ -3469,7 +3474,7 @@ var GlyphsUnicode = { shaddadammaarabic: 0xFC61, shaddadammatanarabic: 0xFC5E, shaddafathaarabic: 0xFC60, - shaddafathatanarabic: "0651 064B", + shaddafathatanarabic: 0x0651064B, shaddakasraarabic: 0xFC62, shaddakasratanarabic: 0xFC5F, shade: 0x2592, @@ -3666,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 6a1589dde..8133fa172 100644 --- a/pdf.js +++ b/pdf.js @@ -1,11 +1,12 @@ /* -*- 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 ERRORS = 0, WARNINGS = 1, TODOS = 5; var verbosity = WARNINGS; function log(msg) { - msg = msg.toString ? msg.toString() : msg; if (console && console.log) console.log(msg); else if (print) @@ -50,7 +51,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 +116,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 +128,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 +140,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 +148,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 +225,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 +301,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 +309,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 +334,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 +366,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 +392,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 @@ -451,11 +458,6 @@ var FlateStream = (function() { var codes = numLitCodes + numDistCodes; var codeLengths = new Array(codes); while (i < codes) { - function repeat(stream, array, len, offset, what) { - var repeat = stream.getBits(len) + offset; - while (repeat-- > 0) - array[i++] = what; - } var code = this.getCode(codeLenCodeTab); if (code == 16) { repeat(this, codeLengths, 2, 3, len); @@ -670,13 +672,99 @@ var FilterPredictor = (function() { return true; } }; + 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; @@ -733,12 +821,6 @@ var Dict = (function() { forEach: function(aCallback) { for (var key in this.map) aCallback(key, this.map[key]); - }, - toString: function() { - var keys = []; - for (var key in this.map) - keys.push(key); - return "Dict with " + keys.length + " keys: " + keys; } }; @@ -905,6 +987,7 @@ var Lexer = (function() { var done = false; var str = ""; var stream = this.stream; + var ch; do { switch (ch = stream.getChar()) { case undefined: @@ -1265,7 +1348,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"); @@ -1310,6 +1395,12 @@ var Parser = (function() { } } return flateStr; + /* + if (params) { + return new PredictorStream(new FlateStream(stream), params); + } + return new FlateStream(stream); + */ } else { error("filter '" + name + "' not supported yet"); } @@ -1401,10 +1492,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 @@ -1459,18 +1550,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"))) { @@ -1480,11 +1571,56 @@ var XRef = (function() { this.xrefstms[pos] = 1; // avoid infinite recursion 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; @@ -1524,11 +1660,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(); @@ -1552,7 +1689,39 @@ 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); @@ -1583,20 +1752,39 @@ 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] }); @@ -1626,7 +1814,7 @@ var Catalog = (function() { return shadow(this, "toplevelPagesDict", obj); }, get numPages() { - obj = this.toplevelPagesDict.get("Count"); + var obj = this.toplevelPagesDict.get("Count"); assertWellFormed(IsInt(obj), "page count in top level pages object is not an integer"); // shadow the prototype getter @@ -1768,7 +1956,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); } }; @@ -2109,9 +2297,11 @@ 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("/"); + charset = charset.split("/"); + } } else if (IsName(encoding)) { var encoding = Encodings[encoding.name]; if (!encoding) @@ -2429,13 +2619,7 @@ var CanvasGraphics = (function() { this.ctx.translate(0, 2 * this.current.y); this.ctx.scale(1, -1); this.ctx.transform.apply(this.ctx, this.current.textMatrix); - - // Replace characters code by glyphs code - var glyphs = []; - for (var i = 0; i < text.length; i++) - glyphs[i] = String.fromCharCode(Fonts.unicodeFromCode(text[i].charCodeAt(0))); - - this.ctx.fillText(glyphs.join(""), this.current.x, this.current.y); + this.ctx.fillText(Fonts.chars2Unicode(text), this.current.x, this.current.y); this.current.x += this.ctx.measureText(text).width; this.ctx.restore(); @@ -2584,7 +2768,7 @@ var CanvasGraphics = (function() { error("No support for array of functions"); else if (!IsPDFFunction(fnObj)) error("Invalid function"); - fn = new PDFFunction(this.xref, fnObj); + var fn = new PDFFunction(this.xref, fnObj); var gradient = this.ctx.createLinearGradient(x0, y0, x1, y1); 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 086648fe2..79ecf257f 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.
diff --git a/viewer.js b/viewer.js
index 59d8167a2..675f2fb87 100644
--- a/viewer.js
+++ b/viewer.js
@@ -1,12 +1,14 @@
 /* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
 /* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
 
-var pdfDocument, canvas, pageDisplay, pageNum, pageInterval;
+"use strict";
+
+var pdfDocument, canvas, numPages, pageDisplay, pageNum, pageInterval;
 function load(userInput) {
     canvas = document.getElementById("canvas");
     canvas.mozOpaque = true;
     pageNum = parseInt(queryParams().page) || 1;
-    fileName = userInput;
+    var fileName = userInput;
     if (!userInput) {
       fileName = queryParams().file || "compressed.tracemonkey-pldi-09.pdf";
     }
@@ -26,7 +28,7 @@ function queryParams() {
 
 function open(url) {
     document.title = url;
-    req = new XMLHttpRequest();
+    var req = new XMLHttpRequest();
     req.open("GET", url);
     req.mozResponseType = req.responseType = "arraybuffer";
     req.expected = (document.URL.indexOf("file:") == 0) ? 0 : 200;