* var type1Font = new Font("MyFontName", binaryFile, propertiesObject);
* type1Font.bind();
*/
- var Font = function(aName, aFile, aProperties) {
- this.name = aName;
- this.encoding = aProperties.encoding;
-
- // If the font has already been decoded simply return
- if (Fonts[aName]) {
- this.font = Fonts[aName].data;
- return;
- }
- fontCount++;
-
- if (aProperties.ignore || kDisableFonts) {
- Fonts[aName] = {
- data: aFile,
- loading: false,
- properties: {},
- cache: Object.create(null)
+ var Font = (function () {
+ var constructor = function(aName, aFile, aProperties) {
+ this.name = aName;
++ this.encoding = aProperties.encoding;
+
+ // If the font has already been decoded simply return it
+ if (Fonts[aName]) {
+ this.font = Fonts[aName].data;
+ return;
}
- return;
- }
-
- switch (aProperties.type) {
- case "Type1":
- var cff = new CFF(aName, aFile, aProperties);
- this.mimetype = "font/opentype";
-
- // Wrap the CFF data inside an OTF font file
- this.font = this.cover(aName, cff, aProperties);
- break;
-
- case "TrueType":
- this.mimetype = "font/opentype";
- var ttf = new TrueType(aName, aFile, aProperties);
- this.font = ttf.data;
- break;
+ fontCount++;
++ fontName = aName;
- default:
- warn("Font " + aProperties.type + " is not supported");
- switch (aProperties.type) {
- case "Type1":
- var cff = new CFF(aName, aFile, aProperties);
- this.mimetype = "font/otf";
-
- // Wrap the CFF data inside an OTF font file
- this.font = this.convert(cff, aProperties);
-- break;
- }
-
- case "TrueType":
- // TrueType is disabled for the moment since the sanitizer prevent it
- // from loading because of an overdated cmap table
- return Fonts[aName] = {
- data: null,
- properties: {
- encoding: {},
- charset: null
- },
++ if (aProperties.ignore || kDisableFonts) {
++ Fonts[aName] = {
++ data: aFile,
+ loading: false,
++ properties: {},
+ cache: Object.create(null)
- };
-
- this.mimetype = "font/ttf";
- var ttf = new TrueType(aFile);
- this.font = ttf.data;
- break;
++ }
++ return;
++ }
- Fonts[aName] = {
- data: this.font,
- properties: aProperties,
- loading: true,
- cache: Object.create(null)
- }
- default:
- warn("Font " + aProperties.type + " is not supported");
- break;
++ switch (aProperties.type) {
++ case "Type1":
++ var cff = new CFF(aName, aFile, aProperties);
++ this.mimetype = "font/opentype";
+
- // Attach the font to the document
- this.bind();
- };
++ // Wrap the CFF data inside an OTF font file
++ this.font = this.cover(aName, cff, aProperties);
++ break;
+
++ case "TrueType":
++ // TrueType is disabled for the moment since the sanitizer prevent it
++ // from loading due to missing tables
++ return Fonts[aName] = {
++ data: null,
++ properties: {
++ encoding: {},
++ charset: null
++ },
++ loading: false,
++ cache: Object.create(null)
++ };
++
++ this.mimetype = "font/opentype";
++ var ttf = new TrueType(aName, aFile, aProperties);
++ this.font = ttf.data;
++ break;
+
- /**
- * A bunch of the OpenType code is duplicate between this class and the
- * TrueType code, this is intentional and will merge in a future version
- * where all the code relative to OpenType will probably have its own
- * class and will take decision without the Fonts consent.
- * But at the moment it allows to develop around the TrueType rewriting
- * on the fly without messing up with the 'regular' Type1 to OTF conversion.
- */
- Font.prototype = {
- name: null,
- font: null,
- mimetype: null,
- encoding: null,
-
- bind: function font_bind() {
- var data = this.font;
-
- // Compute the binary data to base 64
- var str = [];
- var count = data.length;
- for (var i = 0; i < count; i++)
- str.push(data.getChar ? data.getChar()
- : String.fromCharCode(data[i]));
-
- var dataBase64 = window.btoa(str.join(""));
- var fontName = this.name;
-
- /** Hack begin */
-
- // Actually there is not event when a font has finished downloading so
- // the following tons of code are a dirty hack to 'guess' when a font is
- // ready
- var debug = false;
-
- if (debug) {
- var name = document.createElement("font");
- name.setAttribute("style", "position: absolute; left: 20px; top: " +
- (100 * fontCount + 60) + "px");
- name.innerHTML = fontName;
- document.body.appendChild(name);
++ default:
++ warn("Font " + aProperties.type + " is not supported");
++ break;
}
- var canvas = document.createElement("canvas");
- var style = "border: 1px solid black; position:absolute; top: " +
- (debug ? (100 * fontCount) : "-200") + "px; left: 2px; width: 340px; height: 100px";
- canvas.setAttribute("style", style);
- canvas.setAttribute("width", 340);
- canvas.setAttribute("heigth", 100);
- document.body.appendChild(canvas);
-
- // Retrieve font charset
- var charset = Fonts[fontName].properties.charset || [];
- // if the charset is too small make it repeat a few times
- var count = 30;
- while (count-- && charset.length <= 30)
- charset = charset.concat(charset.slice());
-
- // Get the font size canvas think it will be for 'spaces'
- var ctx = canvas.getContext("2d");
- var testString = " ";
-
- // When debugging use the characters provided by the charsets to visually
- // see what's happening
- if (debug) {
- var encoding = this.encoding;
- for (var i = 0; i < charset.length; i++) {
- var unicode = GlyphsUnicode[charset[i]];
- if (!unicode)
- continue;
- testString += String.fromCharCode(unicode);
- }
+ Fonts[aName] = {
+ data: this.font,
+ properties: aProperties,
+ loading: true,
+ cache: Object.create(null)
}
- ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial";
- var textWidth = ctx.measureText(testString).width;
- if (debug)
- ctx.fillText(testString, 20, 20);
+ // Attach the font to the document
+ this.bind();
+ };
- var interval = window.setInterval(function canvasInterval(self) {
- this.start = this.start || Date.now();
- ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial";
+ /**
+ * A bunch of the OpenType code is duplicate between this class and the
+ * TrueType code, this is intentional and will merge in a future version
+ * where all the code relative to OpenType will probably have its own
+ * class and will take decision without the Fonts consent.
+ * But at the moment it allows to develop around the TrueType rewriting
+ * on the fly without messing up with the 'regular' Type1 to OTF conversion.
+ */
+ constructor.prototype = {
+ name: null,
+ font: null,
+ mimetype: null,
++ encoding: null,
+
+ bind: function font_bind() {
+ var data = this.font;
+
+ // Get the base64 encoding of the binary font data
+ var str = "";
+ var length = data.length;
+ for (var i = 0; i < length; ++i)
+ str += String.fromCharCode(data[i]);
+
+ var dataBase64 = window.btoa(str);
+ var fontName = this.name;
+
+ /** Hack begin */
+
+ // Actually there is not event when a font has finished downloading so
+ // the following tons of code are a dirty hack to 'guess' when a font is
+ // ready
+ var debug = false;
+
+ if (debug) {
+ var name = document.createElement("font");
+ name.setAttribute("style", "position: absolute; left: 20px; top: " +
+ (100 * fontCount + 60) + "px");
+ name.innerHTML = fontName;
+ document.body.appendChild(name);
+ }
- // For some reasons the font has not loaded, so mark it loaded for the
- // page to proceed but cry
- if ((Date.now() - this.start) >= kMaxWaitForFontFace) {
- window.clearInterval(interval);
- Fonts[fontName].loading = false;
- warn("Is " + fontName + " for charset: " + charset + " loaded?");
- this.start = 0;
- } else if (textWidth != ctx.measureText(testString).width) {
- window.clearInterval(interval);
- Fonts[fontName].loading = false;
- this.start = 0;
+ var canvas = document.createElement("canvas");
+ var style = "border: 1px solid black; position:absolute; top: " +
+ (debug ? (100 * fontCount) : "-200") + "px; left: 2px; width: 340px; height: 100px";
+ canvas.setAttribute("style", style);
+ canvas.setAttribute("width", 340);
+ canvas.setAttribute("heigth", 100);
+ document.body.appendChild(canvas);
+
+ // Retrieve font charset
- var charset = Fonts[fontName].charset || [];
++ var charset = Fonts[fontName].properties.charset || [];
++
+ // if the charset is too small make it repeat a few times
+ var count = 30;
+ while (count-- && charset.length <= 30)
+ charset = charset.concat(charset.slice());
+
+ // Get the font size canvas think it will be for 'spaces'
+ var ctx = canvas.getContext("2d");
+ var testString = " ";
+
+ // When debugging use the characters provided by the charsets to visually
+ // see what's happening
+ if (debug) {
+ for (var i = 0; i < charset.length; i++) {
+ var unicode = GlyphsUnicode[charset[i]];
+ if (!unicode)
- error("Unicode for " + charset[i] + " is has not been found in the glyphs list");
++ continue;
+ testString += String.fromCharCode(unicode);
+ }
}
+ ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial";
+ var textWidth = ctx.measureText(testString).width;
if (debug)
- ctx.fillText(testString, 20, 50);
- }, 50, this);
-
- /** Hack end */
-
- // Add the @font-face rule to the document
- var url = "url(data:" + this.mimetype + ";base64," + dataBase64 + ");";
- var rule = "@font-face { font-family:'" + fontName + "';src:" + url + "}";
- var styleSheet = document.styleSheets[0];
- styleSheet.insertRule(rule, styleSheet.length);
- },
-
- _createOpenTypeHeader: function font_createOpenTypeHeader(aFile, aOffsets, aNumTables) {
- // sfnt version (4 bytes)
- var version = [0x4F, 0x54, 0x54, 0X4F];
-
- // numTables (2 bytes)
- var numTables = aNumTables;
-
- // searchRange (2 bytes)
- var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables);
- var searchRange = tablesMaxPower2 * 16;
-
- // entrySelector (2 bytes)
- var entrySelector = Math.log(tablesMaxPower2) / Math.log(2);
-
- // rangeShift (2 bytes)
- var rangeShift = numTables * 16 - searchRange;
-
- var header = [].concat(version,
- FontsUtils.integerToBytes(numTables, 2),
- FontsUtils.integerToBytes(searchRange, 2),
- FontsUtils.integerToBytes(entrySelector, 2),
- FontsUtils.integerToBytes(rangeShift, 2));
- aFile.set(header, aOffsets.currentOffset);
- aOffsets.currentOffset += header.length;
- aOffsets.virtualOffset += header.length;
- },
-
- _createTableEntry: function font_createTableEntry(aFile, aOffsets, aTag, aData) {
- // tag
- var tag = [
- aTag.charCodeAt(0),
- aTag.charCodeAt(1),
- aTag.charCodeAt(2),
- aTag.charCodeAt(3)
- ];
-
- // offset
- var offset = aOffsets.virtualOffset;
+ ctx.fillText(testString, 20, 20);
+
- var start = Date.now();
+ var interval = window.setInterval(function canvasInterval(self) {
- ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial";
-
- // For some reasons the font has not loaded, so mark it loaded for the
- // page to proceed but cry
- if ((Date.now() - start) >= kMaxWaitForFontFace) {
- window.clearInterval(interval);
- Fonts[fontName].loading = false;
- warn("Is " + fontName + " for charset: " + charset + " loaded?");
- } else if (textWidth != ctx.measureText(testString).width) {
- window.clearInterval(interval);
- Fonts[fontName].loading = false;
- }
++ this.start = this.start || Date.now();
++ ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial";
++
++ // For some reasons the font has not loaded, so mark it loaded for the
++ // page to proceed but cry
++ if ((Date.now() - this.start) >= kMaxWaitForFontFace) {
++ window.clearInterval(interval);
++ Fonts[fontName].loading = false;
++ warn("Is " + fontName + " for charset: " + charset + " loaded?");
++ this.start = 0;
++ } else if (textWidth != ctx.measureText(testString).width) {
++ window.clearInterval(interval);
++ Fonts[fontName].loading = false;
++ this.start = 0;
++ }
- // Per spec tables must be 4-bytes align so add some 0x00 if needed
- while (aData.length & 3)
- aData.push(0x00);
- if (debug)
- ctx.fillText(testString, 20, 50);
- }, 50, this);
++ if (debug)
++ ctx.fillText(testString, 20, 50);
++ }, 50, this);
- // length
- var length = aData.length;
+ /** Hack end */
- // checksum
- var checksum = FontsUtils.bytesToInteger(tag) + offset + length;
+ // Add the @font-face rule to the document
+ var url = "url(data:" + this.mimetype + ";base64," + dataBase64 + ");";
+ var rule = "@font-face { font-family:'" + fontName + "';src:" + url + "}";
+ var styleSheet = document.styleSheets[0];
+ styleSheet.insertRule(rule, styleSheet.length);
+ },
- var tableEntry = [].concat(tag,
- FontsUtils.integerToBytes(checksum, 4),
- FontsUtils.integerToBytes(offset, 4),
- FontsUtils.integerToBytes(length, 4));
- aFile.set(tableEntry, aOffsets.currentOffset);
- aOffsets.currentOffset += tableEntry.length;
- aOffsets.virtualOffset += aData.length;
- },
- convert: function font_convert(aFont, aProperties) {
- var otf = new Uint8Array(kMaxFontFileSize);
++ cover: function font_cover(aName, aFont, aProperties) {
++ var otf = Uint8Array(kMaxFontFileSize);
- _createCMAPTable: function font_createCMAPTable(aGlyphs) {
- var characters = new Uint16Array(kMaxGlyphsCount);
- for (var i = 0; i < aGlyphs.length; i++)
- characters[aGlyphs[i].unicode] = i + 1;
+ function s2a(s) {
+ var a = [];
+ for (var i = 0; i < s.length; ++i)
+ a[i] = s.charCodeAt(i);
+ return a;
+ }
- // Separate the glyphs into continuous range of codes, aka segment.
- var ranges = [];
- var range = [];
- var count = characters.length;
- for (var i = 0; i < count; i++) {
- if (characters[i]) {
- range.push(i);
- } else if (range.length) {
- ranges.push(range.slice());
- range = [];
+ function s16(value) {
+ return String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff);
}
- }
- // The size in bytes of the header is equal to the size of the
- // different fields * length of a short + (size of the 4 parallels arrays
- // describing segments * length of a short).
- var headerSize = (12 * 2 + (ranges.length * 4 * 2));
+ function s32(value) {
+ return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) +
+ String.fromCharCode((value >> 8) & 0xff) + String.fromCharCode(value & 0xff);
+ }
- var segCount = ranges.length + 1;
- var segCount2 = segCount * 2;
- var searchRange = FontsUtils.getMaxPower2(segCount) * 2;
- var searchEntry = Math.log(segCount) / Math.log(2);
- var rangeShift = 2 * segCount - searchRange;
- var cmap = [].concat(
- [
- 0x00, 0x00, // version
- 0x00, 0x01, // numTables
- 0x00, 0x03, // platformID
- 0x00, 0x01, // encodingID
- 0x00, 0x00, 0x00, 0x0C, // start of the table record
- 0x00, 0x04 // format
- ],
- FontsUtils.integerToBytes(headerSize, 2), // length
- [0x00, 0x00], // language
- FontsUtils.integerToBytes(segCount2, 2),
- FontsUtils.integerToBytes(searchRange, 2),
- FontsUtils.integerToBytes(searchEntry, 2),
- FontsUtils.integerToBytes(rangeShift, 2)
- );
+ function createOpenTypeHeader(aFile, aOffsets, numTables) {
+ var header = "";
- // Fill up the 4 parallel arrays describing the segments.
- var startCount = [];
- var endCount = [];
- var idDeltas = [];
- var idRangeOffsets = [];
- var glyphsIdsArray = [];
- var bias = 0;
- for (var i = 0; i < segCount - 1; i++) {
- var range = ranges[i];
- var start = FontsUtils.integerToBytes(range[0], 2);
- var end = FontsUtils.integerToBytes(range[range.length - 1], 2);
+ // sfnt version (4 bytes)
+ header += "\x4F\x54\x54\x4F";
- var delta = FontsUtils.integerToBytes(((range[0] - 1) - bias) % 65536, 2);
- bias += range.length;
+ // numTables (2 bytes)
+ header += s16(numTables);
- // deltas are signed shorts
- delta[0] ^= 0xFF;
- delta[1] ^= 0xFF;
- delta[1] += 1;
+ // searchRange (2 bytes)
+ var tablesMaxPower2 = FontsUtils.getMaxPower2(numTables);
+ var searchRange = tablesMaxPower2 * 16;
+ header += s16(searchRange);
- startCount.push(start[0], start[1]);
- endCount.push(end[0], end[1]);
- idDeltas.push(delta[0], delta[1]);
- idRangeOffsets.push(0x00, 0x00);
+ // entrySelector (2 bytes)
+ header += s16(Math.log(tablesMaxPower2) / Math.log(2));
- for (var j = 0; j < range.length; j++)
- glyphsIdsArray.push(range[j]);
- }
- startCount.push(0xFF, 0xFF);
- endCount.push(0xFF, 0xFF);
- idDeltas.push(0x00, 0x01);
- idRangeOffsets.push(0x00, 0x00);
+ // rangeShift (2 bytes)
+ header += s16(numTables * 16 - searchRange);
- return cmap.concat(endCount, [0x00, 0x00], startCount,
- idDeltas, idRangeOffsets, glyphsIdsArray);
- },
+ aFile.set(s2a(header), aOffsets.currentOffset);
+ aOffsets.currentOffset += header.length;
+ aOffsets.virtualOffset += header.length;
+ }
- _createNameTable: function font_createNameTable(aName) {
- var names = [
- "See original licence", // Copyright
- aName, // Font family
- "undefined", // Font subfamily (font weight)
- "uniqueID", // Unique ID
- aName, // Full font name
- "0.1", // Version
- "undefined", // Postscript name
- "undefined", // Trademark
- "undefined", // Manufacturer
- "undefined" // Designer
- ];
+ function createTableEntry(aFile, aOffsets, aTag, aData) {
+ // offset
+ var offset = aOffsets.virtualOffset;
+
+ // Per spec tables must be 4-bytes align so add padding as needed
+ while (aData.length & 3)
+ aData.push(0x00);
+
+ // length
+ var length = aData.length;
+
+ // checksum
+ var checksum = aTag.charCodeAt(0) +
+ aTag.charCodeAt(1) +
- aTag.charCodeAt(2) +
- aTag.charCodeAt(3) +
++ aTag.charCodeAt(2) +
++ aTag.charCodeAt(3) +
+ offset +
+ length;
+
+ var tableEntry = aTag + s32(checksum) + s32(offset) + s32(length);
+ tableEntry = s2a(tableEntry);
+ aFile.set(tableEntry, aOffsets.currentOffset);
+ aOffsets.currentOffset += tableEntry.length;
+ aOffsets.virtualOffset += aData.length;
+ }
- var name = [
- 0x00, 0x00, // format
- 0x00, 0x0A, // Number of names Record
- 0x00, 0x7E // Storage
- ];
++ function createNameTable(aName) {
++ var names =
++ "See original licence" + // Copyright
++ aName + // Font family
++ "undefined" + // Font subfamily (font weight)
++ "uniqueID" + // Unique ID
++ aName + // Full font name
++ "0.1" + // Version
++ "undefined" + // Postscript name
++ "undefined" + // Trademark
++ "undefined" + // Manufacturer
++ "undefined"; // Designer
++
++ var name =
++ "\x00\x00" + // format
++ "\x00\x0A" + // Number of names Record
++ "\x00\x7E"; // Storage
++
++ // Build the name records field
++ var strOffset = 0;
++ for (var i = 0; i < names.length; i++) {
++ var str = names[i];
++
++ var nameRecord =
++ "\x00\x01" + // platform ID
++ "\x00\x00" + // encoding ID
++ "\x00\x00" + // language ID
++ "\x00\x00" + // name ID
++ s16(str.length) +
++ s16(strOffset);
++ name += nameRecord;
++
++ strOffset += str.length;
++ }
+
- // Build the name records field
- var strOffset = 0;
- for (var i = 0; i < names.length; i++) {
- var str = names[i];
++ name += names;
++ return name;
++ }
+
- var nameRecord = [
- 0x00, 0x01, // platform ID
- 0x00, 0x00, // encoding ID
- 0x00, 0x00, // language ID
- 0x00, 0x00 // name ID
- ];
+ function getRanges(glyphs) {
+ // Array.sort() sorts by characters, not numerically, so convert to an
+ // array of characters.
+ var codes = [];
+ var length = glyphs.length;
+ for (var n = 0; n < length; ++n)
+ codes.push(String.fromCharCode(glyphs[n].unicode))
+ codes.sort();
+
+ // Split the sorted codes into ranges.
+ var ranges = [];
+ for (var n = 0; n < length; ) {
+ var start = codes[n++].charCodeAt(0);
+ var end = start;
+ while (n < length && end + 1 == codes[n].charCodeAt(0)) {
+ ++end;
+ ++n;
+ }
+ ranges.push([start, end]);
+ }
+ return ranges;
+ }
- nameRecord = nameRecord.concat(
- FontsUtils.integerToBytes(str.length, 2),
- FontsUtils.integerToBytes(strOffset, 2)
- );
- name = name.concat(nameRecord);
+ function createCMAPTable(aGlyphs) {
+ var ranges = getRanges(aGlyphs);
+
+ var headerSize = (12 * 2 + (ranges.length * 4 * 2));
+ var segCount = ranges.length + 1;
+ var segCount2 = segCount * 2;
+ var searchRange = FontsUtils.getMaxPower2(segCount) * 2;
+ var searchEntry = Math.log(segCount) / Math.log(2);
+ var rangeShift = 2 * segCount - searchRange;
+
+ var cmap = "\x00\x00" + // version
+ "\x00\x01" + // numTables
+ "\x00\x03" + // platformID
+ "\x00\x01" + // encodingID
+ "\x00\x00\x00\x0C" + // start of the table record
+ "\x00\x04" + // format
+ s16(headerSize) + // length
+ "\x00\x00" + // languages
+ s16(segCount2) +
+ s16(searchRange) +
+ s16(searchEntry) +
+ s16(rangeShift);
+
+ // Fill up the 4 parallel arrays describing the segments.
+ var startCount = "";
+ var endCount = "";
+ var idDeltas = "";
+ var idRangeOffsets = "";
+ var glyphsIds = "";
+ var bias = 0;
+ for (var i = 0; i < segCount - 1; i++) {
+ var range = ranges[i];
+ var start = range[0];
+ var end = range[1];
+ var delta = (((start - 1) - bias) ^ 0xffff) + 1;
+ bias += (end - start + 1);
+
+ startCount += s16(start);
+ endCount += s16(end);
+ idDeltas += s16(delta);
+ idRangeOffsets += s16(0);
+
+ for (var j = start; j <= end; j++)
+ glyphsIds += String.fromCharCode(j);
+ }
- strOffset += str.length;
- }
+ startCount += "\xFF\xFF";
+ endCount += "\xFF\xFF";
+ idDeltas += "\x00\x01";
+ idRangeOffsets += "\x00\x00";
- // Add the name records data
- for (var i = 0; i < names.length; i++) {
- var str = names[i];
- var strBytes = [];
- for (var j = 0; j < str.length; j++) {
- strBytes.push(str.charCodeAt(j));
+ return s2a(cmap + endCount + "\x00\x00" + startCount +
+ idDeltas + idRangeOffsets + glyphsIds);
}
- name = name.concat(strBytes);
- }
-
- return name;
- },
- cover: function font_cover(aName, aFont, aProperties) {
- var otf = new Uint8Array(kMaxFontFileSize);
-
- // Required Tables
- var CFF = aFont.data, // PostScript Font Program
+ // Required Tables
+ var CFF = aFont.data, // PostScript Font Program
- OS2 = [], // OS/2 and Windows Specific metrics
- cmap = [], // Character to glyphs mapping
- head = [], // Font eader
- hhea = [], // Horizontal header
- hmtx = [], // Horizontal metrics
- maxp = [], // Maximum profile
- name = [], // Naming tables
- post = []; // PostScript informations
+ OS2 = [], // OS/2 and Windows Specific metrics
+ cmap = [], // Character to glyphs mapping
+ head = [], // Font eader
+ hhea = [], // Horizontal header
+ hmtx = [], // Horizontal metrics
+ maxp = [], // Maximum profile
+ name = [], // Naming tables
+ post = []; // PostScript informations
- var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post];
-
- // The offsets object holds at the same time a representation of where
- // to write the table entry information about a table and another offset
- // representing the offset where to draw the actual data of a particular
- // table
- var offsets = {
- currentOffset: 0,
- virtualOffset: tables.length * (4 * 4)
- };
-
- // For files with only one font the offset table is the first thing of the
- // file
- this._createOpenTypeHeader(otf, offsets, tables.length);
-
- // XXX It is probable that in a future we want to get rid of this glue
- // between the CFF and the OTF format in order to be able to embed TrueType
- // data.
- this._createTableEntry(otf, offsets, "CFF ", CFF);
-
- /** OS/2 */
- OS2 = [
- 0x00, 0x03, // version
- 0x02, 0x24, // xAvgCharWidth
- 0x01, 0xF4, // usWeightClass
- 0x00, 0x05, // usWidthClass
- 0x00, 0x00, // fstype
- 0x02, 0x8A, // ySubscriptXSize
- 0x02, 0xBB, // ySubscriptYSize
- 0x00, 0x00, // ySubscriptXOffset
- 0x00, 0x8C, // ySubscriptYOffset
- 0x02, 0x8A, // ySuperScriptXSize
- 0x02, 0xBB, // ySuperScriptYSize
- 0x00, 0x00, // ySuperScriptXOffset
- 0x01, 0xDF, // ySuperScriptYOffset
- 0x00, 0x31, // yStrikeOutSize
- 0x01, 0x02, // yStrikeOutPosition
- 0x00, 0x00, // sFamilyClass
- 0x02, 0x00, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Panose
- 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 0-31)
- 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 32-63)
- 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 64-95)
- 0xFF, 0xFF, 0xFF, 0xFF, // ulUnicodeRange1 (Bits 96-127)
- 0x2A, 0x32, 0x31, 0x2A, // achVendID
- 0x00, 0x20, // fsSelection
- 0x00, 0x2D, // usFirstCharIndex
- 0x00, 0x7A, // usLastCharIndex
- 0x00, 0x03, // sTypoAscender
- 0x00, 0x20, // sTypeDescender
- 0x00, 0x38, // sTypoLineGap
- 0x00, 0x5A, // usWinAscent
- 0x02, 0xB4, // usWinDescent
- 0x00, 0xCE, 0x00, 0x00, // ulCodePageRange1 (Bits 0-31)
- 0x00, 0x01, 0x00, 0x00, // ulCodePageRange2 (Bits 32-63)
- 0x00, 0x00, // sxHeight
- 0x00, 0x00, // sCapHeight
- 0x00, 0x01, // usDefaultChar
- 0x00, 0xCD, // usBreakChar
- 0x00, 0x02 // usMaxContext
- ];
- this._createTableEntry(otf, offsets, "OS/2", OS2);
-
- //XXX Getting charstrings here seems wrong since this is another CFF glue
- var charstrings = aFont.getOrderedCharStrings(aProperties.glyphs);
-
- /** CMAP */
- cmap = this._createCMAPTable(charstrings);
- this._createTableEntry(otf, offsets, "cmap", cmap);
-
- /** HEAD */
- head = [
- 0x00, 0x01, 0x00, 0x00, // Version number
- 0x00, 0x00, 0x50, 0x00, // fontRevision
- 0x00, 0x00, 0x00, 0x00, // checksumAdjustement
- 0x5F, 0x0F, 0x3C, 0xF5, // magicNumber
- 0x00, 0x00, // Flags
- 0x03, 0xE8, // unitsPerEM (defaulting to 1000)
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // creation date
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // modifification date
- 0x00, 0x00, // xMin
- 0x00, 0x00, // yMin
- 0x00, 0x00, // xMax
- 0x00, 0x00, // yMax
- 0x00, 0x00, // macStyle
- 0x00, 0x00, // lowestRecPPEM
- 0x00, 0x00, // fontDirectionHint
- 0x00, 0x00, // indexToLocFormat
- 0x00, 0x00 // glyphDataFormat
- ];
- this._createTableEntry(otf, offsets, "head", head);
-
- /** HHEA */
- hhea = [].concat(
- [
- 0x00, 0x01, 0x00, 0x00, // Version number
- 0x00, 0x00, // Typographic Ascent
- 0x00, 0x00, // Typographic Descent
- 0x00, 0x00, // Line Gap
- 0xFF, 0xFF, // advanceWidthMax
- 0x00, 0x00, // minLeftSidebearing
- 0x00, 0x00, // minRightSidebearing
- 0x00, 0x00, // xMaxExtent
- 0x00, 0x00, // caretSlopeRise
- 0x00, 0x00, // caretSlopeRun
- 0x00, 0x00, // caretOffset
- 0x00, 0x00, // -reserved-
- 0x00, 0x00, // -reserved-
- 0x00, 0x00, // -reserved-
- 0x00, 0x00, // -reserved-
- 0x00, 0x00 // metricDataFormat
- ],
- FontsUtils.integerToBytes(charstrings.length, 2) // numberOfHMetrics
- );
- this._createTableEntry(otf, offsets, "hhea", hhea);
-
- /** HMTX */
- hmtx = [0x01, 0xF4, 0x00, 0x00];
- for (var i = 0; i < charstrings.length; i++) {
- var charstring = charstrings[i].charstring;
- var width = FontsUtils.integerToBytes(charstring[1], 2);
- var lsb = FontsUtils.integerToBytes(charstring[0], 2);
- hmtx = hmtx.concat(width, lsb);
- }
- this._createTableEntry(otf, offsets, "hmtx", hmtx);
-
- /** MAXP */
- maxp = [].concat(
- [
- 0x00, 0x00, 0x50, 0x00, // Version number
- ],
- FontsUtils.integerToBytes(charstrings.length + 1, 2) // Num of glyphs (+1 to pass the sanitizer...)
- );
- this._createTableEntry(otf, offsets, "maxp", maxp);
-
- /** NAME */
- var name = this._createNameTable(aName);
- this._createTableEntry(otf, offsets, "name", name);
-
- /** POST */
- // FIXME Get those informations from the FontInfo structure
- post = [
- 0x00, 0x03, 0x00, 0x00, // Version number
- 0x00, 0x00, 0x01, 0x00, // italicAngle
- 0x00, 0x00, // underlinePosition
- 0x00, 0x00, // underlineThickness
- 0x00, 0x00, 0x00, 0x00, // isFixedPitch
- 0x00, 0x00, 0x00, 0x00, // minMemType42
- 0x00, 0x00, 0x00, 0x00, // maxMemType42
- 0x00, 0x00, 0x00, 0x00, // minMemType1
- 0x00, 0x00, 0x00, 0x00 // maxMemType1
- ];
- this._createTableEntry(otf, offsets, "post", post);
+ var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post];
+
+ // The offsets object holds at the same time a representation of where
+ // to write the table entry information about a table and another offset
+ // representing the offset where to draw the actual data of a particular
+ // table
+ var offsets = {
+ currentOffset: 0,
+ virtualOffset: tables.length * (4 * 4)
+ };
+
+ // For files with only one font the offset table is the first thing of the
+ // file
+ createOpenTypeHeader(otf, offsets, tables.length);
+
+ // TODO: It is probable that in a future we want to get rid of this glue
+ // between the CFF and the OTF format in order to be able to embed TrueType
+ // data.
+ createTableEntry(otf, offsets, "CFF ", CFF);
+
+ /** OS/2 */
+ OS2 = s2a(
+ "\x00\x03" + // version
+ "\x02\x24" + // xAvgCharWidth
+ "\x01\xF4" + // usWeightClass
+ "\x00\x05" + // usWidthClass
+ "\x00\x00" + // fstype
+ "\x02\x8A" + // ySubscriptXSize
+ "\x02\xBB" + // ySubscriptYSize
+ "\x00\x00" + // ySubscriptXOffset
+ "\x00\x8C" + // ySubscriptYOffset
+ "\x02\x8A" + // ySuperScriptXSize
+ "\x02\xBB" + // ySuperScriptYSize
+ "\x00\x00" + // ySuperScriptXOffset
+ "\x01\xDF" + // ySuperScriptYOffset
+ "\x00\x31" + // yStrikeOutSize
+ "\x01\x02" + // yStrikeOutPosition
+ "\x00\x00" + // sFamilyClass
+ "\x02\x00\x06\x03\x00\x00\x00\x00\x00\x00" + // Panose
+ "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 0-31)
+ "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 32-63)
+ "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 64-95)
+ "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 96-127)
+ "\x2A\x32\x31\x2A" + // achVendID
+ "\x00\x20" + // fsSelection
+ "\x00\x2D" + // usFirstCharIndex
+ "\x00\x7A" + // usLastCharIndex
+ "\x00\x03" + // sTypoAscender
+ "\x00\x20" + // sTypeDescender
+ "\x00\x38" + // sTypoLineGap
+ "\x00\x5A" + // usWinAscent
+ "\x02\xB4" + // usWinDescent
+ "\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31)
+ "\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63)
+ "\x00\x00" + // sxHeight
+ "\x00\x00" + // sCapHeight
+ "\x00\x01" + // usDefaultChar
+ "\x00\xCD" + // usBreakChar
+ "\x00\x02" // usMaxContext
+ );
+ createTableEntry(otf, offsets, "OS/2", OS2);
+
+ //XXX Getting charstrings here seems wrong since this is another CFF glue
+ var charstrings = aFont.getOrderedCharStrings(aProperties.glyphs);
+
+ /** CMAP */
+ cmap = createCMAPTable(charstrings);
+ createTableEntry(otf, offsets, "cmap", cmap);
+
+ /** HEAD */
+ head = s2a(
+ "\x00\x01\x00\x00" + // Version number
+ "\x00\x00\x50\x00" + // fontRevision
+ "\x00\x00\x00\x00" + // checksumAdjustement
+ "\x5F\x0F\x3C\xF5" + // magicNumber
+ "\x00\x00" + // Flags
+ "\x03\xE8" + // unitsPerEM (defaulting to 1000)
+ "\x00\x00\x00\x00\x00\x00\x00\x00" + // creation date
+ "\x00\x00\x00\x00\x00\x00\x00\x00" + // modifification date
+ "\x00\x00" + // xMin
+ "\x00\x00" + // yMin
+ "\x00\x00" + // xMax
+ "\x00\x00" + // yMax
+ "\x00\x00" + // macStyle
+ "\x00\x00" + // lowestRecPPEM
+ "\x00\x00" + // fontDirectionHint
+ "\x00\x00" + // indexToLocFormat
+ "\x00\x00" // glyphDataFormat
+ );
+ createTableEntry(otf, offsets, "head", head);
+
+ /** HHEA */
+ hhea = s2a(
+ "\x00\x01\x00\x00" + // Version number
+ "\x00\x00" + // Typographic Ascent
+ "\x00\x00" + // Typographic Descent
+ "\x00\x00" + // Line Gap
+ "\xFF\xFF" + // advanceWidthMax
+ "\x00\x00" + // minLeftSidebearing
+ "\x00\x00" + // minRightSidebearing
+ "\x00\x00" + // xMaxExtent
+ "\x00\x00" + // caretSlopeRise
+ "\x00\x00" + // caretSlopeRun
+ "\x00\x00" + // caretOffset
+ "\x00\x00" + // -reserved-
+ "\x00\x00" + // -reserved-
+ "\x00\x00" + // -reserved-
+ "\x00\x00" + // -reserved-
+ "\x00\x00" + // metricDataFormat
+ s16(charstrings.length)
+ );
+ createTableEntry(otf, offsets, "hhea", hhea);
+
+ /** HMTX */
+ hmtx = "\x01\xF4\x00\x00";
+ for (var i = 0; i < charstrings.length; i++) {
+ var charstring = charstrings[i].charstring;
+ var width = charstring[1];
+ var lsb = charstring[0];
+ hmtx += s16(width) + s16(lsb);
+ }
+ hmtx = s2a(hmtx);
+ createTableEntry(otf, offsets, "hmtx", hmtx);
+
+ /** MAXP */
+ maxp = "\x00\x00\x50\x00" + // Version number
+ s16(charstrings.length + 1); // Num of glyphs (+1 to pass the sanitizer...)
+ maxp = s2a(maxp);
+ createTableEntry(otf, offsets, "maxp", maxp);
+
+ /** NAME */
- name = "\x00\x00" + // Format
- "\x00\x00" + // Number of name records
- "\x00\x00"; // Storage
- name = s2a(name);
++ name = s2a(createNameTable(aName));
+ createTableEntry(otf, offsets, "name", name);
+
+ /** POST */
+ // TODO: get those informations from the FontInfo structure
+ post = "\x00\x03\x00\x00" + // Version number
+ "\x00\x00\x01\x00" + // italicAngle
+ "\x00\x00" + // underlinePosition
+ "\x00\x00" + // underlineThickness
+ "\x00\x00\x00\x00" + // isFixedPitch
+ "\x00\x00\x00\x00" + // minMemType42
+ "\x00\x00\x00\x00" + // maxMemType42
+ "\x00\x00\x00\x00" + // minMemType1
+ "\x00\x00\x00\x00"; // maxMemType1
+ post = s2a(post);
+ createTableEntry(otf, offsets, "post", post);
+
+ // Once all the table entries header are written, dump the data!
+ var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post];
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ otf.set(table, offsets.currentOffset);
+ offsets.currentOffset += table.length;
+ }
- // Once all the table entries header are written, dump the data!
- var tables = [CFF, OS2, cmap, head, hhea, hmtx, maxp, name, post];
- for (var i = 0; i < tables.length; i++) {
- var table = tables[i];
- otf.set(table, offsets.currentOffset);
- offsets.currentOffset += table.length;
+ var fontData = [];
+ for (var i = 0; i < offsets.currentOffset; i++)
+ fontData.push(otf[i]);
+ return fontData;
}
+ };
- var fontData = [];
- for (var i = 0; i < offsets.currentOffset; i++)
- fontData.push(otf[i]);
- return fontData;
- }
- };
-
+ return constructor;
+ })();
/**
* FontsUtils is a static class dedicated to hold codes that are not related
for (var i = 0; i < numTables; i++) {
var table = this._readTableEntry(aFile);
var index = requiredTables.indexOf(table.tag);
- if (index != -1)
- requiredTables.splice(index, 1);
+ if (index != -1) {
+ if (table.tag == "cmap")
+ originalCMAP = table;
- requiredTables.splice(index, 1);
- tables.push(table);
++ tables.push(table);
+ }
-
- tables.push(table);
}
- // Tables needs to be written by ascendant alphabetic order
- tables.sort(function(a, b) {
- return a.tag > b.tag;
- });
-
// If any tables are still in the array this means some required tables are
// missing, which means that we need to rebuild the font in order to pass
// the sanitizer.
0x00, 0x02 // usMaxContext
];
- 0x00, 0x00, // underlinePosition
+ // If the font is missing a OS/2 table it's could be an old mac font
+ // without a 3-1-4 Unicode BMP table, so let's rewrite it.
+ var charset = aProperties.charset;
+ var glyphs = [];
+ for (var i = 0; i < charset.length; i++) {
+ glyphs.push({
+ unicode: GlyphsUnicode[charset[i]]
+ });
+ }
+
+
+ var offsetDelta = 0;
+
+ // Replace the old CMAP table
+ var rewrittedCMAP = this._createCMAPTable(glyphs);
+ offsetDelta = rewrittedCMAP.length - originalCMAP.data.length;
+ originalCMAP.data = rewrittedCMAP;
+
+ // Rewrite the 'post' table if needed
+ var postTable = null;
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ if (table.tag == "post") {
+ postTable = table;
+ break;
+ }
+ }
+
+ if (!postTable) {
+ var post = [
+ 0x00, 0x03, 0x00, 0x00, // Version number
+ 0x00, 0x00, 0x01, 0x00, // italicAngle
++ 0x00, 0x00, // underlinePosition
+ 0x00, 0x00, // underlineThickness
+ 0x00, 0x00, 0x00, 0x00, // isFixedPitch
+ 0x00, 0x00, 0x00, 0x00, // minMemType42
+ 0x00, 0x00, 0x00, 0x00, // maxMemType42
+ 0x00, 0x00, 0x00, 0x00, // minMemType1
+ 0x00, 0x00, 0x00, 0x00 // maxMemType1
+ ];
+
+ offsetDelta += post.length;
+ tables.unshift({
+ tag: "post",
+ data: post
+ });
+ }
+
// Create a new file to hold the new version of our truetype with a new
// header and new offsets
var stream = aFile.stream || aFile;
data: OS2
});
-
+ // Tables needs to be written by ascendant alphabetic order
+ tables.sort(function(a, b) {
+ return a.tag > b.tag;
+ });
+
// rewrite the tables but tweak offsets
for (var i = 0; i < tables.length; i++) {
var table = tables[i];
offsets.currentOffset++;
}
- this.data = ttf;
+ var fontData = [];
+ for (var i = 0; i < offsets.currentOffset; i++)
+ fontData.push(ttf[i]);
+
+ this.data = fontData;
return;
- } else if (requiredTables.lenght) {
+ } else if (requiredTables.length) {
- warn("Missing " + requiredTables + " in the TrueType font");
+ error("Table " + requiredTables[0] + " is missing from the TruType font");
+ } else {
+ this.data = aFile.getBytes();
}
- this.data = aFile;
};
TrueType.prototype = {
aOffsets.virtualOffset += header.length;
},
- _createTableEntry: function font_createTableEntry(aFile, aOffsets, aTag, aData) {
- // tag
- var tag = [
- aTag.charCodeAt(0),
- aTag.charCodeAt(1),
- aTag.charCodeAt(2),
- aTag.charCodeAt(3)
- ];
-
- // Per spec tables must be 4-bytes align so add some 0x00 if needed
- while (aData.length & 3)
- aData.push(0x00);
-
- while (aOffsets.virtualOffset & 3)
- aOffsets.virtualOffset++;
-
- // offset
- var offset = aOffsets.virtualOffset;
-
- // length
- var length = aData.length;
-
- // checksum
- var checksum = FontsUtils.bytesToInteger(tag) + offset + length;
-
- var tableEntry = [].concat(tag,
- FontsUtils.integerToBytes(checksum, 4),
- FontsUtils.integerToBytes(offset, 4),
- FontsUtils.integerToBytes(length, 4));
- aFile.set(tableEntry, aOffsets.currentOffset);
- aOffsets.currentOffset += tableEntry.length;
- aOffsets.virtualOffset += aData.length;
- },
-
- _readOpenTypeHeader: function tt_readOpenTypeHeader(aFile) {
- return {
- version: aFile.getBytes(4),
- numTables: FontsUtils.bytesToInteger(aFile.getBytes(2)),
- searchRange: FontsUtils.bytesToInteger(aFile.getBytes(2)),
- entrySelector: FontsUtils.bytesToInteger(aFile.getBytes(2)),
- rangeShift: FontsUtils.bytesToInteger(aFile.getBytes(2))
- }
- },
-
- _readTableEntry: function tt_readTableEntry(aFile) {
- // tag
- var tag = aFile.getBytes(4);
- tag = String.fromCharCode(tag[0]) +
- String.fromCharCode(tag[1]) +
- String.fromCharCode(tag[2]) +
- String.fromCharCode(tag[3]);
-
- var checksum = FontsUtils.bytesToInteger(aFile.getBytes(4));
- var offset = FontsUtils.bytesToInteger(aFile.getBytes(4));
- var length = FontsUtils.bytesToInteger(aFile.getBytes(4));
-
- // Read the table associated data
- var currentPosition = aFile.pos;
- aFile.pos = aFile.start + offset;
- var data = aFile.getBytes(length);
- aFile.pos = currentPosition;
-
- return {
- tag: tag,
- checksum: checksum,
- length: offset,
- offset: length,
- data: data
- }
- },
-
+ _createCMAPTable: function font_createCMAPTable(aGlyphs) {
- var characters = new Uint16Array(kMaxGlyphsCount);
++ var characters = Uint16Array(65535);
+ for (var i = 0; i < aGlyphs.length; i++)
+ characters[aGlyphs[i].unicode] = i + 1;
+
+ // Separate the glyphs into continuous range of codes, aka segment.
+ var ranges = [];
+ var range = [];
+ var count = characters.length;
+ for (var i = 0; i < count; i++) {
+ if (characters[i]) {
+ range.push(i);
+ } else if (range.length) {
+ ranges.push(range.slice());
+ range = [];
+ }
+ }
+
+ // The size in bytes of the header is equal to the size of the
+ // different fields * length of a short + (size of the 4 parallels arrays
+ // describing segments * length of a short).
+ var headerSize = (12 * 2 + (ranges.length * 4 * 2));
+
+ var segCount = ranges.length + 1;
+ var segCount2 = segCount * 2;
+ var searchRange = FontsUtils.getMaxPower2(segCount) * 2;
+ var searchEntry = Math.log(segCount) / Math.log(2);
+ var rangeShift = 2 * segCount - searchRange;
+ var cmap = [].concat(
+ [
+ 0x00, 0x00, // version
+ 0x00, 0x01, // numTables
+ 0x00, 0x03, // platformID
+ 0x00, 0x01, // encodingID
+ 0x00, 0x00, 0x00, 0x0C, // start of the table record
+ 0x00, 0x04 // format
+ ],
+ FontsUtils.integerToBytes(headerSize, 2), // length
+ [0x00, 0x00], // language
+ FontsUtils.integerToBytes(segCount2, 2),
+ FontsUtils.integerToBytes(searchRange, 2),
+ FontsUtils.integerToBytes(searchEntry, 2),
+ FontsUtils.integerToBytes(rangeShift, 2)
+ );
+
+ // Fill up the 4 parallel arrays describing the segments.
+ var startCount = [];
+ var endCount = [];
+ var idDeltas = [];
+ var idRangeOffsets = [];
+ var glyphsIdsArray = [];
+ var bias = 0;
+ for (var i = 0; i < segCount - 1; i++) {
+ var range = ranges[i];
+ var start = FontsUtils.integerToBytes(range[0], 2);
+ var end = FontsUtils.integerToBytes(range[range.length - 1], 2);
+
+ var delta = FontsUtils.integerToBytes(((range[0] - 1) - bias) % 65536, 2);
+ bias += range.length;
+
+ // deltas are signed shorts
+ delta[0] ^= 0xFF;
+ delta[1] ^= 0xFF;
+ delta[1] += 1;
+
+ startCount.push(start[0], start[1]);
+ endCount.push(end[0], end[1]);
+ idDeltas.push(delta[0], delta[1]);
+ idRangeOffsets.push(0x00, 0x00);
+
+ for (var j = 0; j < range.length; j++)
+ glyphsIdsArray.push(range[j]);
+ }
+ startCount.push(0xFF, 0xFF);
+ endCount.push(0xFF, 0xFF);
+ idDeltas.push(0x00, 0x01);
+ idRangeOffsets.push(0x00, 0x00);
+
+ return cmap.concat(endCount, [0x00, 0x00], startCount,
+ idDeltas, idRangeOffsets, glyphsIdsArray);
++ },
++
+ _createTableEntry: function font_createTableEntry(aFile, aOffsets, aTag, aData) {
+ // tag
+ var tag = [
+ aTag.charCodeAt(0),
+ aTag.charCodeAt(1),
+ aTag.charCodeAt(2),
+ aTag.charCodeAt(3)
+ ];
+
+ // Per spec tables must be 4-bytes align so add some 0x00 if needed
+ while (aData.length & 3)
+ aData.push(0x00);
+
+ while (aOffsets.virtualOffset & 3)
+ aOffsets.virtualOffset++;
+
+ // offset
+ var offset = aOffsets.virtualOffset;
+
+ // length
+ var length = aData.length;
+
+ // checksum
+ var checksum = FontsUtils.bytesToInteger(tag) + offset + length;
+
+ var tableEntry = [].concat(tag,
+ FontsUtils.integerToBytes(checksum, 4),
+ FontsUtils.integerToBytes(offset, 4),
+ FontsUtils.integerToBytes(length, 4));
+ aFile.set(tableEntry, aOffsets.currentOffset);
+ aOffsets.currentOffset += tableEntry.length;
+ aOffsets.virtualOffset += aData.length;
+ },
+
+ _readOpenTypeHeader: function tt_readOpenTypeHeader(aFile) {
+ return {
+ version: aFile.getBytes(4),
+ numTables: FontsUtils.bytesToInteger(aFile.getBytes(2)),
+ searchRange: FontsUtils.bytesToInteger(aFile.getBytes(2)),
+ entrySelector: FontsUtils.bytesToInteger(aFile.getBytes(2)),
+ rangeShift: FontsUtils.bytesToInteger(aFile.getBytes(2))
+ }
+ },
+
+ _readTableEntry: function tt_readTableEntry(aFile) {
+ // tag
+ var tag = aFile.getBytes(4);
+ tag = String.fromCharCode(tag[0]) +
+ String.fromCharCode(tag[1]) +
+ String.fromCharCode(tag[2]) +
+ String.fromCharCode(tag[3]);
+
+ var checksum = FontsUtils.bytesToInteger(aFile.getBytes(4));
+ var offset = FontsUtils.bytesToInteger(aFile.getBytes(4));
+ var length = FontsUtils.bytesToInteger(aFile.getBytes(4));
+
+ // Read the table associated data
+ var currentPosition = aFile.pos;
+ aFile.pos = aFile.start + offset;
+ var data = aFile.getBytes(length);
+ aFile.pos = currentPosition;
+
+ return {
+ tag: tag,
+ checksum: checksum,
+ length: offset,
+ offset: length,
+ data: data
+ }
}
};
return value;
}
-function bytesToString(bytes) {
- var str = "";
- var length = bytes.length;
- for (var n = 0; n < length; ++n)
- str += String.fromCharCode(bytes[n]);
- return str;
-}
-
var Stream = (function() {
function constructor(arrayBuffer, start, length, dict) {
- this.bytes = new Uint8Array(arrayBuffer);
+ this.bytes = Uint8Array(arrayBuffer);
this.start = start || 0;
this.pos = this.start;
this.end = (start + length) || this.bytes.byteLength;
return this.buffer = buffer2;
},
getByte: function() {
+ var bufferLength = this.bufferLength;
var pos = this.pos;
- if (bufferLength == pos) {
- while (this.bufferLength <= pos) {
++ if (bufferLength <= pos) {
if (this.eof)
return;
this.readBlock();
return this.buffer.subarray(pos, end)
},
lookChar: function() {
+ var bufferLength = this.bufferLength;
var pos = this.pos;
- if (bufferLength == pos) {
- while (this.bufferLength <= pos) {
++ if (bufferLength <= pos) {
if (this.eof)
return;
this.readBlock();
return [codes, maxLen];
},
readBlock: function() {
- var bytes = this.bytes;
- var bytesPos = this.bytesPos;
+ 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
var hdr = this.getBits(3);
return constructor;
})();
-// A JpegStream can't be read directly. We use the platform to render the underlying
-// JPEG data for us.
-var JpegStream = (function() {
- function constructor(bytes, dict) {
- // TODO: per poppler, some images may have "junk" before that need to be removed
- this.dict = dict;
-
- // create DOM image
- var img = new Image();
- img.src = "data:image/jpeg;base64," + window.btoa(bytesToString(bytes));
- this.domImage = img;
- }
-
- constructor.prototype = {
- getImage: function() {
- return this.domImage;
- }
- };
-
- return constructor;
-})();
-
+ var PredictorStream = (function() {
+ function constructor(stream, params) {
+ this.stream = stream;
- this.dict = stream.dict;
+ this.predictor = params.get("Predictor") || 1;
+ if (this.predictor <= 1) {
+ return stream; // no prediction
+ }
+ if (params.has("EarlyChange")) {
+ error("EarlyChange predictor parameter is not supported");
+ }
+ this.colors = params.get("Colors") || 1;
+ this.bitsPerComponent = params.get("BitsPerComponent") || 8;
+ this.columns = params.get("Columns") || 1;
+ if (this.colors !== 1 || this.bitsPerComponent !== 8) {
+ error("Multi-color and multi-byte predictors are not supported");
+ }
+ if (this.predictor < 10 || this.predictor > 15) {
+ error("Unsupported predictor");
+ }
+ this.currentRow = new Uint8Array(this.columns);
+ this.pos = 0;
+ this.bufferLength = 0;
+ }
+
+ constructor.prototype = {
+ readRow : function() {
+ var lastRow = this.currentRow;
+ var predictor = this.stream.getByte();
+ var currentRow = this.stream.getBytes(this.columns), i;
+ switch (predictor) {
+ default:
+ error("Unsupported predictor");
+ break;
+ case 0:
+ break;
+ case 2:
+ for (i = 0; i < currentRow.length; ++i) {
+ currentRow[i] = (lastRow[i] + currentRow[i]) & 0xFF;
+ }
+ break;
+ }
+ this.pos = 0;
+ this.bufferLength = currentRow.length;
+ this.currentRow = currentRow;
+ },
+ getByte : function() {
+ if (this.pos >= this.bufferLength) {
+ this.readRow();
+ }
+ return this.currentRow[this.pos++];
+ },
+ getBytes : function(n) {
+ var i, bytes;
+ bytes = new Uint8Array(n);
+ for (i = 0; i < n; ++i) {
+ if (this.pos >= this.bufferLength) {
+ this.readRow();
+ }
+ bytes[i] = this.currentRow[this.pos++];
+ }
+ return bytes;
+ },
+ getChar : function() {
+ return String.formCharCode(this.getByte());
+ },
+ lookChar : function() {
+ if (this.pos >= this.bufferLength) {
+ this.readRow();
+ }
+ return String.formCharCode(this.currentRow[this.pos]);
+ },
+ skip : function(n) {
+ var i;
+ if (!n) {
+ n = 1;
+ }
+ while (n > this.bufferLength - this.pos) {
+ n -= this.bufferLength - this.pos;
+ this.readRow();
+ if (this.bufferLength === 0) break;
+ }
+ this.pos += n;
+ }
+ };
+
+ return constructor;
+ })();
+
var DecryptStream = (function() {
function constructor(str, fileKey, encAlgorithm, keyLength) {
- // TODO
+ TODO("decrypt stream is not implemented");
}
constructor.prototype = Stream.prototype;
this.encAlgorithm,
this.keyLength);
}
- return this.filter(stream, dict);
- stream = this.filter(stream, dict, length);
- stream.parameters = dict;
++ stream = this.filter(stream, dict);
++ stream.parameters = dict;
+ return stream;
},
- filter: function(stream, dict, length) {
+ filter: function(stream, dict) {
var filter = dict.get2("Filter", "F");
var params = dict.get2("DecodeParms", "DP");
if (IsName(filter))
}
return stream;
},
- makeFilter: function(stream, name, length, params) {
+ makeFilter: function(stream, name, params) {
if (name == "FlateDecode" || name == "Fl") {
- if (params)
- error("params not supported yet for FlateDecode");
+ if (params) {
+ return new PredictorStream(new FlateStream(stream), params);
+ }
return new FlateStream(stream);
- } else if (name == "DCTDecode") {
- var bytes = stream.getBytes(length);
- return new JpegStream(bytes, stream.dict);
} else {
error("filter '" + name + "' not supported yet");
}
} else if (IsRef(obj)) {
// certain buggy PDF generators generate "/Prev NNN 0 R" instead
// of "/Prev NNN"
- this.prev = obj.num;
- more = true;
+ prev = obj.num;
+ }
+ if (prev) {
- this.readXRef(prev);
++ this.readXRef(prev);
}
- this.trailerDict = dict;
// check for 'XRefStm' key
if (IsInt(obj = dict.get("XRefStm"))) {
this.xrefstms[pos] = 1; // avoid infinite recursion
this.readXRef(pos);
}
- return more;
- },
- readXRefStream: function(parser) {
- error("Invalid XRef stream");
+
+ return dict;
+ },
+ readXRefStream: function(stream) {
+ var streamParameters = stream.parameters;
+ var length = streamParameters.get("Length");
+ var byteWidths = streamParameters.get("W");
+ var range = streamParameters.get("Index");
+ if (!range)
+ range = [0, streamParameters.get("Size")];
+ var i, j;
+ while (range.length > 0) {
+ var first = range[0], n = range[1];
+ if (!IsInt(first) || !IsInt(n))
+ error("Invalid XRef range fields");
+ var typeFieldWidth = byteWidths[0], offsetFieldWidth = byteWidths[1], generationFieldWidth = byteWidths[2];
+ if (!IsInt(typeFieldWidth) || !IsInt(offsetFieldWidth) || !IsInt(generationFieldWidth))
+ error("Invalid XRef entry fields length");
+ for (i = 0; i < n; ++i) {
+ var type = 0, offset = 0, generation = 0;
+ for (j = 0; j < typeFieldWidth; ++j)
+ type = (type << 8) | stream.getByte();
+ for (j = 0; j < offsetFieldWidth; ++j)
+ offset = (offset << 8) | stream.getByte();
+ for (j = 0; j < generationFieldWidth; ++j)
+ generation = (generation << 8) | stream.getByte();
+ var entry = new Ref(offset, generation);
+ if (typeFieldWidth > 0) {
+ switch (type) {
+ case 0:
+ entry.free = true;
+ break;
+ case 1:
+ entry.uncompressed = true;
+ break;
+ case 2:
+ break;
+ default:
+ error("Invalid XRef entry type");
+ break;
+ }
+ }
+ if (!this.entries[first + i])
+ this.entries[first + i] = entry;
+ }
+ range.splice(0, 2);
+ }
+ var prev = streamParameters.get("Prev");
+ if (IsInt(prev))
+ this.readXRef(prev);
+ return streamParameters;
},
readXRef: function(startXRef) {
var stream = this.stream;
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);
: null));
},
compile: function(gfx, fonts) {
- if (!this.code) {
- var xref = this.xref;
- var content = xref.fetchIfRef(this.content);
- var resources = xref.fetchIfRef(this.resources);
+ if (this.code) {
+ // content was compiled
+ return;
+ }
++
+ var xref = this.xref;
+ var content;
+ var resources = xref.fetchIfRef(this.resources);
+ if (!IsArray(this.content)) {
+ // content is not an array, shortcut
+ content = xref.fetchIfRef(this.content);
this.code = gfx.compile(content, xref, resources, fonts);
+ return;
+ }
+ // the content is an array, compiling all items
+ var i, n = this.content.length, compiledItems = [];
+ for (i = 0; i < n; ++i) {
+ content = xref.fetchIfRef(this.content[i]);
+ compiledItems.push(gfx.compile(content, xref, resources, fonts));
}
+ // creating the function that executes all compiled items
+ this.code = function(gfx) {
+ var i, n = compiledItems.length;
+ for (i = 0; i < n; ++i) {
+ compiledItems[i](gfx);
+ }
+ };
},
display: function(gfx) {
+ assert(this.code instanceof Function, "page content must be compiled first");
var xref = this.xref;
- var content = xref.fetchIfRef(this.content);
var resources = xref.fetchIfRef(this.resources);
var mediaBox = xref.fetchIfRef(this.mediaBox);
- assertWellFormed(IsStream(content) && IsDict(resources),
- "invalid page content or resources");
+ assertWellFormed(IsDict(resources), "invalid page resources");
gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1],
width: mediaBox[2] - mediaBox[0],
height: mediaBox[3] - mediaBox[1] });
// Get the font charset if any
var charset = descriptor.get("CharSet");
- assertWellFormed(IsString(charset), "invalid charset");
- if (charset) {
++ if (charset)
+ assertWellFormed(IsString(charset), "invalid charset");
- charset = charset.split("/");
- }
+ charset = charset.split("/");
} else if (IsName(encoding)) {
var encoding = Encodings[encoding.name];
if (!encoding)
charset.push(encoding[j + firstChar]);
}
}
- var cmap = cmapObj.getBytes(cmapObj.length);
+ } else if (fontDict.has("ToUnicode")) {
+ var cmapObj = xref.fetchIfRef(fontDict.get("ToUnicode"));
+ if (IsName(cmapObj)) {
+ error("ToUnicode file cmap translation not implemented");
+ } else if (IsStream(cmapObj)) {
+ var encoding = Encodings["WinAnsiEncoding"];
+ var firstChar = xref.fetchIfRef(fontDict.get("FirstChar"));
+ for (var i = firstChar; i < encoding.length; i++)
+ encodingMap[i] = new Name(encoding[i]);
+
+ var tokens = [];
+ var token = "";
+
++ var buffer = cmapObj.ensureBuffer();
++ var cmap = cmapObj.getBytes(buffer.byteLength);
+ for (var i =0; i < cmap.length; i++) {
+ var byte = cmap[i];
+ if (byte == 0x20 || byte == 0x0A || byte == 0x3C || byte == 0x3E) {
+ switch (token) {
+ case "useCMap":
+ error("useCMap is not implemented");
+ break;
+
+ case "beginbfrange":
+ ignoreFont = false;
+ case "begincodespacerange":
+ token = "";
+ tokens = [];
+ break;
+
+ case "endcodespacerange":
+ TODO("Support CMap ranges");
+ break;
+
+ case "endbfrange":
+ for (var j = 0; j < tokens.length; j+=3) {
+ var startRange = parseInt("0x" + tokens[j]);
+ var endRange = parseInt("0x" + tokens[j+1]);
+ var code = parseInt("0x" + tokens[j+2]);
+
+ for (var k = startRange; k <= endRange; k++) {
+ encodingMap[k] = GlyphsUnicode[encoding[code]];
+ charset.push(encoding[code++]);
+ }
+ }
+ break;
+
+ case "beginfbchar":
+ case "endfbchar":
+ error("fbchar parsing is not implemented");
+ break;
+
+ default:
+ if (token.length) {
+ tokens.push(token);
+ token = "";
+ }
+ break;
+ }
+ } else if (byte == 0x5B || byte == 0x5D) {
+ error("CMAP list parsing is not implemented");
+ } else {
+ token += String.fromCharCode(byte);
+ }
+ }
+ }
}
var subType = fontDict.get("Subtype");
},
setFillColorN: function(/*...*/) {
// TODO real impl
- this.setFillColor.apply(this, arguments);
+ var colorSpace = this.current.colorSpace;
+ if (!colorSpace) {
+ var stateStack = this.stateStack;
+ var i = stateStack.length - 1;
+ while (!colorSpace && i >= 0) {
+ colorSpace = stateStack[i--].colorSpace;
+ }
+ }
+
+ if (this.current.colorSpace == "Pattern") {
+ var patternName = arguments[0];
+ if (IsName(patternName)) {
+ var xref = this.xref;
+ var patternRes = xref.fetchIfRef(this.res.get("Pattern"));
+ if (!patternRes)
+ error("Unable to find pattern resource");
+
+ var pattern = xref.fetchIfRef(patternRes.get(patternName.name));
-
++
+ const types = [null, this.tilingFill];
+ var typeNum = pattern.dict.get("PatternType");
+ var patternFn = types[typeNum];
+ if (!patternFn)
+ error("Unhandled pattern type");
+ patternFn.call(this, pattern);
+ }
+ } else {
+ // TODO real impl
+ this.setFillColor.apply(this, arguments);
+ }
+ },
+ tilingFill: function(pattern) {
+ function applyMatrix(point, m) {
+ var x = point[0] * m[0] + point[1] * m[2] + m[4];
+ var y = point[0] * m[1] + point[1] * m[3] + m[5];
+ return [x,y];
+ };
+
+ function multiply(m, tm) {
+ var a = m[0] * tm[0] + m[1] * tm[2];
+ var b = m[0] * tm[1] + m[1] * tm[3];
+ var c = m[2] * tm[0] + m[3] * tm[2];
+ var d = m[2] * tm[1] + m[3] * tm[3];
+ var e = m[4] * tm[0] + m[5] * tm[2] + tm[4];
+ var f = m[4] * tm[1] + m[5] * tm[3] + tm[5];
+ return [a, b, c, d, e, f]
+ };
+
+ this.save();
+ var dict = pattern.dict;
+ var ctx = this.ctx;
+
+ var paintType = dict.get("PaintType");
+ switch (paintType) {
+ case PAINT_TYPE_COLORED:
+ // should go to default for color space
+ ctx.fillStyle = this.makeCssRgb(1, 1, 1);
+ ctx.strokeStyle = this.makeCssRgb(0, 0, 0);
+ break;
+ case PAINT_TYPE_UNCOLORED:
+ default:
+ error("Unsupported paint type");
+ }
+
+ TODO("TilingType");
+
+ var matrix = dict.get("Matrix") || IDENTITY_MATRIX;
+
+ var bbox = dict.get("BBox");
+ var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
+
+ var xstep = dict.get("XStep");
+ var ystep = dict.get("YStep");
+
+ // top left corner should correspond to the top left of the bbox
+ var topLeft = applyMatrix([x0,y0], matrix);
+ // we want the canvas to be as large as the step size
+ var botRight = applyMatrix([x0 + xstep, y0 + ystep], matrix);
-
++
+ var tmpCanvas = document.createElement("canvas");
+ tmpCanvas.width = Math.ceil(botRight[0] - topLeft[0]);
+ tmpCanvas.height = Math.ceil(botRight[1] - topLeft[1]);
-
++
+ // set the new canvas element context as the graphics context
+ var tmpCtx = tmpCanvas.getContext("2d");
+ var savedCtx = ctx;
+ this.ctx = tmpCtx;
+
+ // normalize transform matrix so each step
+ // takes up the entire tmpCanvas (need to remove white borders)
+ if (matrix[1] === 0 && matrix[2] === 0) {
- matrix[0] = tmpCanvas.width / xstep;
++ matrix[0] = tmpCanvas.width / xstep;
+ matrix[3] = tmpCanvas.height / ystep;
+ topLeft = applyMatrix([x0,y0], matrix);
+ }
+
+ // move the top left corner of bounding box to [0,0]
+ matrix = multiply(matrix, [1, 0, 0, 1, -topLeft[0], -topLeft[1]]);
-
++
+ this.transform.apply(this, matrix);
-
++
+ if (bbox && IsArray(bbox) && 4 == bbox.length) {
+ this.rectangle.apply(this, bbox);
+ this.clip();
+ this.endPath();
+ }
+
+ var xref = this.xref;
+ var res = xref.fetchIfRef(dict.get("Resources"));
+ if (!pattern.code)
+ pattern.code = this.compile(pattern, xref, res, []);
+ this.execute(pattern.code, xref, res);
-
++
+ this.ctx = savedCtx;
+ this.restore();
+
+ TODO("Inverse pattern is painted");
+ var pattern = this.ctx.createPattern(tmpCanvas, "repeat");
+ this.ctx.fillStyle = pattern;
},
setStrokeGray: function(gray) {
this.setStrokeRGBColor(gray, gray, gray);
var fn = new PDFFunction(this.xref, fnObj);
var gradient = this.ctx.createLinearGradient(x0, y0, x1, y1);
-
++
+ // 10 samples seems good enough for now, but probably won't work
+ // if there are sharp color changes. Ideally, we would implement
+ // the spec faithfully and add lossless optimizations.
var step = (t1 - t0) / 10;
--
++
for (var i = t0; i <= t1; i += step) {
var c = fn.func([i]);
gradient.addColorStop(i, this.makeCssRgb.apply(this, c));
}
this.ctx.fillStyle = gradient;
--
++
// HACK to draw the gradient onto an infinite rectangle.
// PDF gradients are drawn across the entire image while
// Canvas only allows gradients to be drawn in a rectangle