| import 'dart:math' as 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 = new 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 = new 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 Math.max(a, b); |
| } |
| |
| static int _blendDarken(int a, int b) { |
| return Math.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).toInt().clamp(0, 255); |
| } |
| |
| 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); |
| } |
| |
| static int _blendLinearBurn(int a, int b) { |
| return (a + b - 255).clamp(0, 255); |
| } |
| |
| static int _blendScreen(int a, int b) { |
| return (255 - ((255 - b) * (255 - a))).clamp(0, 255); |
| } |
| |
| static int _blendColorDodge(int a, int b) { |
| if (b == 255) { |
| return 255; |
| } |
| return (((a / 255) / (1.0 - (b / 255.0))) * 255.0).toInt().clamp(0, 255); |
| } |
| |
| 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] = new 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 = new 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 = new 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 = new 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(channel0.data, si, ns); |
| pixels[di++] = _ch(channel1.data, si, ns); |
| pixels[di++] = _ch(channel2.data, si, ns); |
| pixels[di++] = numChannels >= 4 ? |
| _ch(channel_1.data, si, ns) : 255; |
| |
| var r = pixels[xi]; |
| var g = pixels[xi + 1]; |
| var b = 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] = (((r + a) - 255) * 255) ~/ a; |
| pixels[xi + 1] = (((g + a) - 255) * 255) ~/ a; |
| pixels[xi + 2] = (((b + 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[0]; |
| pixels[di++] = rgb[1]; |
| pixels[di++] = rgb[2]; |
| 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[0]; |
| pixels[di++] = rgb[1]; |
| pixels[di++] = rgb[2]; |
| 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; |
| } |