psd_sdk 0.2.1
psd_sdk: ^0.2.1 copied to clipboard
A Dart library that directly reads Photoshop PSD files. Supports limited export functionality.
import 'dart:typed_data';
import 'package:psd_sdk/psd_sdk.dart';
import 'tga_exporter.dart' as tga_exporter;
import 'dart:io' as io;
String getSampleInputPath() {
return 'example/';
}
String getSampleOutputPath() {
return 'example/sample_output/';
}
Uint8List? expandChannelToCanvas(
Document document, dynamic layer, Channel channel) {
var canvasData = Uint8List.fromList(List.filled(
(document.bitsPerChannel ?? 0) ~/
8 *
(document.width ?? 0) *
(document.height ?? 0),
0));
if (ImageUtil.copyLayerData(
channel.data!,
canvasData,
document.bitsPerChannel ?? 0,
layer.left,
layer.top,
layer.right,
layer.bottom,
document.width ?? 0,
document.height ?? 0,
)) {
return canvasData;
}
return null;
}
Uint8List? expandMaskToCanvas(Document document, Mask mask) {
var canvasData = Uint8List.fromList(List.filled(
(document.bitsPerChannel ?? 0) ~/
8 *
(document.width ?? 0) *
(document.height ?? 0),
0));
if (ImageUtil.copyLayerData(
mask.data!,
canvasData,
document.bitsPerChannel ?? 0,
mask.left ?? 0,
mask.top ?? 0,
mask.right ?? 0,
mask.bottom ?? 0,
document.width ?? 0,
document.height ?? 0)) {
return canvasData;
}
return null;
}
int sampleReadPsd() {
final srcPath = '${getSampleInputPath()}Sample.psd';
var file = File();
try {
file.setByteData(io.File(srcPath).readAsBytesSync());
} catch (e) {
print('Cannot open file.');
return 1;
}
final document = Document.fromFile(file);
// the sample only supports RGB colormode
if (document.colorMode != ColorMode.rgb) {
print('Document is not in RGB color mode.\n');
return 1;
}
// extract image resources section.
// this gives access to the ICC profile, EXIF data and XMP metadata.
{
var imageResourcesSection = document.parseImageResourcesSection(file);
print('XMP metadata:');
print(imageResourcesSection?.xmpMetadata);
print('\n');
}
var hasTransparencyMask = false;
final layerMaskSection = document.parseLayerMaskSection(file);
hasTransparencyMask = layerMaskSection?.hasTransparencyMask ?? false;
// extract all layers one by one. this should be done in parallel for
// maximum efficiency.
for (var i = 0; i < (layerMaskSection?.layerCount ?? 0); ++i) {
var layer = layerMaskSection?.layers![i];
layer?.extract(file);
// check availability of R, G, B, and A channels.
// we need to determine the indices of channels individually, because
// there is no guarantee that R is the first channel, G is the second, B
// is the third, and so on.
final indexR = layer?.findChannel(ChannelType.r)?.index;
final indexG = layer?.findChannel(ChannelType.g)?.index;
final indexB = layer?.findChannel(ChannelType.b)?.index;
final indexA = layer?.findChannel(ChannelType.transparencyMask)?.index;
// note that channel data is only as big as the layer it belongs to, e.g.
// it can be smaller or bigger than the canvas, depending on where it is
// positioned. therefore, we use the provided utility functions to
// expand/shrink the channel data to the canvas size. of course, you can
// work with the channel data directly if you need to.
var canvasData = List<Uint8List?>.filled(4, null);
var channelCount = 0;
if ((indexR != null) && (indexG != null) && (indexB != null)) {
// RGB channels were found.
canvasData[0] =
expandChannelToCanvas(document, layer, layer!.channels![indexR]!)!;
canvasData[1] =
expandChannelToCanvas(document, layer, layer.channels![indexG]!)!;
canvasData[2] =
expandChannelToCanvas(document, layer, layer.channels![indexB]!)!;
channelCount = 3;
if (indexA != null) {
// A channel was also found.
canvasData[3] =
expandChannelToCanvas(document, layer, layer.channels![indexA]!)!;
channelCount = 4;
}
}
// interleave the different pieces of planar canvas data into one RGB or
// RGBA image, depending on what channels we found, and what color mode
// the document is stored in.
// ignore: unused_local_variable
final image = channelCount == 3
? interleaveRGB(
canvasData[0]!,
canvasData[1]!,
canvasData[2]!,
document.bitsPerChannel ?? 0,
0,
document.width ?? 0,
document.height ?? 0)
: interleaveRGBA(
canvasData[0]!,
canvasData[1]!,
canvasData[2]!,
canvasData[3]!,
document.bitsPerChannel ?? 0,
document.width ?? 0,
document.height ?? 0);
final image8 = document.bitsPerChannel == 8 ? image : null;
// ignore: unused_local_variable
final image16 = document.bitsPerChannel == 16 ? image : null;
// ignore: unused_local_variable
final image32 = document.bitsPerChannel == 32 ? image : null;
// get the layer name.
// Unicode data is preferred because it is not truncated by Photoshop, but
// unfortunately it is optional. fall back to the ASCII name in case no
// Unicode name was found.
String layerName;
if (layer!.utf16Name != null) {
layerName =
String.fromCharCodes(layer.utf16Name!.where((x) => x != 0x00));
} else {
layerName = layer.name ?? '';
}
// at this point, image8, image16 or image32 store either a 8-bit, 16-bit,
// or 32-bit image, respectively. the image data is stored in interleaved
// RGB or RGBA, and has the size "document.width*document.height". it is
// up to you to do whatever you want with the image data. in the sample,
// we simply write the image to a .TGA file.
if (channelCount == 3) {
if (document.bitsPerChannel == 8) {
var filename = '${getSampleOutputPath()}' 'layer$layerName.tga';
tga_exporter.saveRGB(
filename, document.width ?? 0, document.height ?? 0, image8!);
}
} else if (channelCount == 4) {
if (document.bitsPerChannel == 8) {
var filename = '${getSampleOutputPath()}' 'layer$layerName.tga';
tga_exporter.saveRGBA(
filename, document.width ?? 0, document.height ?? 0, image8!);
}
}
// in addition to the layer data, we also want to extract the user and/or
// vector mask. luckily, this has been handled already by the
// ExtractLayer() function. we just need to check whether a mask exists.
if (layer.layerMask != null) {
// a layer mask exists, and data is available. work out the mask's
// dimensions.
final width = (layer.layerMask!.right! - layer.layerMask!.left!);
final height = (layer.layerMask!.bottom! - layer.layerMask!.top!);
// similar to layer data, the mask data can be smaller or bigger than
// the canvas. the mask data is always single-channel (monochrome), and
// has a width and height as calculated above.
var maskData = layer.layerMask!.data;
{
var filename =
'${getSampleOutputPath()}' 'layer$layerName' '_usermask.tga';
tga_exporter.saveMonochrome(filename, width, height, maskData!);
}
// use ExpandMaskToCanvas create an image that is the same size as the
// canvas.
var maskCanvasData = expandMaskToCanvas(document, layer.layerMask!);
{
var filename =
'${getSampleOutputPath()}canvas${layerName}_usermask.tga';
tga_exporter.saveMonochrome(filename, document.width ?? 0,
document.height ?? 0, maskCanvasData!);
}
}
if (layer.vectorMask != null) {
// accessing the vector mask works exactly like accessing the layer
// mask.
final width = (layer.vectorMask!.right! - layer.vectorMask!.left!);
final height = (layer.vectorMask!.bottom! - layer.vectorMask!.top!);
var maskData = layer.vectorMask!.data;
{
var filename =
'${getSampleOutputPath()}' 'layer$layerName' '_vectormask.tga';
tga_exporter.saveMonochrome(filename, width, height, maskData!);
}
var maskCanvasData = expandMaskToCanvas(document, layer.vectorMask!);
{
var filename =
'${getSampleOutputPath()}' 'canvas$layerName' '_vectormask.tga';
tga_exporter.saveMonochrome(filename, document.width ?? 0,
document.height ?? 0, maskCanvasData!);
}
}
}
// extract the image data section, if available. the image data section stores
// the final, merged image, as well as additional alpha channels. this is only
// available when saving the document with "Maximize Compatibility" turned on.
if (document.imageDataSection.length != 0) {
var imageData = document.parseImageDataSection(file);
if (imageData != null) {
// interleave the planar image data into one RGB or RGBA image.
// store the rest of the (alpha) channels and the transparency mask
// separately.
final imageCount = imageData.imageCount;
// note that an image can have more than 3 channels, but still no
// transparency mask in case all extra channels are actual alpha channels.
var isRgb = false;
if (imageCount == 3) {
// imageData.images[0], imageData.images[1] and imageData.images[2]
// contain the R, G, and B channels of the merged image. they are always
// the size of the canvas/document, so we can interleave them using
// imageUtil::InterleaveRGB directly.
isRgb = true;
} else if (imageCount >= 4) {
// check if we really have a transparency mask that belongs to the
// "main" merged image.
if (hasTransparencyMask) {
// we have 4 or more images/channels, and a transparency mask.
// this means that images 0-3 are RGBA, respectively.
isRgb = false;
} else {
// we have 4 or more images stored in the document, but none of them
// is the transparency mask. this means we are dealing with RGB (!)
// data, and several additional alpha channels.
isRgb = true;
}
}
final image = isRgb
? interleaveRGB(
imageData.images![0]!.data!,
imageData.images![1]!.data!,
imageData.images![2]!.data!,
0,
document.bitsPerChannel ?? 0,
document.width ?? 0,
document.height ?? 0)
: interleaveRGBA(
imageData.images![0]!.data!,
imageData.images![1]!.data!,
imageData.images![2]!.data!,
imageData.images![3]!.data!,
document.bitsPerChannel ?? 0,
document.width ?? 0,
document.height ?? 0);
final image8 = document.bitsPerChannel == 8 ? image : null;
// ignore: unused_local_variable
final image16 = document.bitsPerChannel == 16 ? image : null;
// ignore: unused_local_variable
final image32 = document.bitsPerChannel == 32 ? image : null;
if (document.bitsPerChannel == 8) {
var filename = '${getSampleOutputPath()}' 'merged.tga';
if (isRgb) {
tga_exporter.saveRGB(
filename, document.width ?? 0, document.height ?? 0, image8!);
} else {
tga_exporter.saveRGBA(
filename, document.width ?? 0, document.height ?? 0, image8!);
}
}
// extract image resources in order to acquire the alpha channel names.
var imageResources = document.parseImageResourcesSection(file);
// store all the extra alpha channels. in case we have a transparency
// mask, it will always be the first of the extra channels. alpha
// channel names can be accessed using
// imageResources.alphaChannels[index]. loop through all alpha
// channels, and skip all channels that were already merged (either RGB
// or RGBA).
final skipImageCount = isRgb ? 3 : 4;
for (var i = 0; i < imageCount - skipImageCount; ++i) {
var channel = imageResources!.alphaChannels![i];
if (document.bitsPerChannel == 8) {
var filename = '${getSampleOutputPath()}'
'.extra_channel_'
'${channel.asciiName}.tga';
tga_exporter.saveMonochrome(
filename,
document.width ?? 0,
document.height ?? 0,
imageData.images![i + skipImageCount]!.data!);
}
}
}
}
return 0;
}
final targetImageWidth = 256;
final targetImageHeight = 256;
final gMultiplyData = Uint8List(targetImageWidth * targetImageHeight);
final gXorData = Uint8List(targetImageWidth * targetImageHeight);
final gOrData = Uint8List(targetImageWidth * targetImageHeight);
final gAndData = Uint8List(targetImageWidth * targetImageHeight);
final gCheckerBoardData = Uint8List(targetImageWidth * targetImageHeight);
final gMultiplyData16 = Uint16List(targetImageHeight * targetImageWidth);
final gXorData16 = Uint16List(targetImageHeight * targetImageWidth);
final gOrData16 = Uint16List(targetImageHeight * targetImageWidth);
final gAndData16 = Uint16List(targetImageHeight * targetImageWidth);
final gCheckerBoardData16 = Uint16List(targetImageHeight * targetImageWidth);
final gMultiplyData32 = Float32List(targetImageWidth * targetImageHeight);
final gXorData32 = Float32List(targetImageWidth * targetImageHeight);
final gOrData32 = Float32List(targetImageWidth * targetImageHeight);
final gAndData32 = Float32List(targetImageWidth * targetImageHeight);
final gCheckerBoardData32 = Float32List(targetImageWidth * targetImageHeight);
void generateImageData() {
for (var y = 0; y < targetImageHeight; ++y) {
for (var x = 0; x < targetImageWidth; ++x) {
gMultiplyData[y * targetImageWidth + x] = (x * y >> 8) & 0xFF;
gXorData[y * targetImageWidth + x] = (x ^ y) & 0xFF;
gOrData[y * targetImageWidth + x] = (x | y) & 0xFF;
gAndData[y * targetImageWidth + x] = (x & y) & 0xFF;
gCheckerBoardData[y * targetImageWidth + x] =
(x ~/ 8 + y ~/ 8) & 1 != 0 ? 255 : 128;
gMultiplyData16[y * targetImageWidth + x] = (x * y) & 0xFFFF;
gXorData16[y * targetImageWidth + x] = ((x ^ y) * 256) & 0xFFFF;
gOrData16[y * targetImageWidth + x] = ((x | y) * 256) & 0xFFFF;
gAndData16[y * targetImageWidth + x] = ((x & y) * 256) & 0xFFFF;
gCheckerBoardData16[y * targetImageWidth + x] =
(x ~/ 8 + y ~/ 8) & 1 != 0 ? 65535 : 32768;
gMultiplyData32[y * targetImageWidth + x] = (1.0 / 65025.0) * (x * y);
gXorData32[y * targetImageWidth + x] = (1.0 / 65025.0) * ((x ^ y) * 256);
gOrData32[y * targetImageWidth + x] = (1.0 / 65025.0) * ((x | y) * 256);
gAndData32[y * targetImageWidth + x] = (1.0 / 65025.0) * ((x & y) * 256);
gCheckerBoardData32[y * targetImageWidth + x] =
(x ~/ 8 + y ~/ 8) & 1 != 0 ? 1.0 : 0.5;
}
}
}
int sampleWritePsd() {
generateImageData();
{
final dstPath = '${getSampleOutputPath()}SampleWrite_8.psd';
var file = File();
// write an RGB PSD file, 8-bit
var document = ExportDocument(
targetImageWidth, targetImageHeight, 8, ExportColorMode.rgb);
{
// metadata can be added as simple key-value pairs.
// when loading the document, they will be contained in XMP metadata such
// as e.g. <xmp:MyAttribute>MyValue</xmp:MyAttribute>
document.addMetaData('MyAttribute', 'MyValue');
// when adding a layer to the document, you first need to get a new index
// into the layer table. with a valid index, layers can be updated in
// parallel, in any order. this also allows you to only update the layer
// data that has changed, which is crucial when working with large data
// sets.
final layer1 = document.addLayer(document, 'MUL pattern');
final layer2 = document.addLayer(document, 'XOR pattern');
final layer3 =
document.addLayer(document, 'Mixed pattern with transparency');
// note that each layer has its own compression type. it is perfectly
// legal to compress different channels of different layers with different
// settings. RAW is pretty much just a raw data dump. fastest to write,
// but large. RLE stores run-length encoded data which can be good for
// 8-bit channels, but not so much for 16-bit or 32-bit data. ZIP is a
// good compromise between speed and size. ZIP_WITH_PREDICTION first delta
// encodes the data, and then zips it. slowest to write, but also smallest
// in size for most images.
document.updateLayer(layer1!, ExportChannel.red, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData, CompressionType.raw);
document.updateLayer(layer1, ExportChannel.green, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData, CompressionType.raw);
document.updateLayer(layer1, ExportChannel.blue, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData, CompressionType.raw);
document.updateLayer(layer2!, ExportChannel.red, 0, 0, targetImageWidth,
targetImageHeight, gXorData, CompressionType.raw);
document.updateLayer(layer2, ExportChannel.green, 0, 0, targetImageWidth,
targetImageHeight, gXorData, CompressionType.raw);
document.updateLayer(layer2, ExportChannel.blue, 0, 0, targetImageWidth,
targetImageHeight, gXorData, CompressionType.raw);
document.updateLayer(layer3!, ExportChannel.red, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData, CompressionType.raw);
document.updateLayer(layer3, ExportChannel.green, 0, 0, targetImageWidth,
targetImageHeight, gXorData, CompressionType.raw);
document.updateLayer(layer3, ExportChannel.blue, 0, 0, targetImageWidth,
targetImageHeight, gOrData, CompressionType.raw);
// note that transparency information is always supported, regardless of
// the export color mode. it is saved as true transparency, and not as
// separate alpha channel.
document.updateLayer(layer1, ExportChannel.alpha, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData, CompressionType.raw);
document.updateLayer(layer2, ExportChannel.alpha, 0, 0, targetImageWidth,
targetImageHeight, gXorData, CompressionType.raw);
document.updateLayer(layer3, ExportChannel.alpha, 0, 0, targetImageWidth,
targetImageHeight, gOrData, CompressionType.raw);
// merged image data is optional. if none is provided, black channels will
// be exported instead.
document.updateMergedImage(gMultiplyData, gXorData, gOrData);
// when adding a channel to the document, you first need to get a new
// index into the channel table. with a valid index, channels can be
// updated in parallel, in any order. add four spot colors (red, green,
// blue, and a mix) as additional channels.
{
final spotIndex = document.addAlphaChannel(
'Spot Red', 65535, 0, 0, 0, 100, AlphaChannelMode.spot);
document.updateChannel(spotIndex, gMultiplyData);
}
{
final spotIndex = document.addAlphaChannel(
'Spot Green', 0, 65535, 0, 0, 75, AlphaChannelMode.spot);
document.updateChannel(spotIndex, gXorData);
}
{
final spotIndex = document.addAlphaChannel(
'Spot Blue', 0, 0, 65535, 0, 50, AlphaChannelMode.spot);
document.updateChannel(spotIndex, gOrData);
}
{
final spotIndex = document.addAlphaChannel(
'Mix', 20000, 50000, 30000, 0, 100, AlphaChannelMode.spot);
document.updateChannel(spotIndex, gOrData);
}
document.write(file);
}
io.File(dstPath).writeAsBytesSync(file.bytes!);
}
{
final dstPath = '${getSampleOutputPath()}SampleWrite_16.psd';
var file = File();
// write a Grayscale PSD file, 16-bit.
// Grayscale works similar to RGB, only the types of export channels change.
final document = ExportDocument(
targetImageWidth, targetImageHeight, 16, ExportColorMode.grayscale);
{
final layer1 = document.addLayer(document, 'MUL pattern');
document.updateLayer(layer1!, ExportChannel.gray, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData16, CompressionType.raw);
final layer2 = document.addLayer(document, 'XOR pattern');
document.updateLayer(layer2!, ExportChannel.gray, 0, 0, targetImageWidth,
targetImageHeight, gXorData16, CompressionType.rle);
final layer3 = document.addLayer(document, 'AND pattern');
document.updateLayer(layer3!, ExportChannel.gray, 0, 0, targetImageWidth,
targetImageHeight, gAndData16, CompressionType.zip);
final layer4 =
document.addLayer(document, 'OR pattern with transparency');
document.updateLayer(layer4!, ExportChannel.gray, 0, 0, targetImageWidth,
targetImageHeight, gOrData16, CompressionType.zipWithPrediction);
document.updateLayer(
layer4,
ExportChannel.alpha,
0,
0,
targetImageWidth,
targetImageHeight,
gCheckerBoardData16,
CompressionType.zipWithPrediction);
document.updateMergedImage(gMultiplyData16, gXorData16, gAndData16);
document.write(file);
}
io.File(dstPath).writeAsBytesSync(file.bytes!);
}
{
final dstPath = '${getSampleOutputPath()}SampleWrite_32.psd';
var file = File();
// write an RGB PSD file, 32-bit
var document = ExportDocument(
targetImageWidth, targetImageHeight, 32, ExportColorMode.rgb);
{
final layer1 = document.addLayer(document, 'MUL pattern');
document.updateLayer(layer1!, ExportChannel.red, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData32, CompressionType.raw);
document.updateLayer(layer1, ExportChannel.green, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData32, CompressionType.rle);
document.updateLayer(layer1, ExportChannel.blue, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData32, CompressionType.zip);
final layer2 =
document.addLayer(document, 'Mixed pattern with transparency');
document.updateLayer(layer2!, ExportChannel.red, 0, 0, targetImageWidth,
targetImageHeight, gMultiplyData32, CompressionType.rle);
document.updateLayer(layer2, ExportChannel.green, 0, 0, targetImageWidth,
targetImageHeight, gXorData32, CompressionType.zip);
document.updateLayer(layer2, ExportChannel.blue, 0, 0, targetImageWidth,
targetImageHeight, gOrData32, CompressionType.zipWithPrediction);
document.updateLayer(layer2, ExportChannel.alpha, 0, 0, targetImageWidth,
targetImageHeight, gCheckerBoardData32, CompressionType.raw);
document.updateMergedImage(
gMultiplyData32, gXorData32, gCheckerBoardData32);
document.write(file);
}
io.File(dstPath).writeAsBytesSync(file.bytes!);
}
return 0;
}
void main() {
sampleReadPsd();
sampleWritePsd();
}