psd_sdk 0.2.1 copy "psd_sdk: ^0.2.1" to clipboard
psd_sdk: ^0.2.1 copied to clipboard

A Dart library that directly reads Photoshop PSD files. Supports limited export functionality.

example/psd_sdk_example.dart

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();
}
8
likes
40
points
162
downloads

Publisher

verified publishernexo.sh

Weekly Downloads

A Dart library that directly reads Photoshop PSD files. Supports limited export functionality.

Repository (GitHub)

License

BSD-2-Clause (license)

Dependencies

archive

More

Packages that depend on psd_sdk