blob: 30f7e780431a61cbf96302d2843029117aecff78 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.pixelprobe.decoder.psd;
import com.android.tools.chunkio.Chunk;
import com.android.tools.chunkio.Chunked;
import com.android.tools.pixelprobe.ColorMode;
import com.android.tools.pixelprobe.util.Strings;
import java.util.List;
import java.util.Map;
/**
* This class describes the structure of a PSD file.
* Read on to learn how a PSD file is stored on disk.
*/
@SuppressWarnings("unused")
@Chunked
final class PsdFile {
@Chunk
Header header;
@Chunk
ColorData colorData;
@Chunk
ImageResources resources;
@Chunk
LayersInformation layersInfo;
@Chunk
ImageData imageData;
/**
* PSD header. A few magic values and important information
* about the image's dimensions, color depth and mode.
*/
@Chunked
static final class Header {
// Magic marker
@Chunk(byteCount = 4, match = "\"8BPS\"")
String signature;
// Version is always 1
@Chunk(match = "1")
short version;
// 6 reserved bytes that must always be set to 0
@Chunk(byteCount = 6)
Void reserved;
@Chunk(byteCount = 2)
int channels;
// Height comes before width here
@Chunk
int height;
@Chunk
int width;
@Chunk
short depth;
// We only support RGB documents
@Chunk(byteCount = 2)
ColorMode colorMode;
}
/**
* Only useful for indexed and duotone images.
*/
@Chunked
static final class ColorData {
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "colorData.length")
byte[] data;
}
/**
* The image resources section is a mix-bag of a lot of stuff
* (thumbnail, guides, printing data, EXIF, XMPP, etc.).
* This section is divided into typed blocks.
*/
@Chunked
static final class ImageResources {
@Chunk(byteCount = 4)
long length;
// Each block has a padded size to make it even
// Specifying how many bytes we want to read upfront
// ensures we'll be able to successfully read the rest
// of the document
@Chunk(dynamicByteCount = "imageResources.length", key = "imageResourceBlock.id")
Map<Integer, ImageResourceBlock> blocks;
}
/**
* An image resource block has a type (or ID) and an optional
* name. We'll use the ID to find the blocks we want.
*/
@Chunked
static final class ImageResourceBlock {
@Chunk(byteCount = 4, match = "\"8BIM\"")
String signature;
@Chunk(byteCount = 2)
int id;
// The name is stored as a Pascal string with even padding
// The padding takes into account the length byte
@Chunk(byteCount = 1)
short nameLength;
@Chunk(dynamicByteCount = "imageResourceBlock.nameLength")
String name;
@SuppressWarnings("unused")
@Chunk(dynamicByteCount = "Math.max(1, imageResourceBlock.nameLength & 1)")
Void padding;
// The actual byte length of the block
@Chunk(byteCount = 4)
long length;
// The length must be padded to make it even if we want to
// successfully read a block. We could also add a Void pad after.
@Chunk(dynamicByteCount = "imageResourceBlock.length + (imageResourceBlock.length & 1)",
switchType = {
@Chunk.Case(test = "imageResourceBlock.id == 0x0408", type = GuidesResourceBlock.class),
@Chunk.Case(test = "imageResourceBlock.id == 0x040C", type = ThumbnailResourceBlock.class),
@Chunk.Case(test = "imageResourceBlock.id == 0x03ED", type = ResolutionInfoBlock.class),
@Chunk.Case(test = "imageResourceBlock.id == 0x040F", type = ColorProfileBlock.class),
@Chunk.Case(test = "imageResourceBlock.id == 0x0416", type = UnsignedShortBlock.class),
@Chunk.Case(test = "imageResourceBlock.id == 0x0417", type = UnsignedShortBlock.class)
}
)
Object data;
}
/**
* The layers information section contains the list of layers
* and a lot of data for each layer.
*/
@Chunked
static final class LayersInformation {
@Chunk(byteCount = 4)
long length;
@Chunk(byteCount = 4)
long listLength;
@Chunk(dynamicByteCount = "layersInformation.listLength", readIf = "layersInformation.listLength > 0")
LayersList layers;
@Chunk(byteCount = 4)
long globalMaskInfoLength;
@Chunk(dynamicByteCount = "layersInformation.globalMaskInfoLength")
Void globalMaskInfo;
// Subtract 8 for listLength and globalMaskInfoLength
@Chunk(dynamicByteCount =
"layersInformation.length - layersInformation.listLength - layersInformation.globalMaskInfoLength - 8",
key = "layerProperty.key")
Map<String, LayerProperty> extras;
}
/**
* The list of layers is actually made of two lists in the PSD
* file. First, the description and extra data for each layer
* (name, bounds, etc.). Then an image representation for each
* layer, as a series of independently encoded channels.
*/
@Chunked
static final class LayersList {
// The count can be negative, which means the first
// alpha channel contains the transparency data for
// the flattened image. This means we must ensure
// we always take the absolute value of the layer
// count to build lists
@Chunk
short count;
@Chunk(dynamicSize = "Math.abs(layersList.count)")
List<RawLayer> layers;
@Chunk(dynamicSize = "Math.abs(layersList.count)")
List<ChannelsContainer> channels;
}
/**
* A layer contains a few static properties and a list of
* keyed "extras", The extras are crucial for non-image
* layers. They also contain layer effects (drop shadows,
* inner glow, etc.).
*/
@Chunked
static final class RawLayer {
// Mask for the flags field, indicating whether the layer is visible or not
static final int INVISIBLE = 0x2;
// Firs we have the layer's bounds, in pixels,
// in absolute image coordinates
@Chunk
int top;
@Chunk
int left;
@Chunk
int bottom;
@Chunk
int right;
// The channels count, 3 or 4 in our case since we
// only support RGB files
@Chunk(byteCount = 2)
short channels;
// Important stuff in there, read on
@Chunk(dynamicSize = "rawLayer.channels")
List<ChannelInformation> channelsInfo;
@Chunk(byteCount = 4, match = "\"8BIM\"")
String signature;
@Chunk(byteCount = 4)
String blendMode;
// The opacity is stored as an unsigned byte,
// from 0 (transparent) to 255 (opaque)
@Chunk(byteCount = 1)
short opacity;
@Chunk
byte clipping;
@Chunk
byte flags;
// Padding gunk
@Chunk(byteCount = 1)
Void filler;
// The number of bytes taken by all the extras
@Chunk(byteCount = 4)
long extraLength;
@Chunk(dynamicByteCount = "rawLayer.extraLength")
LayerExtras extras;
}
/**
* Extremely important: this section gives us the number of bytes
* used to encode the image data of each channel in a given layer.
*/
@Chunked
static final class ChannelInformation {
@Chunk
short id;
@Chunk(byteCount = 4)
long dataLength;
}
/**
* The layer's extras contains important values such as
* the layer's name and a series of named properties.
*/
@Chunked
static final class LayerExtras {
@Chunk
MaskAdjustment maskAdjustment;
@Chunk(byteCount = 4)
long blendRangesLength;
// The first blend range is always composite gray
@Chunk(dynamicByteCount = "layerExtras.blendRangesLength")
List<BlendRange> layerBlendRanges;
// The layer's name is stored as a Pascal string,
// padded to a multiple of 4 bytes
@Chunk(byteCount = 1)
short nameLength;
@Chunk(dynamicByteCount = "layerExtras.nameLength")
String name;
@Chunk(dynamicByteCount = "((layerExtras.nameLength + 4) & ~3) - (layerExtras.nameLength + 1)")
Void namePadding;
@Chunk(key = "layerProperty.key")
Map<String, LayerProperty> properties;
}
/**
* Specific to masks. This section has to be read carefully from the
* file has its length varies depending on a set of flags.
*/
@Chunked
static final class MaskAdjustment {
@Chunk(byteCount = 4, stopIf = "maskAdjustment.length == 0")
long length;
@Chunk(byteCount = 4)
long top;
@Chunk(byteCount = 4)
long left;
@Chunk(byteCount = 4)
long bottom;
@Chunk(byteCount = 4)
long right;
@Chunk(byteCount = 1)
short defaultColor;
@Chunk
byte flags;
@Chunk(readIf = "(maskAdjustment.flags & 0x10) != 0")
byte maskParameters;
@Chunk(byteCount = 1, readIf = "(maskAdjustment.maskParameters & 0x1) != 0")
short userMaskDensity;
@Chunk(readIf = "(maskAdjustment.maskParameters & 0x2) != 0")
double userMaskFeather;
@Chunk(byteCount = 1, readIf = "(maskAdjustment.maskParameters & 0x4) != 0")
short vectorMaskDensity;
@Chunk(readIf = "(maskAdjustment.maskParameters & 0x8) != 0")
double vectorMaskFeather;
@Chunk(readIf = "maskAdjustment.length == 20", stopIf = "maskAdjustment.length == 20")
short padding;
@Chunk
byte realFlags;
@Chunk(byteCount = 1)
short userMaskBackground;
@Chunk(byteCount = 4)
long realTop;
@Chunk(byteCount = 4)
long realLeft;
@Chunk(byteCount = 4)
long realBottom;
@Chunk(byteCount = 4)
long realRight;
}
/**
* A blend range indicates the tonal range for a given channel.
*/
@Chunked
static final class BlendRange {
@Chunk(byteCount = 1)
short srcBlackIn;
@Chunk(byteCount = 1)
short srcWhiteIn;
@Chunk(byteCount = 1)
short srcBlackOut;
@Chunk(byteCount = 1)
short srcWhiteOut;
@Chunk(byteCount = 1)
short dstBlackIn;
@Chunk(byteCount = 1)
short dstWhiteIn;
@Chunk(byteCount = 1)
short dstBlackOut;
@Chunk(byteCount = 1)
short dstWhiteOut;
}
/**
* Layer properties encode a lot of interesting attributes but we will
* only decode a few of them.
*/
@Chunked
static final class LayerProperty {
// The property holds the layer's effects (drop shadows, etc.)
static final String KEY_EFFECTS = "lfx2";
// The property holds the layer's stacked effects (multiple
// drop shadows, etc.) If this property is present, the
// "lfx2" property above should not be present
static final String KEY_MULTI_EFFECTS = "lmfx";
// Indicates that this layer is a section (start or end of a layer group)
static final String KEY_SECTION = "lsct";
// The property holds the Unicode name of the layer
static final String KEY_NAME = "luni";
// The property holds the solid color adjustment information
static final String KEY_ADJUSTMENT_SOLID_COLOR = "SoCo";
// Fill opacity
static final String KEY_FILL_OPACITY = "iOpa";
// The property holds the text information (styles, text data, etc.)
static final String KEY_TEXT = "TySh";
// The property holds the vector data (can be "vsms" instead)
static final String KEY_VECTOR_MASK = "vmsk";
// The property holds the vector data (can be "vmsk" instead)
// When this key is present, we must also look for "vscg"
static final String KEY_SHAPE_MASK = "vsms";
// The property holds graphics data for a shape mask defined by "vsms"
static final String KEY_SHAPE_GRAPHICS = "vscg";
// The property holds the stroke data
static final String KEY_STROKE = "vstk";
// The layer has a depth of 16 bit per channel
static final String KEY_LAYER_DEPTH_16 = "Lr16";
// The layer has a depth of 32 bit per channel
static final String KEY_LAYER_DEPTH_32 = "Lr32";
@Chunk(byteCount = 4)
String signature;
@Chunk(byteCount = 4)
String key;
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "(layerProperty.length + 3) & ~3",
switchType = {
@Chunk.Case(test = "layerProperty.key.equals(\"lmfx\")", type = LayerEffects.class),
@Chunk.Case(test = "layerProperty.key.equals(\"lfx2\")", type = LayerEffects.class),
@Chunk.Case(test = "layerProperty.key.equals(\"lsct\")", type = LayerSection.class),
@Chunk.Case(test = "layerProperty.key.equals(\"luni\")", type = UnicodeString.class),
@Chunk.Case(test = "layerProperty.key.equals(\"SoCo\")", type = SolidColorAdjustment.class),
@Chunk.Case(test = "layerProperty.key.equals(\"iOpa\")", type = byte.class),
@Chunk.Case(test = "layerProperty.key.equals(\"TySh\")", type = TypeToolObject.class),
@Chunk.Case(test = "layerProperty.key.equals(\"vmsk\")", type = ShapeMask.class),
@Chunk.Case(test = "layerProperty.key.equals(\"vsms\")", type = ShapeMask.class),
@Chunk.Case(test = "layerProperty.key.equals(\"vscg\")", type = ShapeGraphics.class),
@Chunk.Case(test = "layerProperty.key.equals(\"vstk\")", type = ShapeStroke.class),
@Chunk.Case(test = "layerProperty.key.equals(\"Lr16\")", type = LayersList.class),
@Chunk.Case(test = "layerProperty.key.equals(\"Lr32\")", type = LayersList.class),
}
)
Object data;
}
@Chunked
static final class LayerEffects {
// Boolean to toggle effects on/off
static final String KEY_MASTER_SWITCH = "masterFXSwitch";
static final String KEY_PRESENT = "present";
static final String KEY_ENABLED = "enab";
// Shadows
static final String KEY_INNER_SHADOW = "IrSh";
static final String KEY_INNER_SHADOW_MULTI = "innerShadowMulti";
static final String KEY_DROP_SHADOW = "DrSh";
static final String KEY_DROP_SHADOW_MULTI = "dropShadowMulti";
// Shape stroke
static final String KEY_STROKE = "FrFX";
@Chunk(match = "0")
int version;
@Chunk
int descriptorVersion;
@Chunk
Descriptor effects;
}
/**
* A layer section is a property that marks layer groups.
*/
@Chunked
static final class LayerSection {
/**
* The section type.
*/
enum Type {
/**
* Who knows.
*/
OTHER,
/**
* Open group of layers.
*/
GROUP_OPENED,
/**
* Closed group of layers.
*/
GROUP_CLOSED,
/**
* End of a group (invisible in the UI).
*/
BOUNDING
}
@Chunk(byteCount = 4)
Type type;
@Chunk(byteCount = 4,
readIf = "(($T) stack.get(1)).length >= 12",
readIfParams = { LayerProperty.class }
)
String signature;
@Chunk(byteCount = 4,
readIf = "(($T) stack.get(1)).length >= 12",
readIfParams = { LayerProperty.class }
)
String blendMode;
// Apparently only used for the animation timeline
@Chunk(byteCount = 4,
readIf = "(($T) stack.get(1)).length >= 16",
readIfParams = { LayerProperty.class }
)
int subType;
}
@Chunked
static final class SolidColorAdjustment {
@Chunk
int version;
@Chunk
Descriptor solidColor;
}
/**
* The TypeToolObject layer property contains all the data needed to
* render a text layer.
*/
@Chunked
static final class TypeToolObject {
// Descriptor that holds the actual text
static final String KEY_TEXT = "Txt ";
// Descriptor that holds structured text data used for styling, see TextEngine
static final String KEY_ENGINE_DATA = "EngineData";
@Chunk
short version;
// The text's transform (translation, scale and shear)
@Chunk
double xx;
@Chunk
double xy;
@Chunk
double yx;
@Chunk
double yy;
@Chunk
double tx;
@Chunk
double ty;
@Chunk
short textVersion;
@Chunk
int testDescriptorVersion;
// The descriptor is a horrifyingly generic object
// that happens to hold important data (the actual
// text, styling info, etc.)
@Chunk
Descriptor text;
@Chunk
short warpVersion;
@Chunk
int warpDescriptorVersion;
@Chunk
Descriptor warp;
// These always seem to be set to 0
@Chunk
int left;
@Chunk
int top;
@Chunk
int right;
@Chunk
int bottom;
}
/**
* A vector mask is a layer property that contains a list of path
* records, used to describe a path (or vector shape).
*/
@Chunked
static final class ShapeMask {
@Chunk
int version;
@Chunk
int flags;
// LayerProperty.length is rounded to an even byte count
// Subtract the 8 bytes used for version and flags, and divide
// by the length of each path record (26 bytes) to know how many
// path records to read
@Chunk(dynamicSize = "(int) Math.floor(((($T) stack.get(1)).length - 8) / 26)",
sizeParams = { LayerProperty.class })
List<PathRecord> pathRecords;
}
/**
* A shape graphics object describes all the graphics properties of
* a shape mask (color, gradient, etc.)
*/
@Chunked
static final class ShapeGraphics {
@Chunk(byteCount = 4)
String key;
@Chunk(byteCount = 4)
long version;
@Chunk
Descriptor graphics;
}
/**
* Describes a shape layer's stroke properties.
*/
@Chunked
static final class ShapeStroke {
@Chunk(byteCount = 4)
long version;
@Chunk
Descriptor stroke;
}
/**
* A path records is either a single point in a path, a subpath
* marker or a command. A subpath can be closed or open.
*/
@Chunked
static final class PathRecord {
// Marks the beginning of a closed subpath
static final int CLOSED_SUBPATH_LENGTH = 0;
// Linked/unlinked matters only to interactive editing
// Linked just means that the two control points of a knot
// move together when one of them moves
static final int CLOSED_SUBPATH_KNOT_LINKED = 1;
static final int CLOSED_SUBPATH_KNOT_UNLINKED = 2;
// Marks the beginning of an open subpath
static final int OPEN_SUBPATH_LENGTH = 3;
static final int OPEN_SUBPATH_KNOT_LINKED = 4;
static final int OPEN_SUBPATH_KNOT_UNLINKED = 5;
// Photoshop only deal with even/odd fill rules, we can ignore it
static final int PATH_FILL_RULE = 6;
// Not sure what this does
static final int CLIPBOARD = 7;
// Initial fill rule, always present as first item in the list
// of path records
static final int INITIAL_FILL_RULE = 8;
/**
* A curve (or path) is made of a series of Bézier knot.
* Each knot is made of an anchor (point on the curve/path)
* and of two control points. One defines the slope of the
* curve before the anchor, the other the slope of the curve
* after the anchor.
*/
@Chunked
static final class BezierKnot {
@Chunk
int controlEnterY;
@Chunk
int controlEnterX;
@Chunk
int anchorY;
@Chunk
int anchorX;
@Chunk
int controlExitY;
@Chunk
int controlExitX;
}
/**
* Marks the start of a sub-path. The first command of a
* sub-path is always a "move to". While sub-paths could
* all be recorded in a single path, we must take into
* account Photoshop's operators: merge, subtract,
* intersect and XOR). We therefore record the sub-paths
* individually to be able to apply the operators
* properly later on.
*/
@Chunked
static final class SubPath {
// If the tag equals this value, then the subpath is
// not a path operation and must be added to the
// current path
static final int NO_OP = 0x0;
static final int OP_XOR = 0x0;
static final int OP_MERGE = 0x1;
static final int OP_SUBTRACT = 0x2;
static final int OP_INTERSECT = 0x3;
@Chunk(byteCount = 2)
int knotCount;
@Chunk(byteCount = 2)
int op;
@Chunk(byteCount = 2)
int tag;
}
// Indicates the path record type
@Chunk
short selector;
@Chunk(byteCount = 24,
switchType = {
@Chunk.Case(test = "pathRecord.selector == 0 || pathRecord.selector == 3",
type = SubPath.class),
@Chunk.Case(test = "pathRecord.selector == 1 || pathRecord.selector == 2 || " +
"pathRecord.selector == 4 || pathRecord.selector == 5",
type = BezierKnot.class)
}
)
Object data;
}
/**
* Contains the image data for each channel of a layer.
* There is one ChannelsContainer per layer.
*/
@Chunked
static final class ChannelsContainer {
@Chunk(
dynamicSize =
"$T list = ($T) stack.get(1);\n" +
"size = list.layers.get(list.channels.size()).channels",
sizeParams = {
LayersList.class,
LayersList.class
}
)
List<ChannelImageData> imageData;
}
/**
* The image data for a layer's channel. The compression method can
* in theory vary per layer. The overall length of the data comes from
* the ChannelInformation section seen earlier.
*/
@Chunked
static final class ChannelImageData {
@Chunk(byteCount = 2)
CompressionMethod compression;
// Subtract 2 bytes because the channel info data length takes the
// compression method into account
@Chunk(
dynamicByteCount =
"$T list = ($T) stack.get(2);\n" +
"$T layer = list.layers.get(list.channels.size());\n" +
"$T container = ($T) stack.get(1);\n" +
"$T info = layer.channelsInfo.get(container.imageData.size());\n" +
"byteCount = info.dataLength - 2",
byteCountParams = {
LayersList.class, LayersList.class,
RawLayer.class,
ChannelsContainer.class, ChannelsContainer.class,
ChannelInformation.class
}
)
byte[] data;
}
/**
* Photoshop's compression method for layer channels and
* the flattened planar image.
*/
enum CompressionMethod {
RAW,
RLE,
ZIP,
ZIP_NO_PREDICTION
}
/**
* The flattened image data. The compression method applies to all
* the channels. The channels are stored sequentially in the data array.
*/
@Chunked
static final class ImageData {
@Chunk(byteCount = 2)
CompressionMethod compression;
// Use a 128K buffer because images can be large
// We might want to choose a buffer size that's function of
// the dimensions/compression method
@Chunk(bufferSize = 0x20000)
byte[] data;
}
/**
* A descriptor is a generic way to represent typed and named
* data. Photoshop seems to abuse descriptor quite a bit in
* few places, particularly for text layers.
*/
@Chunked
static final class Descriptor {
static final String CLASS_ID_COLOR_RGB = "RGBC";
static final String CLASS_ID_COLOR_HSB = "HSBC";
static final String CLASS_ID_COLOR_CMYK = "CMYC";
static final String CLASS_ID_COLOR_LAB = "LbCl";
static final String CLASS_ID_COLOR_GRAY = "Grsc";
// Name from classID, usually not set
@Chunk
UnicodeString name;
// ClassID
@Chunk
MinimumString classId;
// Number of items in the descriptor
@Chunk
int count;
@Chunk(dynamicSize = "descriptor.count", key = "String.valueOf(descriptorItem.key)")
Map<String, DescriptorItem> items;
@Override
public String toString() {
return "<descriptor name=\"" + name + "\" classId=\"" + classId + "\">" +
Strings.join(items.values(), "\n") +
"</descriptor>";
}
}
/**
* A descriptor item has a name and a value.
* The value itself is typed.
*/
@Chunked
static final class DescriptorItem {
/**
* Enum reference (see Reference descriptor item)
*/
@Chunked
static final class Enumerated {
@Chunk
MinimumString type;
@Chunk
MinimumString value;
@Override
public String toString() {
return String.valueOf(value);
}
}
/**
* A double value with a unit.
*/
@Chunked
static final class UnitDouble {
static final String POINTS = "#Pnt";
static final String MILLIMETERS = "#Mlm";
static final String CENTIMETERS = "RrCm";
static final String INCHES = "RrIn";
static final String ANGLE_DEGREES = "#Ang";
static final String RESOLUTION = "#Rsl"; // base per inch
static final String RELATIVE = "#Rlt"; // base 72ppi
static final String NONE = "#Nne";
static final String PERCENT = "#Prc";
static final String PIXELS = "#Pxl";
@Chunk(byteCount = 4)
String unit;
@Chunk
double value;
@Override
public String toString() {
String s = Double.toString(value);
switch (unit) {
case POINTS:
s += "pt";
break;
case MILLIMETERS:
s += "mm";
break;
case CENTIMETERS:
s += "cm";
break;
case INCHES:
s += "in";
break;
case ANGLE_DEGREES:
s += "°";
break;
case RESOLUTION:
s += "dpi";
break;
case RELATIVE:
s += "dpp";
break;
case NONE:
break;
case PERCENT:
s += "%";
break;
case PIXELS:
s += "px";
break;
}
return s;
}
}
/**
* A float value with a unit.
* See {@link UnitDouble} for possible units.
*/
@Chunked
static final class UnitFloat {
@Chunk(byteCount = 4)
String unit;
@Chunk
float value;
@Override
public String toString() {
return Float.toString(value);
}
}
/**
* A class name + class ID. Used in references.
*/
@Chunked
static final class ClassType {
@Chunk
UnicodeString name;
@Chunk
MinimumString classId;
}
/**
* A property reference.
*/
@Chunked
static final class Property {
@Chunk
ClassType classType;
@Chunk
MinimumString keyId;
}
/**
* A reference is actually a list of items.
* Hard to say why.
*/
@Chunked
static final class Reference {
@Chunked
static final class Item {
@Chunk(byteCount = 4)
String type;
@Chunk(switchType = {
@Chunk.Case(test = "item.type.equals(\"Enmr\")", type = Enumerated.class),
@Chunk.Case(test = "item.type.equals(\"Clss\")", type = ClassType.class),
@Chunk.Case(test = "item.type.equals(\"Idnt\")", type = int.class),
@Chunk.Case(test = "item.type.equals(\"indx\")", type = int.class),
@Chunk.Case(test = "item.type.equals(\"name\")", type = UnicodeString.class),
@Chunk.Case(test = "item.type.equals(\"prop\")", type = Property.class),
@Chunk.Case(test = "item.type.equals(\"rele\")", type = int.class)
})
Object data;
}
@Chunk
int count;
@Chunk(dynamicSize = "reference.count")
List<Item> items;
}
/**
* Holds a list of descriptor values.
*/
@Chunked
static final class ValueList {
@Chunk
int count;
@Chunk(dynamicSize = "valueList.count")
List<Value> items;
@Override
public String toString() {
return "<list>" + Strings.join(items, ", ") + "</list>";
}
}
/**
* The value of a descriptor. A value can be one of many types,
* see the annotation below to find out the exact list.
*/
@Chunked
static final class Value {
@Chunk(byteCount = 4)
String type;
@Chunk(switchType = {
@Chunk.Case(test = "value.type.equals(\"alis\")", type = FixedString.class),
@Chunk.Case(test = "value.type.equals(\"bool\")", type = boolean.class),
@Chunk.Case(test = "value.type.equals(\"comp\")", type = long.class),
@Chunk.Case(test = "value.type.equals(\"doub\")", type = double.class),
@Chunk.Case(test = "value.type.equals(\"enum\")", type = Enumerated.class),
@Chunk.Case(test = "value.type.equals(\"GlbC\")", type = ClassType.class),
@Chunk.Case(test = "value.type.equals(\"GlbO\")", type = Descriptor.class),
@Chunk.Case(test = "value.type.equals(\"long\")", type = int.class),
@Chunk.Case(test = "value.type.equals(\"obj\" )", type = Reference.class),
@Chunk.Case(test = "value.type.equals(\"Objc\")", type = Descriptor.class),
@Chunk.Case(test = "value.type.equals(\"TEXT\")", type = UnicodeString.class),
@Chunk.Case(test = "value.type.equals(\"tdta\")", type = FixedByteArray.class),
@Chunk.Case(test = "value.type.equals(\"type\")", type = ClassType.class),
@Chunk.Case(test = "value.type.equals(\"UnFl\")", type = UnitFloat.class),
@Chunk.Case(test = "value.type.equals(\"UntF\")", type = UnitDouble.class),
@Chunk.Case(test = "value.type.equals(\"VlLs\")", type = ValueList.class)
})
Object data;
@Override
public String toString() {
if (data == null) return null;
return data.toString();
}
}
// The name of the descriptor
@Chunk
MinimumString key;
@Chunk
Value value;
@Override
public String toString() {
return "<item key=\"" + key + "\">" + String.valueOf(value) + "</item>";
}
}
/**
* Stores the documents guides. Pretty straightforward.
*/
@Chunked
static final class GuidesResourceBlock {
static final int ID = 0x0408;
@Chunk
int version;
// Reserved chunk of 8 bytes. Ignore.
@Chunk(byteCount = 8)
Void future;
@Chunk
int guideCount;
@Chunk(dynamicSize = "guidesResourceBlock.guideCount")
List<GuideBlock> guides;
}
/**
* Orientation, used for guides.
*/
enum Orientation {
VERTICAL,
HORIZONTAL
}
/**
* An actual guide. It has a location (fixed-point 27.5 format)
* and an orientation.
*/
@Chunked
static final class GuideBlock {
@Chunk
int location;
@Chunk(byteCount = 1)
Orientation orientation;
}
/**
* Stores an ICC color profile.
*/
@Chunked
static final class ColorProfileBlock {
static final int ID = 0x040F;
@Chunk
byte[] icc;
}
/**
* Resolution units for the resolution info block.
*/
enum ResolutionUnit {
UNKNOWN,
PIXEL_PER_INCH,
PIXEL_PER_CM
}
/**
* Display units for the resolution info block.
* We don't really care about this unit.
*/
enum DisplayUnit {
UNKNOWN,
INCHES,
CENTIMETERS,
POINTS,
PICAS,
COLUMNS
}
/**
* Stores the document's vertical and horizontal resolution
* as well as information on how to display dimensions in the UI.
*/
@Chunked
static final class ResolutionInfoBlock {
static final int ID = 0x03ED;
@Chunk
int horizontalResolution;
@Chunk(byteCount = 2)
ResolutionUnit horizontalUnit;
@Chunk(byteCount = 2)
DisplayUnit widthUnit;
@Chunk
int verticalResolution;
@Chunk(byteCount = 2)
ResolutionUnit verticalUnit;
@Chunk(byteCount = 2)
DisplayUnit heightUnit;
}
/**
* Thumbnails can be stored as RAW data or JPEGs.
* We currently only support the JPEG format.
*/
@Chunked
static final class ThumbnailResourceBlock {
static final int ID = 0x040C;
// RAW=0, JPEG=1, we only want JPEG
@Chunk(match = "1")
int format;
@Chunk(byteCount = 4)
long width;
@Chunk(byteCount = 4)
long height;
@Chunk(byteCount = 4)
long rowBytes;
@Chunk(byteCount = 4)
long size;
@Chunk(byteCount = 4)
long compressedSize;
// JPEG guarantees 24bpp and 1 plane
@Chunk(match = "24")
short bpp;
@Chunk(match = "1")
short planes;
@Chunk(dynamicByteCount = "thumbnailResourceBlock.compressedSize")
byte[] thumbnail;
}
/**
* Stores an unsigned short.
*/
@Chunked
static final class UnsignedShortBlock {
static final int ID_INDEX_TABLE_COUNT = 0x0416;
static final int ID_INDEX_TRANSPARENCY = 0x0417;
@Chunk(byteCount = 2)
int data;
}
/**
* Helper class to read a byte array whose length is stored
* as a 32 bits unsigned integer.
*/
@Chunked
static final class FixedByteArray {
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "fixedByteArray.length")
byte[] value;
@Override
public String toString() {
if (value == null) return null;
return new String(value);
}
}
/**
* Helper class to read a Unicode string, encoded in UTF-16.
* The length, in characters, of the string is stored as a 32
* bits unsigned integer. The string is made of length*2 bytes.
*/
@Chunked
static final class UnicodeString {
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "unicodeString.length * 2", encoding = "UTF-16")
String value;
@Override
public String toString() {
if (value == null) return null;
if (value.isEmpty()) return "";
int lastChar = value.length() - 1;
if (value.charAt(lastChar) == '\0') {
return value.substring(0, lastChar);
}
return value;
}
}
/**
* Helper class to read an ASCII string whose length is
* stored as a 32 bits unsigned integer. If the length
* is 0, the string is assumed to have a length of 4 bytes.
*/
@Chunked
static final class MinimumString {
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "Math.max(minimumString.length, 4)")
String value;
@Override
public String toString() {
return value;
}
}
/**
* Helper class to read an ASCII string whose length is
* stored as a 32 bits unsigned integer.
*/
@Chunked
static final class FixedString {
@Chunk(byteCount = 4)
long length;
@Chunk(dynamicByteCount = "fixedString.length")
String value;
@Override
public String toString() {
return value;
}
}
}