Get rid of the PostScript interpreter (part 1)

This commit is contained in:
Vivien Nicolas 2011-06-15 23:02:30 +02:00
parent 2519e4f53b
commit 650ed04a70
2 changed files with 105 additions and 347 deletions

View File

@ -14,7 +14,7 @@ var kMaxGlyphsCount = 65526;
/** /**
* Maximum time to wait for a font to be loaded by @font-face * Maximum time to wait for a font to be loaded by @font-face
*/ */
var kMaxWaitForFontFace = 2000; var kMaxWaitForFontFace = 1000;
/* /*
* Useful for debugging when you want to certains operations depending on how * Useful for debugging when you want to certains operations depending on how
@ -59,8 +59,11 @@ var Fonts = {
* *
* As an improvment the last parameter can be replaced by an automatic guess * As an improvment the last parameter can be replaced by an automatic guess
* of the font type based on the first byte of the file. * of the font type based on the first byte of the file.
*
* XXX There is now too many parameters, this should be turned into an
* object containing all the required informations about the font
*/ */
var Font = function(aName, aFile, aEncoding, aCharset, aType) { var Font = function(aName, aFile, aEncoding, aCharset, aBBox, aType) {
this.name = aName; this.name = aName;
// If the font has already been decoded simply return // If the font has already been decoded simply return
@ -73,7 +76,7 @@ var Font = function(aName, aFile, aEncoding, aCharset, aType) {
var start = Date.now(); var start = Date.now();
switch (aType) { switch (aType) {
case "Type1": case "Type1":
var cff = new CFF(aFile); var cff = new CFF(aName, aBBox, aFile);
this.mimetype = "font/otf"; this.mimetype = "font/otf";
// Wrap the CFF data inside an OTF font file // Wrap the CFF data inside an OTF font file
@ -175,7 +178,7 @@ Font.prototype = {
if (debug) if (debug)
ctx.fillText(testString, 20, 50); ctx.fillText(testString, 20, 50);
}, 150, this); }, 20, this);
/** Hack end */ /** Hack end */
@ -402,7 +405,7 @@ Font.prototype = {
this._createTableEntry(otf, offsets, "OS/2", OS2); this._createTableEntry(otf, offsets, "OS/2", OS2);
//XXX Getting charstrings here seems wrong since this is another CFF glue //XXX Getting charstrings here seems wrong since this is another CFF glue
var charstrings = aFont.getOrderedCharStrings(aFont.font); var charstrings = aFont.getOrderedCharStrings(aFont.glyphs);
/** CMAP */ /** CMAP */
cmap = this._createCMAPTable(charstrings); cmap = this._createCMAPTable(charstrings);
@ -851,9 +854,7 @@ var Stack = function(aStackSize) {
}; };
}; };
var Type1Parser = function(aAsciiStream, aBinaryStream) { var Type1Parser = function() {
var lexer = aAsciiStream ? new Lexer(aAsciiStream) : null;
// Turn on this flag for additional debugging logs // Turn on this flag for additional debugging logs
var debug = false; var debug = false;
@ -862,30 +863,6 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
log(aData); log(aData);
}; };
// Hold the fontName as declared inside the /FontName postscript directive
// XXX This is a hack but at the moment I need it to map the name declared
// in the PDF and the name in the PS code.
var fontName = "";
/*
* Parse a whole Type1 font stream (from the first segment to the last)
* assuming the 'eexec' block is binary data and fill up the 'Fonts'
* dictionary with the font informations.
*/
var self = this;
this.parse = function() {
if (!debug) {
while (!processNextToken()) {};
return fontName;
} else {
// debug mode is used to debug postcript processing
setTimeout(function() {
if (!processNextToken())
self.parse();
}, 0);
}
};
/* /*
* Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence
* of Plaintext Bytes. The function took a key as a parameter which can be * of Plaintext Bytes. The function took a key as a parameter which can be
@ -894,7 +871,7 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
var kEexecEncryptionKey = 55665; var kEexecEncryptionKey = 55665;
var kCharStringsEncryptionKey = 4330; var kCharStringsEncryptionKey = 4330;
function decrypt(aStream, aKey, aDiscardNumber) { function decrypt(aStream, aKey, aDiscardNumber, aByteArray) {
var start = Date.now(); var start = Date.now();
var r = aKey, c1 = 52845, c2 = 22719; var r = aKey, c1 = 52845, c2 = 22719;
var decryptedString = []; var decryptedString = [];
@ -903,7 +880,10 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
var count = aStream.length; var count = aStream.length;
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
value = aStream.getByte(); value = aStream.getByte();
decryptedString[i] = String.fromCharCode(value ^ (r >> 8)); if (aByteArray)
decryptedString[i] = value ^ (r >> 8);
else
decryptedString[i] = String.fromCharCode(value ^ (r >> 8));
r = ((value + r) * c1 + c2) & ((1 << 16) - 1); r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
} }
var end = Date.now(); var end = Date.now();
@ -1017,7 +997,7 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
var end = Date.now(); var end = Date.now();
dump("Time to decode charString of length " + count + " is " + (end - start)); dump("Time to decode charString of length " + count + " is " + (end - start));
return charString; return charString;
} };
/* /*
* The operand stack holds arbitrary PostScript objects that are the operands * The operand stack holds arbitrary PostScript objects that are the operands
@ -1068,305 +1048,76 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
*/ */
function nextInStack() { function nextInStack() {
var currentProcedure = executionStack.peek(); var currentProcedure = executionStack.peek();
if (currentProcedure) { var command = currentProcedure.shift();
var command = currentProcedure.shift(); if (!currentProcedure.length)
if (!currentProcedure.length) executionStack.pop();
executionStack.pop(); return command;
return command;
}
return lexer.getObj();
}; };
/* /**
* Get the next token from the executionStack and process it. * Returns an object containing a Subrs array and a CharStrings array
* Actually the function does not process the third segment of a Type1 font * extracted from and eexec encrypted block of data
* and end on 'closefile'.
*
* The method thrown an error if it encounters an unknown token.
*/ */
function processNextToken() { this.extractFontInfo = function(aStream) {
var obj = nextInStack(); var eexecString = decrypt(new Stream(aStream), kEexecEncryptionKey, 4, true);
if (operandIsArray && !IsCmd(obj, "{") && !IsCmd(obj, "[") && var subrs = [], glyphs = [];
!IsCmd(obj, "]") && !IsCmd(obj, "}")) { var inSubrs = inGlyphs = false;
dump("Adding an object: " + obj +" to array " + operandIsArray); var glyph = "";
var currentArray = operandStack.peek();
for (var i = 1; i < operandIsArray; i++)
currentArray = currentArray[currentArray.length - 1];
currentArray.push(obj); var token = "";
} else if (IsBool(obj) || IsInt(obj) || IsNum(obj) || IsString(obj)) { var index = 0;
dump("Value: " + obj); var length = 0;
operandStack.push(obj);
} else if (IsName(obj)) {
dump("Name: " + obj.name);
operandStack.push(obj.name);
} else if (IsCmd(obj)) {
var command = obj.cmd;
dump(command);
switch (command) { var count = eexecString.length;
case "[": var c = "";
case "{": for (var i = 0; i < count; i++) {
dump("Start" + (command == "{" ? " Executable " : " ") + "Array"); var c = eexecString[i];
operandIsArray++;
var currentArray = operandStack;
for (var i = 1; i < operandIsArray; i++)
if (currentArray.peek)
currentArray = currentArray.peek();
else
currentArray = currentArray[currentArray.length - 1];
currentArray.push([]);
break;
case "]": if (inSubrs && c == 0x52) {
case "}": length = parseInt(length);
var currentArray = operandStack.peek(); var stream = new Stream(eexecString.slice(i + 3, i + 3 + length));
for (var i = 1; i < operandIsArray; i++) var encodedSubr = decrypt(stream, kCharStringsEncryptionKey, 4).join("");
currentArray = currentArray[currentArray.length - 1]; var subr = decodeCharString(new StringStream(encodedSubr));
dump("End" + (command == "}" ? " Executable " : " ") + "Array: " + currentArray.join(" "));
operandIsArray--;
break;
case "if": subrs.push(subr);
var procedure = operandStack.pop(); i += 3 + length;
var bool = operandStack.pop(); } else if (inGlyphs && c == 0x52) {
if (!IsBool(bool)) { length = parseInt(length);
dump("if: " + bool); var stream = new Stream(eexecString.slice(i + 3, i + 3 + length));
// we need to execute things, let be dirty var encodedCharstring = decrypt(stream, kCharStringsEncryptionKey, 4).join("");
executionStack.push(bool); var subr = decodeCharString(new StringStream(encodedCharstring));
} else {
dump("if ( " + bool + " ) { " + procedure + " }");
if (bool)
executionStack.push(procedure);
}
break;
case "ifelse": glyphs.push({
var procedure1 = operandStack.pop(); glyph: glyph,
var procedure2 = operandStack.pop(); data: subr
var bool = !!operandStack.pop(); });
dump("if ( " + bool + " ) { " + procedure2 + " } else { " + procedure1 + " }"); i += 3 + length;
executionStack.push(bool ? procedure2 : procedure1); } else if (inGlyphs && c == 0x2F) {
break; token = "";
glyph = "";
case "for": while ((c = eexecString[++i]) != 0x20 && i < count)
var procedure = operandStack.pop(); glyph += String.fromCharCode(c);
var limit = operandStack.pop(); } else if (c == 0x2F && eexecString[i+1] == 0x53 && !inGlyphs && !inSubrs) {
var increment = operandStack.pop(); while ((c = eexecString[++i]) != 0x20) {};
var initial = operandStack.pop(); inSubrs = true;
for (var i = 0; i < limit; i += increment) { } else if (c == 0x20) {
operandStack.push(i); index = length;
executionStack.push(procedure.slice()); length = token;
} token = "";
break; } else if (c == 0x2F && eexecString[i+1] == 0x43 && eexecString[i+2] == 0x68) {
while ((c = eexecString[++i]) != 0x20) {};
case "dup": inSubrs = false;
dump("duplicate: " + operandStack.peek()); inGlyphs = true;
operandStack.push(operandStack.peek()); } else {
break; token += String.fromCharCode(c);
case "mark":
operandStack.push("mark");
break;
case "cleartomark":
var command = "";
do {
command = operandStack.pop();
} while (command != "mark");
break;
case "put":
var data = operandStack.pop();
var indexOrKey = operandStack.pop();
var object = operandStack.pop();
dump("put " + data + " in " + object + "[" + indexOrKey + "]");
object.set ? object.set(indexOrKey, data)
: object[indexOrKey] = data;
break;
case "pop":
operandStack.pop();
break;
case "exch":
var operand1 = operandStack.pop();
var operand2 = operandStack.pop();
operandStack.push(operand1);
operandStack.push(operand2);
break;
case "get":
var indexOrKey = operandStack.pop();
var object = operandStack.pop();
var data = object.get ? object.get(indexOrKey) : object[indexOrKey];
dump("get " + object + "[" + indexOrKey + "]: " + data);
operandStack.push(data);
break;
case "currentdict":
var dict = dictionaryStack.peek();
operandStack.push(dict);
break;
case "systemdict":
operandStack.push(systemDict);
break;
case "readonly":
case "executeonly":
case "noaccess":
// Do nothing for the moment
break;
case "currentfile":
operandStack.push("currentfile");
break;
case "array":
var size = operandStack.pop();
var array = new Array(size);
operandStack.push(array);
break;
case "dict":
var size = operandStack.pop();
var dict = new Dict(size);
operandStack.push(dict);
break;
case "begin":
dictionaryStack.push(operandStack.pop());
break;
case "end":
dictionaryStack.pop();
break;
case "def":
var value = operandStack.pop();
var key = operandStack.pop();
dump("def: " + key + " = " + value);
dictionaryStack.peek().set(key, value);
break;
case "definefont":
var font = operandStack.pop();
var key = operandStack.pop();
dump("definefont " + font + " with key: " + key);
// The key will be the identifier to recognize this font
fontName = key;
PSFonts.set(key, font);
operandStack.push(font);
break;
case "known":
var name = operandStack.pop();
var dict = operandStack.pop();
var data = !!dict.get(name);
dump("known: " + data + " :: " + name + " in dict: " + dict);
operandStack.push(data);
break;
case "exec":
executionStack.push(operandStack.pop());
break;
case "eexec":
// All the first segment data has been read, decrypt the second segment
// and start interpreting it in order to decode it
var file = operandStack.pop();
var eexecString = decrypt(aBinaryStream, kEexecEncryptionKey, 4).join("");
lexer = new Lexer(new StringStream(eexecString));
break;
case "LenIV":
error("LenIV: argh! we need to modify the length of discard characters for charStrings");
break;
case "closefile":
var file = operandStack.pop();
return true;
break;
case "index":
var operands = [];
var size = operandStack.pop();
for (var i = 0; i < size; i++)
operands.push(operandStack.pop());
var newOperand = operandStack.peek();
while (operands.length)
operandStack.push(operands.pop());
operandStack.push(newOperand);
break;
case "string":
var size = operandStack.pop();
var str = (new Array(size + 1)).join(" ");
operandStack.push(str);
break;
case "readstring":
var str = operandStack.pop();
var size = str.length;
var file = operandStack.pop();
// Add '1' because of the space separator, this is dirty
var stream = lexer.stream.makeSubStream(lexer.stream.start + lexer.stream.pos + 1, size);
lexer.stream.skip(size + 1);
var charString = decrypt(stream, kCharStringsEncryptionKey, 4).join("");
var charStream = new StringStream(charString);
var decodedCharString = decodeCharString(charStream);
operandStack.push(decodedCharString);
// boolean indicating if the operation is a success or not
operandStack.push(true);
break;
case "StandardEncoding":
// For some reason the value is considered as a command, maybe it is
// because of the uppercase 'S'
operandStack.push(obj.cmd);
break;
default:
var command = null;
if (IsCmd(obj)) {
for (var i = 0; i < dictionaryStack.count(); i++) {
if (command = dictionaryStack.get(i).get(obj.cmd)) {
dump("found in dictionnary for " + obj.cmd + " command: " + command);
executionStack.push(command.slice());
break;
}
}
}
if (!command) {
log("operandStack: " + operandStack);
log("dictionaryStack: " + dictionaryStack);
log(obj);
error("Unknow command while parsing font");
}
break;
} }
} else if (obj) {
dump("unknow: " + obj);
operandStack.push(obj);
} else { // The End!
operandStack.dump();
return true;
} }
return {
return false; subrs: subrs,
} charstrings: glyphs
}
};
/* /*
* Flatten the commands by interpreting the postscript code and replacing * Flatten the commands by interpreting the postscript code and replacing
@ -1462,19 +1213,25 @@ var Type1Parser = function(aAsciiStream, aBinaryStream) {
} }
}; };
var CFF = function(aFontFile) { var CFF = function(aFontName, aFontBBox, aFontFile) {
var start = Date.now(); var start = Date.now();
// Get the data block containing glyphs and subrs informations
var length1 = aFontFile.dict.get("Length1"); var length1 = aFontFile.dict.get("Length1");
var length2 = aFontFile.dict.get("Length2"); var length2 = aFontFile.dict.get("Length2");
aFontFile.skip(length1);
var eexecBlock = aFontFile.getBytes(length2);
var ASCIIStream = new Stream(aFontFile.getBytes(length1)); // Extract informations from it
var binaryStream = new Stream(aFontFile.getBytes(length2)); var parser = new Type1Parser();
var fontInfo = parser.extractFontInfo(eexecBlock);
fontInfo.name = aFontName;
fontInfo.bbox = aFontBBox;
this.parser = new Type1Parser(ASCIIStream, binaryStream); // XXX
var fontName = this.parser.parse(); this.glyphs = fontInfo.charstrings;
this.font = PSFonts.get(fontName);
this.data = this.convertToCFF(this.font); this.data = this.convertToCFF(fontInfo);
var end = Date.now(); var end = Date.now();
//log("Time to parse font is:" + (end - start)); //log("Time to parse font is:" + (end - start));
}; };
@ -1537,11 +1294,11 @@ CFF.prototype = {
} }
}, },
getOrderedCharStrings: function(aFont) { getOrderedCharStrings: function(aGlyphs) {
var charstrings = []; var charstrings = [];
var glyphs = aFont.get("CharStrings") for (var i = 0; i < aGlyphs.length; i++) {
glyphs.forEach(function(glyph, glyphData) { var glyph = aGlyphs[i].glyph;
var unicode = GlyphsUnicode[glyph]; var unicode = GlyphsUnicode[glyph];
if (!unicode) { if (!unicode) {
if (glyph != ".notdef") if (glyph != ".notdef")
@ -1554,10 +1311,10 @@ CFF.prototype = {
charstrings.push({ charstrings.push({
glyph: glyph, glyph: glyph,
unicode: unicode, unicode: unicode,
charstring: glyphData.slice() charstring: aGlyphs[i].data.slice()
}); });
} }
}); };
charstrings.sort(function(a, b) { charstrings.sort(function(a, b) {
return a.unicode > b.unicode; return a.unicode > b.unicode;
@ -1565,20 +1322,20 @@ CFF.prototype = {
return charstrings; return charstrings;
}, },
convertToCFF: function(aFont) { convertToCFF: function(aFontInfo) {
var debug = false; var debug = false;
function dump(aMsg) { function dump(aMsg) {
if (debug) if (debug)
log(aMsg); log(aMsg);
}; };
var charstrings = this.getOrderedCharStrings(aFont); var charstrings = this.getOrderedCharStrings(aFontInfo.charstrings);
var charstringsCount = 0; var charstringsCount = 0;
var charstringsDataLength = 0; var charstringsDataLength = 0;
var glyphs = []; var glyphs = [];
var glyphsChecker = {}; var glyphsChecker = {};
var subrs = aFont.get("Private").get("Subrs"); var subrs = aFontInfo.subrs;
var parser = new Type1Parser(); var parser = new Type1Parser();
for (var i = 0; i < charstrings.length; i++) { for (var i = 0; i < charstrings.length; i++) {
var charstring = charstrings[i].charstring.slice(); var charstring = charstrings[i].charstring.slice();
@ -1604,19 +1361,18 @@ CFF.prototype = {
cff.set(header); cff.set(header);
// Names Index // Names Index
var nameIndex = this.createCFFIndexHeader([aFont.get("FontName")]); var nameIndex = this.createCFFIndexHeader([aFontInfo.name]);
cff.set(nameIndex, currentOffset); cff.set(nameIndex, currentOffset);
currentOffset += nameIndex.length; currentOffset += nameIndex.length;
// Calculate strings before writing the TopDICT index in order // Calculate strings before writing the TopDICT index in order
// to calculate correct relative offsets for storing 'charset' // to calculate correct relative offsets for storing 'charset'
// and 'charstrings' data // and 'charstrings' data
var fontInfo = aFont.get("FontInfo"); var version = "";
var version = fontInfo.get("version"); var notice = "";
var notice = fontInfo.get("Notice"); var fullName = "";
var fullName = fontInfo.get("FullName"); var familyName = "";
var familyName = fontInfo.get("FamilyName"); var weight = "";
var weight = fontInfo.get("Weight");
var strings = [version, notice, fullName, var strings = [version, notice, fullName,
familyName, weight]; familyName, weight];
var stringsIndex = this.createCFFIndexHeader(strings); var stringsIndex = this.createCFFIndexHeader(strings);
@ -1692,7 +1448,7 @@ CFF.prototype = {
248, 31, 4 // Weight 248, 31, 4 // Weight
]; ];
var fontBBox = aFont.get("FontBBox"); var fontBBox = aFontInfo.bbox;
for (var i = 0; i < fontBBox.length; i++) for (var i = 0; i < fontBBox.length; i++)
topDictIndex = topDictIndex.concat(this.encodeNumber(fontBBox[i])); topDictIndex = topDictIndex.concat(this.encodeNumber(fontBBox[i]));
topDictIndex.push(5) // FontBBox; topDictIndex.push(5) // FontBBox;

View File

@ -119,8 +119,10 @@ function displayPage(num) {
} }
} }
var fontBBox = descriptor.get("FontBBox");
var subtype = fontDict.get("Subtype").name; var subtype = fontDict.get("Subtype").name;
new Font(fontName, fontFile, encodingMap, charset, subtype); new Font(fontName, fontFile, encodingMap, charset, fontBBox, subtype);
return fontsReady = false; return fontsReady = false;
} else if (font.loading) { } else if (font.loading) {
return fontsReady = false; return fontsReady = false;