You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
16 KiB
532 lines
16 KiB
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
|
|
|
'use strict'; |
|
|
|
var ColorSpace = (function ColorSpaceClosure() { |
|
// Constructor should define this.numComps, this.defaultColor, this.name |
|
function ColorSpace() { |
|
error('should not call ColorSpace constructor'); |
|
} |
|
|
|
ColorSpace.prototype = { |
|
// Input: array of size numComps representing color component values |
|
// Output: array of rgb values, each value ranging from [0.1] |
|
getRgb: function colorSpaceGetRgb(color) { |
|
error('Should not call ColorSpace.getRgb: ' + color); |
|
}, |
|
// Input: Uint8Array of component values, each value scaled to [0,255] |
|
// Output: Uint8Array of rgb values, each value scaled to [0,255] |
|
getRgbBuffer: function colorSpaceGetRgbBuffer(input) { |
|
error('Should not call ColorSpace.getRgbBuffer: ' + input); |
|
} |
|
}; |
|
|
|
ColorSpace.parse = function colorSpaceParse(cs, xref, res) { |
|
var IR = ColorSpace.parseToIR(cs, xref, res); |
|
if (IR instanceof AlternateCS) |
|
return IR; |
|
|
|
return ColorSpace.fromIR(IR); |
|
}; |
|
|
|
ColorSpace.fromIR = function colorSpaceFromIR(IR) { |
|
var name = isArray(IR) ? IR[0] : IR; |
|
|
|
switch (name) { |
|
case 'DeviceGrayCS': |
|
return new DeviceGrayCS(); |
|
case 'DeviceRgbCS': |
|
return new DeviceRgbCS(); |
|
case 'DeviceCmykCS': |
|
return new DeviceCmykCS(); |
|
case 'PatternCS': |
|
var basePatternCS = IR[1]; |
|
if (basePatternCS) |
|
basePatternCS = ColorSpace.fromIR(basePatternCS); |
|
return new PatternCS(basePatternCS); |
|
case 'IndexedCS': |
|
var baseIndexedCS = IR[1]; |
|
var hiVal = IR[2]; |
|
var lookup = IR[3]; |
|
return new IndexedCS(ColorSpace.fromIR(baseIndexedCS), hiVal, lookup); |
|
case 'AlternateCS': |
|
var numComps = IR[1]; |
|
var alt = IR[2]; |
|
var tintFnIR = IR[3]; |
|
|
|
return new AlternateCS(numComps, ColorSpace.fromIR(alt), |
|
PDFFunction.fromIR(tintFnIR)); |
|
case 'LabCS': |
|
var whitePoint = IR[1].WhitePoint; |
|
var blackPoint = IR[1].BlackPoint; |
|
var range = IR[1].Range; |
|
return new LabCS(whitePoint, blackPoint, range); |
|
default: |
|
error('Unkown name ' + name); |
|
} |
|
return null; |
|
}; |
|
|
|
ColorSpace.parseToIR = function colorSpaceParseToIR(cs, xref, res) { |
|
if (isName(cs)) { |
|
var colorSpaces = xref.fetchIfRef(res.get('ColorSpace')); |
|
if (isDict(colorSpaces)) { |
|
var refcs = colorSpaces.get(cs.name); |
|
if (refcs) |
|
cs = refcs; |
|
} |
|
} |
|
|
|
cs = xref.fetchIfRef(cs); |
|
var mode; |
|
|
|
if (isName(cs)) { |
|
mode = cs.name; |
|
this.mode = mode; |
|
|
|
switch (mode) { |
|
case 'DeviceGray': |
|
case 'G': |
|
return 'DeviceGrayCS'; |
|
case 'DeviceRGB': |
|
case 'RGB': |
|
return 'DeviceRgbCS'; |
|
case 'DeviceCMYK': |
|
case 'CMYK': |
|
return 'DeviceCmykCS'; |
|
case 'Pattern': |
|
return ['PatternCS', null]; |
|
default: |
|
error('unrecognized colorspace ' + mode); |
|
} |
|
} else if (isArray(cs)) { |
|
mode = cs[0].name; |
|
this.mode = mode; |
|
|
|
switch (mode) { |
|
case 'DeviceGray': |
|
case 'G': |
|
return 'DeviceGrayCS'; |
|
case 'DeviceRGB': |
|
case 'RGB': |
|
return 'DeviceRgbCS'; |
|
case 'DeviceCMYK': |
|
case 'CMYK': |
|
return 'DeviceCmykCS'; |
|
case 'CalGray': |
|
return 'DeviceGrayCS'; |
|
case 'CalRGB': |
|
return 'DeviceRgbCS'; |
|
case 'ICCBased': |
|
var stream = xref.fetchIfRef(cs[1]); |
|
var dict = stream.dict; |
|
var numComps = dict.get('N'); |
|
if (numComps == 1) |
|
return 'DeviceGrayCS'; |
|
if (numComps == 3) |
|
return 'DeviceRgbCS'; |
|
if (numComps == 4) |
|
return 'DeviceCmykCS'; |
|
break; |
|
case 'Pattern': |
|
var basePatternCS = cs[1]; |
|
if (basePatternCS) |
|
basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res); |
|
return ['PatternCS', basePatternCS]; |
|
case 'Indexed': |
|
case 'I': |
|
var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res); |
|
var hiVal = cs[2] + 1; |
|
var lookup = xref.fetchIfRef(cs[3]); |
|
return ['IndexedCS', baseIndexedCS, hiVal, lookup]; |
|
case 'Separation': |
|
case 'DeviceN': |
|
var name = cs[1]; |
|
var numComps = 1; |
|
if (isName(name)) |
|
numComps = 1; |
|
else if (isArray(name)) |
|
numComps = name.length; |
|
var alt = ColorSpace.parseToIR(cs[2], xref, res); |
|
var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3])); |
|
return ['AlternateCS', numComps, alt, tintFnIR]; |
|
case 'Lab': |
|
var params = cs[1].map; |
|
return ['LabCS', params]; |
|
default: |
|
error('unimplemented color space object "' + mode + '"'); |
|
} |
|
} else { |
|
error('unrecognized color space object: "' + cs + '"'); |
|
} |
|
return null; |
|
}; |
|
/** |
|
* Checks if a decode map matches the default decode map for a color space. |
|
* This handles the general decode maps where there are two values per |
|
* component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color. |
|
* This does not handle Lab, Indexed, or Pattern decode maps since they are |
|
* slightly different. |
|
* @param {Array} decode Decode map (usually from an image). |
|
* @param {Number} n Number of components the color space has. |
|
*/ |
|
ColorSpace.isDefaultDecode = function colorSpaceIsDefaultDecode(decode, n) { |
|
if (!decode) |
|
return true; |
|
|
|
if (n * 2 !== decode.length) { |
|
warning('The decode map is not the correct length'); |
|
return true; |
|
} |
|
for (var i = 0, ii = decode.length; i < ii; i += 2) { |
|
if (decode[i] != 0 || decode[i + 1] != 1) |
|
return false; |
|
} |
|
return true; |
|
}; |
|
|
|
return ColorSpace; |
|
})(); |
|
|
|
/** |
|
* Alternate color space handles both Separation and DeviceN color spaces. A |
|
* Separation color space is actually just a DeviceN with one color component. |
|
* Both color spaces use a tinting function to convert colors to a base color |
|
* space. |
|
*/ |
|
var AlternateCS = (function AlternateCSClosure() { |
|
function AlternateCS(numComps, base, tintFn) { |
|
this.name = 'Alternate'; |
|
this.numComps = numComps; |
|
this.defaultColor = []; |
|
for (var i = 0; i < numComps; ++i) |
|
this.defaultColor.push(1); |
|
this.base = base; |
|
this.tintFn = tintFn; |
|
} |
|
|
|
AlternateCS.prototype = { |
|
getRgb: function altcs_getRgb(color) { |
|
var tinted = this.tintFn(color); |
|
return this.base.getRgb(tinted); |
|
}, |
|
getRgbBuffer: function altcs_getRgbBuffer(input, bits) { |
|
var tintFn = this.tintFn; |
|
var base = this.base; |
|
var scale = 1 / ((1 << bits) - 1); |
|
var length = input.length; |
|
var pos = 0; |
|
var baseNumComps = base.numComps; |
|
var baseBuf = new Uint8Array(baseNumComps * length); |
|
var numComps = this.numComps; |
|
var scaled = []; |
|
|
|
for (var i = 0; i < length; i += numComps) { |
|
for (var z = 0; z < numComps; ++z) |
|
scaled[z] = input[i + z] * scale; |
|
|
|
var tinted = tintFn(scaled); |
|
for (var j = 0; j < baseNumComps; ++j) |
|
baseBuf[pos++] = 255 * tinted[j]; |
|
} |
|
return base.getRgbBuffer(baseBuf, 8); |
|
}, |
|
isDefaultDecode: function altcs_isDefaultDecode(decodeMap) { |
|
return ColorSpace.isDefaultDecode(decodeMap, this.numComps); |
|
} |
|
}; |
|
|
|
return AlternateCS; |
|
})(); |
|
|
|
var PatternCS = (function PatternCSClosure() { |
|
function PatternCS(baseCS) { |
|
this.name = 'Pattern'; |
|
this.base = baseCS; |
|
} |
|
PatternCS.prototype = {}; |
|
|
|
return PatternCS; |
|
})(); |
|
|
|
var IndexedCS = (function IndexedCSClosure() { |
|
function IndexedCS(base, highVal, lookup) { |
|
this.name = 'Indexed'; |
|
this.numComps = 1; |
|
this.defaultColor = [0]; |
|
this.base = base; |
|
this.highVal = highVal; |
|
|
|
var baseNumComps = base.numComps; |
|
var length = baseNumComps * highVal; |
|
var lookupArray = new Uint8Array(length); |
|
|
|
if (isStream(lookup)) { |
|
var bytes = lookup.getBytes(length); |
|
lookupArray.set(bytes); |
|
} else if (isString(lookup)) { |
|
for (var i = 0; i < length; ++i) |
|
lookupArray[i] = lookup.charCodeAt(i); |
|
} else { |
|
error('Unrecognized lookup table: ' + lookup); |
|
} |
|
this.lookup = lookupArray; |
|
} |
|
|
|
IndexedCS.prototype = { |
|
getRgb: function indexcs_getRgb(color) { |
|
var numComps = this.base.numComps; |
|
var start = color[0] * numComps; |
|
var c = []; |
|
|
|
for (var i = start, ii = start + numComps; i < ii; ++i) |
|
c.push(this.lookup[i]); |
|
|
|
return this.base.getRgb(c); |
|
}, |
|
getRgbBuffer: function indexcs_getRgbBuffer(input) { |
|
var base = this.base; |
|
var numComps = base.numComps; |
|
var lookup = this.lookup; |
|
var length = input.length; |
|
var baseBuf = new Uint8Array(length * numComps); |
|
var baseBufPos = 0; |
|
|
|
for (var i = 0; i < length; ++i) { |
|
var lookupPos = input[i] * numComps; |
|
for (var j = 0; j < numComps; ++j) { |
|
baseBuf[baseBufPos++] = lookup[lookupPos + j]; |
|
} |
|
} |
|
|
|
return base.getRgbBuffer(baseBuf, 8); |
|
}, |
|
isDefaultDecode: function indexcs_isDefaultDecode(decodeMap) { |
|
// indexed color maps shouldn't be changed |
|
return true; |
|
} |
|
}; |
|
return IndexedCS; |
|
})(); |
|
|
|
var DeviceGrayCS = (function DeviceGrayCSClosure() { |
|
function DeviceGrayCS() { |
|
this.name = 'DeviceGray'; |
|
this.numComps = 1; |
|
this.defaultColor = [0]; |
|
} |
|
|
|
DeviceGrayCS.prototype = { |
|
getRgb: function graycs_getRgb(color) { |
|
var c = color[0]; |
|
return [c, c, c]; |
|
}, |
|
getRgbBuffer: function graycs_getRgbBuffer(input, bits) { |
|
var scale = 255 / ((1 << bits) - 1); |
|
var length = input.length; |
|
var rgbBuf = new Uint8Array(length * 3); |
|
for (var i = 0, j = 0; i < length; ++i) { |
|
var c = (scale * input[i]) | 0; |
|
rgbBuf[j++] = c; |
|
rgbBuf[j++] = c; |
|
rgbBuf[j++] = c; |
|
} |
|
return rgbBuf; |
|
}, |
|
isDefaultDecode: function graycs_isDefaultDecode(decodeMap) { |
|
return ColorSpace.isDefaultDecode(decodeMap, this.numComps); |
|
} |
|
}; |
|
return DeviceGrayCS; |
|
})(); |
|
|
|
var DeviceRgbCS = (function DeviceRgbCSClosure() { |
|
function DeviceRgbCS() { |
|
this.name = 'DeviceRGB'; |
|
this.numComps = 3; |
|
this.defaultColor = [0, 0, 0]; |
|
} |
|
DeviceRgbCS.prototype = { |
|
getRgb: function rgbcs_getRgb(color) { |
|
return color; |
|
}, |
|
getRgbBuffer: function rgbcs_getRgbBuffer(input, bits) { |
|
if (bits == 8) |
|
return input; |
|
var scale = 255 / ((1 << bits) - 1); |
|
var i, length = input.length; |
|
var rgbBuf = new Uint8Array(length); |
|
for (i = 0; i < length; ++i) |
|
rgbBuf[i] = (scale * input[i]) | 0; |
|
return rgbBuf; |
|
}, |
|
isDefaultDecode: function rgbcs_isDefaultDecode(decodeMap) { |
|
return ColorSpace.isDefaultDecode(decodeMap, this.numComps); |
|
} |
|
}; |
|
return DeviceRgbCS; |
|
})(); |
|
|
|
var DeviceCmykCS = (function DeviceCmykCSClosure() { |
|
function DeviceCmykCS() { |
|
this.name = 'DeviceCMYK'; |
|
this.numComps = 4; |
|
this.defaultColor = [0, 0, 0, 1]; |
|
} |
|
DeviceCmykCS.prototype = { |
|
getRgb: function cmykcs_getRgb(color) { |
|
var c = color[0], m = color[1], y = color[2], k = color[3]; |
|
|
|
// CMYK -> CMY: http://www.easyrgb.com/index.php?X=MATH&H=14#text14 |
|
c = (c * (1 - k) + k); |
|
m = (m * (1 - k) + k); |
|
y = (y * (1 - k) + k); |
|
|
|
// CMY -> RGB: http://www.easyrgb.com/index.php?X=MATH&H=12#text12 |
|
var r = (1 - c); |
|
var g = (1 - m); |
|
var b = (1 - y); |
|
|
|
return [r, g, b]; |
|
}, |
|
getRgbBuffer: function cmykcs_getRgbBuffer(colorBuf, bits) { |
|
var scale = 1 / ((1 << bits) - 1); |
|
var length = colorBuf.length / 4; |
|
var rgbBuf = new Uint8Array(length * 3); |
|
var rgbBufPos = 0; |
|
var colorBufPos = 0; |
|
|
|
for (var i = 0; i < length; i++) { |
|
var cmyk = []; |
|
for (var j = 0; j < 4; ++j) |
|
cmyk.push(scale * colorBuf[colorBufPos++]); |
|
|
|
var rgb = this.getRgb(cmyk); |
|
for (var j = 0; j < 3; ++j) |
|
rgbBuf[rgbBufPos++] = Math.round(rgb[j] * 255); |
|
} |
|
|
|
return rgbBuf; |
|
}, |
|
isDefaultDecode: function cmykcs_isDefaultDecode(decodeMap) { |
|
return ColorSpace.isDefaultDecode(decodeMap, this.numComps); |
|
} |
|
}; |
|
|
|
return DeviceCmykCS; |
|
})(); |
|
|
|
// |
|
// LabCS: Based on "PDF Reference, Sixth Ed", p.250 |
|
// |
|
var LabCS = (function LabCSClosure() { |
|
function LabCS(whitePoint, blackPoint, range) { |
|
this.name = 'Lab'; |
|
this.numComps = 3; |
|
this.defaultColor = [0, 0, 0]; |
|
|
|
if (!whitePoint) |
|
error('WhitePoint missing - required for color space Lab'); |
|
blackPoint = blackPoint || [0, 0, 0]; |
|
range = range || [-100, 100, -100, 100]; |
|
|
|
// Translate args to spec variables |
|
this.XW = whitePoint[0]; |
|
this.YW = whitePoint[1]; |
|
this.ZW = whitePoint[2]; |
|
this.amin = range[0]; |
|
this.amax = range[1]; |
|
this.bmin = range[2]; |
|
this.bmax = range[3]; |
|
|
|
// These are here just for completeness - the spec doesn't offer any |
|
// formulas that use BlackPoint in Lab |
|
this.XB = blackPoint[0]; |
|
this.YB = blackPoint[1]; |
|
this.ZB = blackPoint[2]; |
|
|
|
// Validate vars as per spec |
|
if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) |
|
error('Invalid WhitePoint components, no fallback available'); |
|
|
|
if (this.XB < 0 || this.YB < 0 || this.ZB < 0) { |
|
warn('Invalid BlackPoint, falling back to default'); |
|
this.XB = this.YB = this.ZB = 0; |
|
} |
|
|
|
if (this.amin > this.amax || this.bmin > this.bmax) { |
|
warn('Invalid Range, falling back to defaults'); |
|
this.amin = -100; |
|
this.amax = 100; |
|
this.bmin = -100; |
|
this.bmax = 100; |
|
} |
|
}; |
|
|
|
// Function g(x) from spec |
|
function g(x) { |
|
if (x >= 6 / 29) |
|
return x * x * x; |
|
else |
|
return (108 / 841) * (x - 4 / 29); |
|
} |
|
|
|
LabCS.prototype = { |
|
getRgb: function labcs_getRgb(color) { |
|
// Ls,as,bs <---> L*,a*,b* in the spec |
|
var Ls = color[0], as = color[1], bs = color[2]; |
|
|
|
// Adjust limits of 'as' and 'bs' |
|
as = as > this.amax ? this.amax : as; |
|
as = as < this.amin ? this.amin : as; |
|
bs = bs > this.bmax ? this.bmax : bs; |
|
bs = bs < this.bmin ? this.bmin : bs; |
|
|
|
// Computes intermediate variables X,Y,Z as per spec |
|
var M = (Ls + 16) / 116; |
|
var L = M + (as / 500); |
|
var N = M - (bs / 200); |
|
var X = this.XW * g(L); |
|
var Y = this.YW * g(M); |
|
var Z = this.ZW * g(N); |
|
|
|
// XYZ to RGB 3x3 matrix, from: |
|
// http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC18 |
|
var XYZtoRGB = [3.240479, -1.537150, -0.498535, |
|
-0.969256, 1.875992, 0.041556, |
|
0.055648, -0.204043, 1.057311]; |
|
|
|
return Util.apply3dTransform(XYZtoRGB, [X, Y, Z]); |
|
}, |
|
getRgbBuffer: function labcs_getRgbBuffer(input, bits) { |
|
if (bits == 8) |
|
return input; |
|
var scale = 255 / ((1 << bits) - 1); |
|
var i, length = input.length / 3; |
|
var rgbBuf = new Uint8Array(length); |
|
|
|
var j = 0; |
|
for (i = 0; i < length; ++i) { |
|
// Convert L*, a*, s* into RGB |
|
var rgb = this.getRgb([input[i], input[i + 1], input[i + 2]]); |
|
rgbBuf[j++] = rgb[0]; |
|
rgbBuf[j++] = rgb[1]; |
|
rgbBuf[j++] = rgb[2]; |
|
} |
|
|
|
return rgbBuf; |
|
}, |
|
isDefaultDecode: function labcs_isDefaultDecode(decodeMap) { |
|
// From Table 90 in Adobe's: |
|
// "Document management - Portable document format", 1st ed, 2008 |
|
if (decodeMap[0] === 0 && decodeMap[1] === 100 && |
|
decodeMap[2] === this.amin && decodeMap[3] === this.amax && |
|
decodeMap[4] === this.bmin && decodeMap[5] === this.bmax) |
|
return true; |
|
else |
|
return false; |
|
} |
|
}; |
|
return LabCS; |
|
})();
|
|
|