diff --git a/pdf.js b/pdf.js index f7e553699..9ba91297b 100644 --- a/pdf.js +++ b/pdf.js @@ -65,6 +65,19 @@ var Stream = (function() { return -1; return bytes[this.pos++]; }, + // returns subarray of original buffer + // should only be read + getBytes: function(length) { + var bytes = this.bytes; + var pos = this.pos; + var end = pos + length; + var strEnd = this.end; + if (end > strEnd) + end = strEnd; + + this.pos = end; + return bytes.subarray(pos, end); + }, lookChar: function() { var bytes = this.bytes; if (this.pos >= this.end) @@ -78,15 +91,6 @@ var Stream = (function() { this.pos++; return ch; }, - snarf: function(dest) { - var bytes = this.bytes; - var pos = this.pos; - var end = this.end; - var n = 0; - while (pos < end) - dest[n++] = bytes[pos++]; - this.pos = this.end; - }, skip: function(n) { if (!n) n = 1; @@ -242,8 +246,8 @@ var FlateStream = (function() { this.eof = false; this.codeSize = 0; this.codeBuf = 0; + this.pos = 0; - this.bufferPos = 0; this.bufferLength = 0; } @@ -299,38 +303,48 @@ var FlateStream = (function() { buffer2[i] = buffer[i]; return this.buffer = buffer2; }, - lookChar: function() { + getByte: function() { var bufferLength = this.bufferLength; - var bufferPos = this.bufferPos; - if (bufferLength == bufferPos) { + var pos = this.pos; + if (bufferLength == pos) { if (this.eof) return; this.readBlock(); } - return String.fromCharCode(this.buffer[bufferPos]); + return this.buffer[this.pos++]; + }, + getBytes: function(length) { + var pos = this.pos; + + while (!this.eof && this.bufferLength < pos + length) + this.readBlock(); + + var end = pos + length; + var bufEnd = this.bufferLength; + + if (end > bufEnd) + end = bufEnd; + + this.pos = end; + return this.buffer.subarray(pos, end) + }, + lookChar: function() { + var bufferLength = this.bufferLength; + var pos = this.pos; + if (bufferLength == pos) { + if (this.eof) + return; + this.readBlock(); + } + return String.fromCharCode(this.buffer[pos]); }, getChar: function() { var ch = this.lookChar(); if (!ch) return; this.pos++; - this.bufferPos++; return ch; }, - snarf: function(dest) { - // copy the leftover data in the buffer into dest - var bufferLength = this.bufferLength; - var bufferPos = this.bufferPos; - var n = 0; - while (bufferPos < bufferLength) - dest[n++] = this.buffer[bufferPos++]; - // now use dest as our buffer and fill it - this.buffer = dest; - while (!this.eof) - this.readBlock(); - // update stream position - this.pos = n; - }, skip: function(n) { if (!n) n = 1; @@ -1032,7 +1046,7 @@ var Parser = (function() { length = xref.fetchIfRef(length); if (!IsInt(length)) { error("Bad 'Length' attribute in stream"); - lenght = 0; + length = 0; } // skip over the stream data @@ -1886,10 +1900,21 @@ var CanvasGraphics = (function() { return; xobj = this.xref.fetchIfRef(xobj); assertWellFormed(IsStream(xobj), "XObject should be a stream"); + + var oc = xobj.dict.get("OC"); + if (oc) { + TODO("oc for xobject"); + } + + var opi = xobj.dict.get("OPI"); + if (opi) { + TODO("opi for xobject"); + } + var type = xobj.dict.get("Subtype"); assertWellFormed(IsName(type), "XObject should have a Name subtype"); if ("Image" == type.name) { - this.paintImageXObject(xobj); + this.paintImageXObject(xobj, false); } else if ("Form" == type.name) { this.paintFormXObject(xobj); } else if ("PS" == type.name) { @@ -1919,9 +1944,15 @@ var CanvasGraphics = (function() { this.restore(); }, - paintImageXObject: function(image) { + paintImageXObject: function(image, inline) { this.save(); - + if (image.getParams) { + // JPX/JPEG2000 streams directly contain bits per component + // and color space mode information. + TODO("get params from actual stream"); + // var bits = ... + // var colorspace = ... + } // TODO cache rendered images? var dict = image.dict; @@ -1930,6 +1961,10 @@ var CanvasGraphics = (function() { if (w < 1 || h < 1) error("Invalid image width or height"); + + var ctx = this.ctx; + // scale the image to the unit square + ctx.scale(1/w, 1/h); var interpolate = dict.get2("Interpolate", "I"); if (!IsBool(interpolate)) @@ -1938,13 +1973,9 @@ var CanvasGraphics = (function() { if (!IsBool(imageMask)) imageMask = false; - // JPX/JPEG2000 streams directly contain bits per component - // and color space mode information. var bitsPerComponent = image.bitsPerComponent; - var csMode = image.csMode; - if (!bitsPerComponent) { - bitsPerComponent = dict.get("BitsPerComponent", "BPC"); + bitsPerComponent = dict.get2("BitsPerComponent", "BPC"); if (!bitsPerComponent) { if (imageMask) bitsPerComponent = 1; @@ -1953,57 +1984,140 @@ var CanvasGraphics = (function() { } } + if (bitsPerComponent !== 8) + error("Unsupported bpc"); + + var xref = this.xref; + var colorSpaces = this.colorSpaces; + + if (imageMask) { + error("support image masks"); + } + + // actual image + var csStream = dict.get2("ColorSpace", "CS"); + csStream = xref.fetchIfRef(csStream); + if (IsName(csStream) && inline) + csStream = colorSpaces.get(csStream); + + var colorSpace = new ColorSpace(xref, csStream); + + var decode = dict.get2("Decode", "D"); + + TODO("create color map"); + + var mask = image.dict.get("Mask"); + mask = xref.fetchIfRef(mask); + var smask = image.dict.get("SMask"); + smask = xref.fetchIfRef(smask); + + if (IsStream(smask)) { + if (inline) + error("cannot combine smask and inlining"); + + var maskDict = smask.dict; + var maskW = maskDict.get2("Width", "W"); + var maskH = maskDict.get2("Height", "H"); + if (!IsNum(maskW) || !IsNum(maskH) || maskW < 1 || maskH < 1) + error("Invalid image width or height"); + if (maskW !== w || maskH !== h) + error("Invalid image width or height"); + + var maskInterpolate = maskDict.get2("Interpolate", "I"); + if (!IsBool(maskInterpolate)) + maskInterpolate = false; + + var maskBPC = maskDict.get2("BitsPerComponent", "BPC"); + if (!maskBPC) + error("Invalid image mask bpc"); + + var maskCsStream = maskDict.get2("ColorSpace", "CS"); + maskCsStream = xref.fetchIfRef(maskCsStream); + var maskColorSpace = new ColorSpace(xref, maskCsStream); + if (maskColorSpace.mode !== "DeviceGray") + error("Invalid color space for smask"); + + var maskDecode = maskDict.get2("Decode", "D"); + if (maskDecode) + TODO("Handle mask decode"); + // handle matte object + } else { + smask = null; + } + var tmpCanvas = document.createElement("canvas"); tmpCanvas.width = w; tmpCanvas.height = h; var tmpCtx = tmpCanvas.getContext("2d"); var imgData = tmpCtx.getImageData(0, 0, w, h); var pixels = imgData.data; - - // Read in image data - image.snarf(pixels); - - var alpha = 25; - if (image.dict.has("SMask")) { - var smask = image.dict.get("SMask"); - smask = this.xref.fetchIfRef(smask); - // Specifies either a shape or opacity mask to be - // applied to the image samples - TODO("SMask"); - } - + + if (bitsPerComponent != 8) + error("unhandled number of bits per component"); + if (smask) { - var smaskDict = smask.dict; - if (!smaskDict) - error("No dictionary for smask"); - var smaskBitsPerComponent = smaskDict.get2("BitsPerComponent", "BPC"); - if (!smaskBitsPerComponent) - error("Bad BPC for smask"); - var max = (1 << bitsPerComponent) - 1; + if (maskColorSpace.numComps != 1) + error("Incorrect number of components in smask"); + + var numComps = colorSpace.numComps; + var imgArray = image.getBytes(numComps * w * h); + var imgIdx = 0; - var matte = smaskDict.get("Matte"); - if (matte) { - TODO(matte); + var smArray = smask.getBytes(w * h); + var smIdx = 0; + + var length = 4 * w * h; + switch (numComps) { + case 1: + for (var i = 0; i < length; i += 4) { + var p = imgArray[imageIdx++]; + pixels[i] = p; + pixels[i+1] = p; + pixels[i+2] = p; + pixels[i+3] = smArray[smIdx++]; + } + break; + case 3: + for (var i = 0; i < length; i += 4) { + pixels[i] = imgArray[imgIdx++]; + pixels[i+1] = imgArray[imgIdx++]; + pixels[i+2] = imgArray[imgIdx++]; + pixels[i+3] = smArray[smIdx++]; + } + break; + default: + error("unhandled amount of components per pixel: " + numComps); } - - // read in mask data - var mask = new Uint8Array(4 * w * h); - smask.snarf(mask); - - // and blend images with it - var stop = 4 * w * h; - for (var i = 0; i < stop; ++i) - pixels[i] = pixels[i] * mask[i] / max; } else { - // TODO blend if SMask is a mask image - var stop = 4 * w * h; - for (var i = 3; i < stop; i += 4) - pixels[i] = alpha; + var numComps = colorSpace.numComps; + var imgArray = image.getBytes(numComps * w * h); + var imgIdx = 0; + + var length = 4 * w * h; + switch (numComps) { + case 1: + for (var i = 0; i < length; i += 4) { + var p = imgArray[imageIdx++]; + pixels[i] = p; + pixels[i+1] = p; + pixels[i+2] = p; + pixels[i+3] = 255; + } + break; + case 3: + for (var i = 0; i < length; i += 4) { + pixels[i] = imgArray[imgIdx++]; + pixels[i+1] = imgArray[imgIdx++]; + pixels[i+2] = imgArray[imgIdx++]; + pixels[i+3] = 255; + } + break; + default: + error("unhandled amount of components per pixel: " + numComps); + } } tmpCtx.putImageData(imgData, 0, 0); - - this.ctx.drawImage(tmpCanvas, 0, 0); - + ctx.drawImage(tmpCanvas, 0, 0); this.restore(); }, @@ -2042,3 +2156,50 @@ var CanvasGraphics = (function() { return constructor; })(); + +var ColorSpace = (function() { + function constructor(xref, cs) { + if (IsName(cs)) { + var mode = cs.name; + this.mode = mode; + switch(mode) { + case "DeviceGray": + case "G": + this.numComps = 1; + break; + } + TODO("fill in color space constructor"); + } else if (IsArray(cs)) { + var mode = cs[0].name; + this.mode = mode; + + var stream = cs[1]; + stream = xref.fetchIfRef(stream); + + switch (mode) { + case "DeviceGray": + case "G": + this.stream = stream; + this.dict = stream.dict; + this.numComps = 1; + break; + case "ICCBased": + var dict = stream.dict; + + this.stream = stream; + this.dict = dict; + this.numComps = dict.get("N"); + break; + default: + error("unrecognized color space object"); + } + } else { + error("unrecognized color space object"); + } + }; + + constructor.prototype = { + }; + + return constructor; +})();