SMask emulation

This commit is contained in:
Yury Delendik 2014-01-23 11:13:32 -06:00
parent ac91047f6d
commit 4054b0c385
2 changed files with 183 additions and 15 deletions

View File

@ -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

View File

@ -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();
},