/* -*- 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 time to wait for a font to be loaded by @font-face */ var kMaxWaitForFontFace = 1000; /** * Useful for debugging when you want to certains operations depending on how * many fonts are loaded. */ var fontCount = 0; var fontName = ""; /** * If for some reason one want to debug without fonts activated, it just need * to turn this pref to true/false. */ var kDisableFonts = false; /** * Hold a map of decoded fonts and of the standard fourteen Type1 fonts and * their acronyms. * TODO Add the standard fourteen Type1 fonts list by default * http://cgit.freedesktop.org/poppler/poppler/tree/poppler/GfxFont.cc#n65 */ var kScalePrecision = 40; var Fonts = { _active: null, get active() { return this._active; }, setActive: function fonts_setActive(name, size) { this._active = this[name]; this.ctx.font = (size * kScalePrecision) + 'px "' + name + '"'; }, charsToUnicode: function fonts_chars2Unicode(chars) { var active = this._active; if (!active) return chars; // if we translated this string before, just grab it from the cache var str = active.cache[chars]; if (str) return str; // translate the string using the font's encoding var encoding = active.properties.encoding; if (!encoding) return chars; str = ""; for (var i = 0; i < chars.length; ++i) { var charcode = chars.charCodeAt(i); var unicode = encoding[charcode]; // Check if the glyph has already been converted if (!IsNum(unicode)) unicode = encoding[unicode] = GlyphsUnicode[unicode.name]; // Handle surrogate pairs if (unicode > 0xFFFF) { str += String.fromCharCode(unicode & 0xFFFF); unicode >>= 16; } str += String.fromCharCode(unicode); } // Enter the translated string into the cache return active.cache[chars] = str; }, get ctx() { var ctx = document.createElement("canvas").getContext("2d"); ctx.scale(1 / kScalePrecision, 1); return shadow(this, "ctx", ctx); }, measureText: function fonts_measureText(text) { return this.ctx.measureText(text).width / kScalePrecision; } }; var FontLoader = { bind: function(fonts, callback) { var worker = (typeof window == "undefined"); function checkFontsLoaded() { for (var i = 0; i < fonts.length; i++) { var font = fonts[i]; if (Fonts[font.name].loading) { return false; } } document.documentElement.removeEventListener( "pdfjsFontLoad", checkFontsLoaded, false); callback(); return true; } for (var i = 0; i < fonts.length; i++) { var font = fonts[i]; if (!Fonts[font.name]) { var obj = new Font(font.name, font.file, font.properties); var str = ""; var data = Fonts[font.name].data; var length = data.length; for (var j = 0; j < length; j++) str += String.fromCharCode(data[j]); worker ? obj.bindWorker(str) : obj.bindDOM(str); } } if (!checkFontsLoaded()) { document.documentElement.addEventListener( "pdfjsFontLoad", checkFontsLoaded, false); } return; } }; /** * 'Font' is the class the outside world should use, it encapsulate all the font * decoding logics whatever type it is (assuming the font type is supported). * * For example to read a Type1 font and to attach it to the document: * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); * type1Font.bind(); */ var Font = (function () { var constructor = function font_constructor(name, file, properties) { this.name = name; this.encoding = properties.encoding; // If the font has already been decoded simply return it if (Fonts[name]) { this.font = Fonts[name].data; return; } fontCount++; fontName = name; // If the font is to be ignored, register it like an already loaded font // to avoid the cost of waiting for it be be loaded by the platform. if (properties.ignore || kDisableFonts) { Fonts[name] = { data: file, loading: false, properties: {}, cache: Object.create(null) } return; } var data; switch (properties.type) { case "Type1": var cff = new CFF(name, file, properties); this.mimetype = "font/opentype"; // Wrap the CFF data inside an OTF font file data = this.convert(name, cff, properties); break; case "TrueType": this.mimetype = "font/opentype"; // Repair the TrueType file if it is can be damaged in the point of // view of the sanitizer data = this.checkAndRepair(name, file, properties); break; default: warn("Font " + properties.type + " is not supported"); break; } this.data = data; Fonts[name] = { data: data, properties: properties, loading: true, cache: Object.create(null) }; }; function stringToArray(str) { var array = []; for (var i = 0; i < str.length; ++i) array[i] = str.charCodeAt(i); return array; }; function string16(value) { return String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); }; function string32(value) { return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) + String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff); }; function createOpenTypeHeader(sfnt, file, offsets, numTables) { // sfnt version (4 bytes) var header = sfnt; // numTables (2 bytes) header += string16(numTables); // searchRange (2 bytes) var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables); var searchRange = tablesMaxPower2 * 16; header += string16(searchRange); // entrySelector (2 bytes) header += string16(Math.log(tablesMaxPower2) / Math.log(2)); // rangeShift (2 bytes) header += string16(numTables * 16 - searchRange); file.set(stringToArray(header), offsets.currentOffset); offsets.currentOffset += header.length; offsets.virtualOffset += header.length; }; function createTableEntry(file, offsets, tag, data) { // offset var offset = offsets.virtualOffset; // Per spec tables must be 4-bytes align so add padding as needed while (data.length & 3) data.push(0x00); while (offsets.virtualOffset & 3) offsets.virtualOffset++; // length var length = data.length; // checksum var checksum = tag.charCodeAt(0) + tag.charCodeAt(1) + tag.charCodeAt(2) + tag.charCodeAt(3) + offset + length; var tableEntry = tag + string32(checksum) + string32(offset) + string32(length); tableEntry = stringToArray(tableEntry); file.set(tableEntry, offsets.currentOffset); offsets.currentOffset += tableEntry.length; offsets.virtualOffset += data.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(glyphs) { var ranges = getRanges(glyphs); 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 string16(headerSize) + // length "\x00\x00" + // languages string16(segCount2) + string16(searchRange) + string16(searchEntry) + string16(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 += string16(start); endCount += string16(end); idDeltas += string16(delta); idRangeOffsets += string16(0); for (var j = 0; j < range.length; j++) glyphsIds += String.fromCharCode(range[j]); } startCount += "\xFF\xFF"; endCount += "\xFF\xFF"; idDeltas += "\x00\x01"; idRangeOffsets += "\x00\x00"; return stringToArray(cmap + endCount + "\x00\x00" + startCount + idDeltas + idRangeOffsets + glyphsIds); }; function createOS2Table(properties) { return "\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 string16(properties.ascent) + // usWinAscent string16(properties.descent) + // usWinDescent "\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31) "\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63) string16(properties.xHeight) + // sxHeight string16(properties.capHeight) + // sCapHeight "\x00\x01" + // usDefaultChar "\x00\xCD" + // usBreakChar "\x00\x02"; // usMaxContext }; function createPostTable(properties) { TODO("Fill with real values from the font dict"); return "\x00\x03\x00\x00" + // Version number string32(properties.italicAngle) + // 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 }; constructor.prototype = { name: null, font: null, mimetype: null, encoding: null, checkAndRepair: function font_checkAndRepair(name, font, properties) { function readTableEntry(file) { // tag var tag = file.getBytes(4); tag = String.fromCharCode(tag[0]) + String.fromCharCode(tag[1]) + String.fromCharCode(tag[2]) + String.fromCharCode(tag[3]); var checksum = FontsUtils.bytesToInteger(file.getBytes(4)); var offset = FontsUtils.bytesToInteger(file.getBytes(4)); var length = FontsUtils.bytesToInteger(file.getBytes(4)); // Read the table associated data var previousPosition = file.pos; file.pos = file.start ? file.start : 0; file.skip(offset); var data = file.getBytes(length); file.pos = previousPosition; return { tag: tag, checksum: checksum, length: offset, offset: length, data: data } }; function readOpenTypeHeader(ttf) { return { version: ttf.getBytes(4), numTables: FontsUtils.bytesToInteger(ttf.getBytes(2)), searchRange: FontsUtils.bytesToInteger(ttf.getBytes(2)), entrySelector: FontsUtils.bytesToInteger(ttf.getBytes(2)), rangeShift: FontsUtils.bytesToInteger(ttf.getBytes(2)) } }; function replaceCMapTable(cmap, font, properties) { var version = FontsUtils.bytesToInteger(font.getBytes(2)); var numTables = FontsUtils.bytesToInteger(font.getBytes(2)); for (var i = 0; i < numTables; i++) { var platformID = FontsUtils.bytesToInteger(font.getBytes(2)); var encodingID = FontsUtils.bytesToInteger(font.getBytes(2)); var offset = FontsUtils.bytesToInteger(font.getBytes(4)); var format = FontsUtils.bytesToInteger(font.getBytes(2)); var length = FontsUtils.bytesToInteger(font.getBytes(2)); var language = FontsUtils.bytesToInteger(font.getBytes(2)); if ((format == 0 && numTables == 1) || (format == 6 && numTables == 1 && !properties.encoding.empty)) { // Format 0 alone is not allowed by the sanitizer so let's rewrite // that to a 3-1-4 Unicode BMP table TODO("Use an other source of informations than charset here, it is not reliable"); var charset = properties.charset; var glyphs = []; for (var j = 0; j < charset.length; j++) { glyphs.push({ unicode: GlyphsUnicode[charset[j]] || 0 }); } cmap.data = createCMapTable(glyphs); } else if (format == 6 && numTables == 1) { // Format 6 is a 2-bytes dense mapping, which means the font data // lives glue together even if they are pretty far in the unicode // table. (This looks weird, so I can have missed something), this // works on Linux but seems to fails on Mac so let's rewrite the // cmap table to a 3-1-4 style var firstCode = FontsUtils.bytesToInteger(font.getBytes(2)); var entryCount = FontsUtils.bytesToInteger(font.getBytes(2)); var glyphs = []; var min = 0xffff, max = 0; for (var j = 0; j < entryCount; j++) { var charcode = FontsUtils.bytesToInteger(font.getBytes(2)); glyphs.push(charcode); if (charcode < min) min = charcode; if (charcode > max) max = charcode; } // Since Format 6 is a dense array, check for gaps for (var j = min; j < max; j++) { if (glyphs.indexOf(j) == -1) glyphs.push(j); } for (var j = 0; j < glyphs.length; j++) glyphs[j] = { unicode: glyphs[j] + firstCode }; var ranges= getRanges(glyphs); assert(ranges.length == 1, "Got " + ranges.length + " ranges in a dense array"); var encoding = properties.encoding; var denseRange = ranges[0]; var start = denseRange[0]; var end = denseRange[1]; var index = firstCode; for (var j = start; j <= end; j++) encoding[index++] = glyphs[j - firstCode - 1].unicode; cmap.data = createCMapTable(glyphs); } } }; // Check that required tables are present var requiredTables = [ "OS/2", "cmap", "head", "hhea", "hmtx", "maxp", "name", "post" ]; var header = readOpenTypeHeader(font); var numTables = header.numTables; // This keep a reference to the CMap and the post tables since they can // be rewritted var cmap, post; var tables = []; for (var i = 0; i < numTables; i++) { var table = readTableEntry(font); var index = requiredTables.indexOf(table.tag); if (index != -1) { if (table.tag == "cmap") cmap = table; else if (table.tag == "post") post = table; requiredTables.splice(index, 1); } tables.push(table); } // If any tables are still in the array this means some required tables are // missing, which means that we need to rebuild the font in order to pass // the sanitizer. if (requiredTables.length && requiredTables[0] == "OS/2") { // Create a new file to hold the new version of our truetype with a new // header and new offsets var ttf = new Uint8Array(kMaxFontFileSize); // 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 put the actual data of a particular // table var numTables = header.numTables + requiredTables.length; var offsets = { currentOffset: 0, virtualOffset: numTables * (4 * 4) }; // The new numbers of tables will be the last one plus the num of missing // tables createOpenTypeHeader("\x00\x01\x00\x00", ttf, offsets, numTables); // Insert the missing table tables.push({ tag: "OS/2", data: stringToArray(createOS2Table(properties)) }); // Replace the old CMAP table with a shiny new one replaceCMapTable(cmap, font, properties); // Rewrite the 'post' table if needed if (!post) { tables.push({ tag: "post", data: stringToArray(createPostTable(properties)) }); } // Tables needs to be written by ascendant alphabetic order tables.sort(function tables_sort(a, b) { return a.tag > b.tag; }); // rewrite the tables but tweak offsets for (var i = 0; i < tables.length; i++) { var table = tables[i]; var data = []; var tableData = table.data; for (var j = 0; j < tableData.length; j++) data.push(tableData[j]); createTableEntry(ttf, offsets, table.tag, data); } // Add the table datas for (var i = 0; i < tables.length; i++) { var table = tables[i]; var tableData = table.data; ttf.set(tableData, offsets.currentOffset); offsets.currentOffset += tableData.length; // 4-byte aligned data while (offsets.currentOffset & 3) offsets.currentOffset++; } var fontData = []; for (var i = 0; i < offsets.currentOffset; i++) fontData.push(ttf[i]); return fontData; } else if (requiredTables.length) { error("Table " + requiredTables[0] + " is missing from the TrueType font"); } return font.getBytes(); }, convert: function font_convert(fontName, font, properties) { var otf = new Uint8Array(kMaxFontFileSize); function createNameTable(name) { var names = [ "See original licence", // Copyright fontName, // Font family "undefined", // Font subfamily (font weight) "uniqueID", // Unique ID fontName, // Full font name "0.1", // Version "undefined", // Postscript name "undefined", // Trademark "undefined", // Manufacturer "undefined" // Designer ]; var nameTable = "\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 string16(str.length) + string16(strOffset); nameTable += nameRecord; strOffset += str.length; } nameTable += names.join(""); return nameTable; } // Required Tables var CFF = font.data, // PostScript Font Program OS2, // OS/2 and Windows Specific metrics cmap, // Character to glyphs mapping head, // Font header 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) }; // It there is only one font, offset table is the first bytes of the file createOpenTypeHeader("\x4F\x54\x54\x4F", otf, offsets, tables.length); /** CFF */ createTableEntry(otf, offsets, "CFF ", CFF); /** OS/2 */ OS2 = stringToArray(createOS2Table(properties)); createTableEntry(otf, offsets, "OS/2", OS2); /** CMAP */ var charstrings = font.charstrings; cmap = createCMapTable(charstrings); createTableEntry(otf, offsets, "cmap", cmap); /** HEAD */ head = stringToArray( "\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 = stringToArray( "\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 string16(charstrings.length + 1) // Number of HMetrics ); createTableEntry(otf, offsets, "hhea", hhea); /** HMTX */ /* For some reasons, probably related to how the backend handle fonts, * Linux seems to ignore this file and prefer the data from the CFF itself * while Windows use this data. So be careful if you hack on Linux and * have to touch the 'hmtx' table */ hmtx = "\x00\x00\x00\x00"; // Fake .notdef var width = 0, lsb = 0; for (var i = 0; i < charstrings.length; i++) { var charstring = charstrings[i]; hmtx += string16(charstring.width) + string16(0); } hmtx = stringToArray(hmtx); createTableEntry(otf, offsets, "hmtx", hmtx); /** MAXP */ maxp = "\x00\x00\x50\x00" + // Version number string16(charstrings.length + 1); // Num of glyphs maxp = stringToArray(maxp); createTableEntry(otf, offsets, "maxp", maxp); /** NAME */ name = stringToArray(createNameTable(name)); createTableEntry(otf, offsets, "name", name); /** POST */ post = stringToArray(createPostTable(properties)); 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; }, bindWorker: function font_bindWorker(data) { postMessage({ action: "font", data: { raw: data, fontName: this.name, mimetype: this.mimetype } }); }, bindDOM: function font_bindDom(data) { var fontName = this.name; // Add the @font-face rule to the document var url = "url(data:" + this.mimetype + ";base64," + window.btoa(data) + ");"; var rule = "@font-face { font-family:'" + fontName + "';src:" + url + "}"; var styleSheet = document.styleSheets[0]; styleSheet.insertRule(rule, styleSheet.length); /** Hack begin */ // There's no event when a font has finished downloading so the // following code is a dirty hack to 'guess' when a font is // ready. This code will be obsoleted by Mozilla bug 471915. // // The only reliable way to know if a font is loaded in Gecko // (at the moment) is document.onload in a document with // a @font-face rule defined in a "static" stylesheet. We use a // subdocument in an