diff --git a/src/core/evaluator.js b/src/core/evaluator.js index bdeb1af80..d23add64c 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -96,7 +96,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var groupOptions = { matrix: matrix, bbox: bbox, - smask: !!smask, + smask: smask, isolated: false, knockout: false }; @@ -105,8 +105,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { if (isName(groupSubtype) && groupSubtype.name === 'Transparency') { groupOptions.isolated = group.get('I') || false; groupOptions.knockout = group.get('K') || false; - // There is also a group colorspace, but since we put everything in - // RGB I'm not sure we need it. + var colorSpace = group.get('CS'); + groupOptions.colorSpace = colorSpace ? + ColorSpace.parseToIR(colorSpace, this.xref, resources) : null; } operatorList.addOp(OPS.beginGroup, [groupOptions]); } @@ -196,6 +197,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList.addOp(OPS.paintImageXObject, args); }, + handleSMask: function PartialEvaluator_handleSmask(smask, resources, + operatorList) { + var smaskContent = smask.get('G'); + var smaskOptions = { + subtype: smask.get('S').name, + backdrop: smask.get('BC') + }; + + this.buildFormXObject(resources, smaskContent, smaskOptions, + operatorList); + }, + handleTilingType: function PartialEvaluator_handleTilingType( fn, args, resources, pattern, patternDict, operatorList) { @@ -265,7 +278,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, setGState: function PartialEvaluator_setGState(resources, gState, - operatorList) { + operatorList, xref) { var self = this; // TODO(mack): This should be rewritten so that this function returns @@ -295,9 +308,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { gStateObj.push([key, value]); break; case 'SMask': - // We support the default so don't trigger a warning bar. - if (!isName(value) || value.name != 'None') - UnsupportedManager.notify(UNSUPPORTED_FEATURES.smask); + if (isName(value) && value.name === 'None') { + gStateObj.push([key, false]); + break; + } + var dict = xref.fetchIfRef(value); + if (isDict(dict)) { + self.handleSMask(dict, resources, operatorList); + gStateObj.push([key, true]); + } else { + warn('Unsupported SMask type'); + } + break; // Only generate info log messages for the following since // they are unlikey to have a big impact on the rendering. @@ -579,7 +601,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { break; var gState = extGState.get(dictName.name); - self.setGState(resources, gState, operatorList); + self.setGState(resources, gState, operatorList, xref); args = []; continue; } // switch diff --git a/src/display/canvas.js b/src/display/canvas.js index e898dd535..a2dee78a6 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -376,6 +376,7 @@ var CanvasExtraState = (function CanvasExtraStateClosure() { this.fillAlpha = 1; this.strokeAlpha = 1; this.lineWidth = 1; + this.activeSMask = null; // nonclonable field (see the save method below) this.old = old; } @@ -416,6 +417,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.baseTransform = null; this.baseTransformStack = []; this.groupLevel = 0; + this.smaskStack = []; + this.smaskCounter = 0; + this.tempSMask = null; if (canvasCtx) { addContextCurrentTransform(canvasCtx); } @@ -522,6 +526,74 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } } + function composeSMask(ctx, smask, layerCtx) { + var mask = smask.canvas; + var maskCtx = smask.context; + var width = mask.width, height = mask.height; + + var removeBackdropFn; + if (smask.backdrop) { + var cs = smask.colorSpace || ColorSpace.singletons.rgb; + var backdrop = cs.getRgb(smask.backdrop, 0); + removeBackdropFn = function (r0, g0, b0, layerDataBytes) { + var length = layerDataBytes.length; + for (var i = 3; i < length; i += 4) { + var alpha = layerDataBytes[i]; + if (alpha !== 0 && alpha !== 255) { + var r = ((layerDataBytes[i - 3] * 255 - + r0 * (255 - alpha)) / alpha) | 0; + layerDataBytes[i - 3] = r < 0 ? 0 : r > 255 ? 255 : r; + var g = ((layerDataBytes[i - 2] * 255 - + g0 * (255 - alpha)) / alpha) | 0; + layerDataBytes[i - 2] = g < 0 ? 0 : g > 255 ? 255 : g; + var b = ((layerDataBytes[i - 1] * 255 - + b0 * (255 - alpha)) / alpha) | 0; + layerDataBytes[i - 1] = b < 0 ? 0 : b > 255 ? 255 : b; + } + } + }.bind(null, backdrop[0], backdrop[1], backdrop[2]); + } else { + removeBackdropFn = function () {}; + } + + var composeFn; + if (smask.subtype === 'Luminosity') { + composeFn = function (maskDataBytes, layerDataBytes) { + var length = maskDataBytes.length; + for (var i = 3; i < length; i += 4) { + var y = ((maskDataBytes[i - 3] * 77) + // * 0.3 / 255 * 0x10000 + (maskDataBytes[i - 2] * 152) + // * 0.59 .... + (maskDataBytes[i - 1] * 28)) | 0; // * 0.11 .... + layerDataBytes[i] = (layerDataBytes[i] * y) >> 16; + } + }; + } else { + composeFn = function (maskDataBytes, layerDataBytes) { + var length = maskDataBytes.length; + for (var i = 3; i < length; i += 4) { + var alpha = maskDataBytes[i]; + layerDataBytes[i] = (layerDataBytes[i] * alpha / 255) | 0; + } + }; + } + + // processing image in chunks to save memory + var chunkSize = 16; + for (var row = 0; row < height; row += chunkSize) { + var chunkHeight = Math.min(chunkSize, height - row); + var maskData = maskCtx.getImageData(0, row, width, chunkHeight); + var layerData = layerCtx.getImageData(0, row, width, chunkHeight); + + removeBackdropFn(layerData.data); + composeFn(maskData.data, layerData.data); + + maskCtx.putImageData(layerData, 0, row); + } + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(mask, smask.offsetX, smask.offsetY); + } + var LINE_CAP_STYLES = ['butt', 'round', 'square']; var LINE_JOIN_STYLES = ['miter', 'round', 'bevel']; var NORMAL_CLIP = {}; @@ -730,18 +802,70 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.ctx.globalCompositeOperation = 'source-over'; } break; + case 'SMask': + if (this.current.activeSMask) { + this.endSMaskGroup(); + } + this.current.activeSMask = value ? this.tempSMask : null; + if (this.current.activeSMask) { + this.beginSMaskGroup(); + } + this.tempSMask = null; + break; } } }, + beginSMaskGroup: function CanvasGraphics_beginSMaskGroup() { + + var activeSMask = this.current.activeSMask; + var drawnWidth = activeSMask.canvas.width; + var drawnHeight = activeSMask.canvas.height; + var cacheId = 'smaskGroupAt' + this.groupLevel; + var scratchCanvas = CachedCanvases.getCanvas( + cacheId, drawnWidth, drawnHeight, true); + + var currentCtx = this.ctx; + var currentTransform = currentCtx.mozCurrentTransform; + this.ctx.save(); + + var groupCtx = scratchCanvas.context; + groupCtx.translate(-activeSMask.offsetX, -activeSMask.offsetY); + groupCtx.transform.apply(groupCtx, currentTransform); + + copyCtxState(currentCtx, groupCtx); + this.ctx = groupCtx; + this.setGState([ + ['BM', 'Normal'], + ['ca', 1], + ['CA', 1] + ]); + this.groupStack.push(currentCtx); + this.groupLevel++; + }, + endSMaskGroup: function CanvasGraphics_endSMaskGroup() { + var groupCtx = this.ctx; + this.groupLevel--; + this.ctx = this.groupStack.pop(); + + composeSMask(this.ctx, this.current.activeSMask, groupCtx); + this.ctx.restore(); + }, save: function CanvasGraphics_save() { this.ctx.save(); var old = this.current; this.stateStack.push(old); this.current = old.clone(); + if (this.current.activeSMask) { + this.current.activeSMask = null; + } }, restore: function CanvasGraphics_restore() { var prev = this.stateStack.pop(); if (prev) { + if (this.current.activeSMask) { + this.endSMaskGroup(); + } + this.current = prev; this.ctx.restore(); } @@ -1571,9 +1695,15 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var drawnWidth = Math.max(Math.ceil(bounds[2] - bounds[0]), 1); var drawnHeight = Math.max(Math.ceil(bounds[3] - bounds[1]), 1); + var cacheId = 'groupAt' + this.groupLevel; + if (group.smask) { + // Using two cache entries is case if masks are used one after another. + cacheId += '_smask_' + ((this.smaskCounter++) % 2); + } var scratchCanvas = CachedCanvases.getCanvas( - 'groupAt' + this.groupLevel, drawnWidth, drawnHeight, true); + cacheId, drawnWidth, drawnHeight, true); var groupCtx = scratchCanvas.context; + // Since we created a new canvas that is just the size of the bounding box // we have to translate the group ctx. var offsetX = bounds[0]; @@ -1581,16 +1711,28 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { groupCtx.translate(-offsetX, -offsetY); groupCtx.transform.apply(groupCtx, currentTransform); - // Setup the current ctx so when the group is popped we draw it the right - // location. - currentCtx.setTransform(1, 0, 0, 1, 0, 0); - currentCtx.translate(offsetX, offsetY); + if (group.smask) { + // Saving state and cached mask to be used in setGState. + this.smaskStack.push({ + canvas: scratchCanvas.canvas, + context: groupCtx, + offsetX: offsetX, + offsetY: offsetY, + subtype: group.smask.subtype, + backdrop: group.smask.backdrop, + colorSpace: group.colorSpace && ColorSpace.fromIR(group.colorSpace) + }); + } else { + // Setup the current ctx so when the group is popped we draw it at the + // right location. + currentCtx.setTransform(1, 0, 0, 1, 0, 0); + currentCtx.translate(offsetX, offsetY); + } // The transparency group inherits all off the current graphics state // except the blend mode, soft mask, and alpha constants. copyCtxState(currentCtx, groupCtx); this.ctx = groupCtx; this.setGState([ - ['SMask', 'None'], ['BM', 'Normal'], ['ca', 1], ['CA', 1] @@ -1610,7 +1752,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else { this.ctx.mozImageSmoothingEnabled = false; } - this.ctx.drawImage(groupCtx.canvas, 0, 0); + if (group.smask) { + this.tempSMask = this.smaskStack.pop(); + } else { + this.ctx.drawImage(groupCtx.canvas, 0, 0); + } this.restore(); },