blob: f8e3f8deefb4460a2724396f7fcffe52860d5290 [file] [log] [blame]
import 'dart:math';
import 'dart:typed_data';
import '../decode_info.dart';
import '../../color.dart';
import '../../image.dart';
import '../../image_exception.dart';
import '../../util/input_buffer.dart';
import 'psd_channel.dart';
import 'psd_image_resource.dart';
import 'psd_layer.dart';
class PsdImage extends DecodeInfo {
static const int SIGNATURE = 0x38425053; // '8BPS'
static const int COLORMODE_BITMAP = 0;
static const int COLORMODE_GRAYSCALE = 1;
static const int COLORMODE_INDEXED = 2;
static const int COLORMODE_RGB = 3;
static const int COLORMODE_CMYK = 4;
static const int COLORMODE_MULTICHANNEL = 7;
static const int COLORMODE_DUOTONE = 8;
static const int COLORMODE_LAB = 9;
int signature;
int version;
int channels;
int depth;
int colorMode;
List<PsdLayer> layers;
List<PsdChannel> mergeImageChannels;
Image mergedImage;
Map<int, PsdImageResource> imageResources = {};
bool hasAlpha = false;
PsdImage(List<int> bytes) {
_input = InputBuffer(bytes, bigEndian: true);
_readHeader();
if (!isValid) {
return;
}
int len = _input.readUint32();
/*_colorData =*/ _input.readBytes(len);
len = _input.readUint32();
_imageResourceData = _input.readBytes(len);
len = _input.readUint32();
_layerAndMaskData = _input.readBytes(len);
_imageData = _input.readBytes(_input.length);
}
bool get isValid => signature == SIGNATURE;
/// The number of frames that can be decoded.
int get numFrames => 1;
/// Decode the raw psd structure without rendering the output image.
/// Use [renderImage] to render the output image.
bool decode() {
if (!isValid || _input == null) {
return false;
}
// Color Mode Data Block:
// Indexed and duotone images have palette data in colorData...
_readColorModeData();
// Image Resource Block:
// Image resources are used to store non-pixel data associated with images,
// such as pen tool paths.
_readImageResources();
_readLayerAndMaskData();
_readMergeImageData();
_input = null;
//_colorData = null;
_imageResourceData = null;
_layerAndMaskData = null;
_imageData = null;
return true;
}
Image decodeImage() {
if (!decode()) {
return null;
}
return renderImage();
}
Image renderImage() {
if (mergedImage != null) {
return mergedImage;
}
mergedImage = Image(width, height);
mergedImage.fill(0);
Uint8List pixels = mergedImage.getBytes();
for (int li = 0; li < layers.length; ++li) {
PsdLayer layer = layers[li];
if (!layer.isVisible()) {
continue;
}
double opacity = layer.opacity / 255.0;
int blendMode = layer.blendMode;
//int ns = depth == 16 ? 2 : 1;
Uint8List srcP = layer.layerImage.getBytes();
for (int y = 0, sy = layer.top, si = 0; y < layer.height; ++y, ++sy) {
int di = (layer.top + y) * width * 4 + layer.left * 4;
for (int x = 0, sx = layer.left; x < layer.width; ++x, ++sx) {
int br = srcP[si++];
int bg = srcP[si++];
int bb = srcP[si++];
int ba = srcP[si++];
if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
int ar = pixels[di];
int ag = pixels[di + 1];
int ab = pixels[di + 2];
int aa = pixels[di + 3];
_blend(
ar, ag, ab, aa, br, bg, bb, ba, blendMode, opacity, pixels, di);
}
di += 4;
}
}
}
return mergedImage;
}
void _blend(int ar, int ag, int ab, int aa, int br, int bg, int bb, int ba,
int blendMode, double opacity, Uint8List pixels, int di) {
int r = br;
int g = bg;
int b = bb;
int a = ba;
double da = (ba / 255.0) * opacity;
switch (blendMode) {
case PsdLayer.BLEND_PASSTHROUGH:
r = ar;
g = ag;
b = ab;
a = aa;
break;
case PsdLayer.BLEND_NORMAL:
break;
case PsdLayer.BLEND_DISSOLVE:
break;
case PsdLayer.BLEND_DARKEN:
r = _blendDarken(ar, br);
g = _blendDarken(ag, bg);
b = _blendDarken(ab, bb);
break;
case PsdLayer.BLEND_MULTIPLY:
r = _blendMultiply(ar, br);
g = _blendMultiply(ag, bg);
b = _blendMultiply(ab, bb);
break;
case PsdLayer.BLEND_COLOR_BURN:
r = _blendColorBurn(ar, br);
g = _blendColorBurn(ag, bg);
b = _blendColorBurn(ab, bb);
break;
case PsdLayer.BLEND_LINEAR_BURN:
r = _blendLinearBurn(ar, br);
g = _blendLinearBurn(ag, bg);
b = _blendLinearBurn(ab, bb);
break;
case PsdLayer.BLEND_DARKEN_COLOR:
break;
case PsdLayer.BLEND_LIGHTEN:
r = _blendLighten(ar, br);
g = _blendLighten(ag, bg);
b = _blendLighten(ab, bb);
break;
case PsdLayer.BLEND_SCREEN:
r = _blendScreen(ar, br);
g = _blendScreen(ag, bg);
b = _blendScreen(ab, bb);
break;
case PsdLayer.BLEND_COLOR_DODGE:
r = _blendColorDodge(ar, br);
g = _blendColorDodge(ag, bg);
b = _blendColorDodge(ab, bb);
break;
case PsdLayer.BLEND_LINEAR_DODGE:
r = _blendLinearDodge(ar, br);
g = _blendLinearDodge(ag, bg);
b = _blendLinearDodge(ab, bb);
break;
case PsdLayer.BLEND_LIGHTER_COLOR:
break;
case PsdLayer.BLEND_OVERLAY:
r = _blendOverlay(ar, br, aa, ba);
g = _blendOverlay(ag, bg, aa, ba);
b = _blendOverlay(ab, bb, aa, ba);
break;
case PsdLayer.BLEND_SOFT_LIGHT:
r = _blendSoftLight(ar, br);
g = _blendSoftLight(ag, bg);
b = _blendSoftLight(ab, bb);
break;
case PsdLayer.BLEND_HARD_LIGHT:
r = _blendHardLight(ar, br);
g = _blendHardLight(ag, bg);
b = _blendHardLight(ab, bb);
break;
case PsdLayer.BLEND_VIVID_LIGHT:
r = _blendVividLight(ar, br);
g = _blendVividLight(ag, bg);
b = _blendVividLight(ab, bb);
break;
case PsdLayer.BLEND_LINEAR_LIGHT:
r = _blendLinearLight(ar, br);
g = _blendLinearLight(ag, bg);
b = _blendLinearLight(ab, bb);
break;
case PsdLayer.BLEND_PIN_LIGHT:
r = _blendPinLight(ar, br);
g = _blendPinLight(ag, bg);
b = _blendPinLight(ab, bb);
break;
case PsdLayer.BLEND_HARD_MIX:
r = _blendHardMix(ar, br);
g = _blendHardMix(ag, bg);
b = _blendHardMix(ab, bb);
break;
case PsdLayer.BLEND_DIFFERENCE:
r = _blendDifference(ar, br);
g = _blendDifference(ag, bg);
b = _blendDifference(ab, bb);
break;
case PsdLayer.BLEND_EXCLUSION:
r = _blendExclusion(ar, br);
g = _blendExclusion(ag, bg);
b = _blendExclusion(ab, bb);
break;
case PsdLayer.BLEND_SUBTRACT:
break;
case PsdLayer.BLEND_DIVIDE:
break;
case PsdLayer.BLEND_HUE:
break;
case PsdLayer.BLEND_SATURATION:
break;
case PsdLayer.BLEND_COLOR:
break;
case PsdLayer.BLEND_LUMINOSITY:
break;
}
r = ((ar * (1.0 - da)) + (r * da)).toInt();
g = ((ag * (1.0 - da)) + (g * da)).toInt();
b = ((ab * (1.0 - da)) + (b * da)).toInt();
a = ((aa * (1.0 - da)) + (a * da)).toInt();
pixels[di++] = r;
pixels[di++] = g;
pixels[di++] = b;
pixels[di++] = a;
}
static int _blendLighten(int a, int b) {
return max(a, b);
}
static int _blendDarken(int a, int b) {
return min(a, b);
}
static int _blendMultiply(int a, int b) {
return (a * b) >> 8;
}
static int _blendOverlay(int a, int b, int aAlpha, int bAlpha) {
double x = a / 255.0;
double y = b / 255.0;
double aa = aAlpha / 255.0;
double ba = bAlpha / 255.0;
double z;
if (2.0 * x < aa) {
z = 2.0 * y * x + y * (1.0 - aa) + x * (1.0 - ba);
} else {
z = ba * aa - 2.0 * (aa - x) * (ba - y) + y * (1.0 - aa) + x * (1.0 - ba);
}
return (z * 255.0).clamp(0, 255).toInt();
}
static int _blendColorBurn(int a, int b) {
if (b == 0) {
return 0; // We don't want to divide by zero
}
int c = (255.0 * (1.0 - (1.0 - (a / 255.0)) / (b / 255.0))).toInt();
return c.clamp(0, 255).toInt();
}
static int _blendLinearBurn(int a, int b) {
return (a + b - 255).clamp(0, 255).toInt();
}
static int _blendScreen(int a, int b) {
return (255 - ((255 - b) * (255 - a))).clamp(0, 255).toInt();
}
static int _blendColorDodge(int a, int b) {
if (b == 255) {
return 255;
}
return (((a / 255) / (1.0 - (b / 255.0))) * 255.0).clamp(0, 255).toInt();
}
static int _blendLinearDodge(int a, int b) {
return (b + a > 255) ? 0xff : a + b;
}
static int _blendSoftLight(int a, int b) {
double aa = a / 255.0;
double bb = b / 255.0;
return (255.0 *
((1.0 - bb) * bb * aa + bb * (1.0 - (1.0 - bb) * (1.0 - aa))))
.round();
}
static int _blendHardLight(int bottom, int top) {
double a = top / 255.0;
double b = bottom / 255.0;
if (b < 0.5) {
return (255.0 * 2.0 * a * b).round();
} else {
return (255.0 * (1.0 - 2.0 * (1.0 - a) * (1.0 - b))).round();
}
}
static int _blendVividLight(int bottom, int top) {
if (top < 128) {
return _blendColorBurn(bottom, 2 * top);
} else {
return _blendColorDodge(bottom, 2 * (top - 128));
}
}
static int _blendLinearLight(int bottom, int top) {
if (top < 128) {
return _blendLinearBurn(bottom, 2 * top);
} else {
return _blendLinearDodge(bottom, 2 * (top - 128));
}
}
static int _blendPinLight(int bottom, int top) {
return (top < 128)
? _blendDarken(bottom, 2 * top)
: _blendLighten(bottom, 2 * (top - 128));
}
static int _blendHardMix(int bottom, int top) {
return (top < 255 - bottom) ? 0 : 255;
}
static int _blendDifference(int bottom, int top) {
return (top - bottom).abs();
}
static int _blendExclusion(int bottom, int top) {
return (top + bottom - 2 * top * bottom / 255.0).round();
}
void _readHeader() {
signature = _input.readUint32();
version = _input.readUint16();
// version should be 1 (2 for PSB files).
if (version != 1) {
signature = 0;
return;
}
// padding should be all 0's
InputBuffer padding = _input.readBytes(6);
for (int i = 0; i < 6; ++i) {
if (padding[i] != 0) {
signature = 0;
return;
}
}
channels = _input.readUint16();
height = _input.readUint32();
width = _input.readUint32();
depth = _input.readUint16();
colorMode = _input.readUint16();
}
void _readColorModeData() {
// TODO support indexed and duotone images.
}
void _readImageResources() {
_imageResourceData.rewind();
while (!_imageResourceData.isEOS) {
int blockSignature = _imageResourceData.readUint32();
int blockId = _imageResourceData.readUint16();
int len = _imageResourceData.readByte();
String blockName = _imageResourceData.readString(len);
// name string is padded to an even size
if (len & 1 == 0) {
_imageResourceData.skip(1);
}
len = _imageResourceData.readUint32();
InputBuffer blockData = _imageResourceData.readBytes(len);
// blocks are padded to an even length.
if (len & 1 == 1) {
_imageResourceData.skip(1);
}
if (blockSignature == RESOURCE_BLOCK_SIGNATURE) {
imageResources[blockId] =
PsdImageResource(blockId, blockName, blockData);
}
}
}
void _readLayerAndMaskData() {
_layerAndMaskData.rewind();
int len = _layerAndMaskData.readUint32();
if ((len & 1) != 0) {
len++;
}
InputBuffer layerData = _layerAndMaskData.readBytes(len);
layers = [];
if (len > 0) {
int count = layerData.readInt16();
// If it is a negative number, its absolute value is the number of
// layers and the first alpha channel contains the transparency data for
// the merged result.
if (count < 0) {
hasAlpha = true;
count = -count;
}
for (int i = 0; i < count; ++i) {
PsdLayer layer = PsdLayer(layerData);
layers.add(layer);
}
}
for (int i = 0; i < layers.length; ++i) {
layers[i].readImageData(layerData, this);
}
// Global layer mask info
len = _layerAndMaskData.readUint32();
InputBuffer maskData = _layerAndMaskData.readBytes(len);
if (len > 0) {
/*int colorSpace =*/ maskData.readUint16();
/*int rc =*/ maskData.readUint16();
/*int gc =*/ maskData.readUint16();
/*int bc =*/ maskData.readUint16();
/*int ac =*/ maskData.readUint16();
/*int opacity =*/ maskData.readUint16(); // 0-100
/*int kind =*/ maskData.readByte();
}
}
void _readMergeImageData() {
_imageData.rewind();
int compression = _imageData.readUint16();
Uint16List lineLengths;
if (compression == PsdChannel.COMPRESS_RLE) {
int numLines = height * this.channels;
lineLengths = Uint16List(numLines);
for (int i = 0; i < numLines; ++i) {
lineLengths[i] = _imageData.readUint16();
}
}
mergeImageChannels = [];
for (int i = 0; i < channels; ++i) {
mergeImageChannels.add(new PsdChannel.read(_imageData, i == 3 ? -1 : i,
width, height, depth, compression, lineLengths, i));
}
mergedImage = createImageFromChannels(
colorMode, depth, width, height, mergeImageChannels);
}
static int _ch(List<int> data, int si, int ns) {
return ns == 1 ? data[si] : ((data[si] << 8) | data[si + 1]) >> 8;
}
static Image createImageFromChannels(int colorMode, int bitDepth, int width,
int height, List<PsdChannel> channelList) {
Image output = Image(width, height);
Uint8List pixels = output.getBytes();
Map<int, PsdChannel> channels = {};
for (PsdChannel ch in channelList) {
channels[ch.id] = ch;
}
int numChannels = channelList.length;
int ns = (bitDepth == 8) ? 1 : (bitDepth == 16) ? 2 : -1;
if (ns == -1) {
throw new ImageException('PSD: unsupported bit depth: $bitDepth');
}
final channel0 = channels[0];
final channel1 = channels[1];
final channel2 = channels[2];
final channel_1 = channels[-1];
for (int y = 0, di = 0, si = 0; y < height; ++y) {
for (int x = 0; x < width; ++x, si += ns) {
switch (colorMode) {
case COLORMODE_RGB:
int xi = di;
pixels[di++] = _ch(channel2.data, si, ns);
pixels[di++] = _ch(channel1.data, si, ns);
pixels[di++] = _ch(channel0.data, si, ns);
pixels[di++] = numChannels >= 4 ? _ch(channel_1.data, si, ns) : 255;
var b = pixels[xi];
var g = pixels[xi + 1];
var r = pixels[xi + 2];
var a = pixels[xi + 3];
if (a != 0) {
// Photoshop/Gimp blend the image against white (argh!),
// which is not what we want for compositing. Invert the blend
// operation to try and undo the damage.
pixels[xi] = (((b + a) - 255) * 255) ~/ a;
pixels[xi + 1] = (((g + a) - 255) * 255) ~/ a;
pixels[xi + 2] = (((r + a) - 255) * 255) ~/ a;
}
break;
case COLORMODE_LAB:
int L = _ch(channel0.data, si, ns) * 100 >> 8;
int a = _ch(channel1.data, si, ns) - 128;
int b = _ch(channel2.data, si, ns) - 128;
int alpha = numChannels >= 4 ? _ch(channel_1.data, si, ns) : 255;
List<int> rgb = labToRGB(L, a, b);
pixels[di++] = rgb[2];
pixels[di++] = rgb[1];
pixels[di++] = rgb[0];
pixels[di++] = alpha;
break;
case COLORMODE_GRAYSCALE:
int gray = _ch(channel0.data, si, ns);
int alpha = numChannels >= 2 ? _ch(channel_1.data, si, ns) : 255;
pixels[di++] = gray;
pixels[di++] = gray;
pixels[di++] = gray;
pixels[di++] = alpha;
break;
case COLORMODE_CMYK:
int c = _ch(channel0.data, si, ns);
int m = _ch(channel1.data, si, ns);
int y = _ch(channel2.data, si, ns);
int k = _ch(channels[numChannels == 4 ? -1 : 3].data, si, ns);
int alpha = numChannels >= 5 ? _ch(channel_1.data, si, ns) : 255;
List<int> rgb = cmykToRGB(255 - c, 255 - m, 255 - y, 255 - k);
pixels[di++] = rgb[2];
pixels[di++] = rgb[1];
pixels[di++] = rgb[0];
pixels[di++] = alpha;
break;
default:
throw new ImageException('Unhandled color mode: $colorMode');
}
}
}
return output;
}
static const int RESOURCE_BLOCK_SIGNATURE = 0x3842494d; // '8BIM'
InputBuffer _input;
//InputBuffer _colorData;
InputBuffer _imageResourceData;
InputBuffer _layerAndMaskData;
InputBuffer _imageData;
}