Skip to content

Instantly share code, notes, and snippets.

@esnya
Created January 26, 2015 09:38
Show Gist options
  • Save esnya/b2bc953bd6e606edaa81 to your computer and use it in GitHub Desktop.
Save esnya/b2bc953bd6e606edaa81 to your computer and use it in GitHub Desktop.
import std.bitmanip;
import std.conv;
import std.exception;
import std.range;
import std.stdio;
import std.traits;
struct PSDHeader {
char[4] signature;
ushort version_;
ubyte[6] reserved;
ushort channels;
uint height;
uint width;
ushort depth;
ColorMode colorMode;
}
enum ColorMode : ushort {
Bitmap = 0,
Grayscale = 1,
Indexed = 2,
RGB = 3,
CMYK = 4,
Multichannel = 7,
Duotone = 8,
Lab = 9,
}
struct ColorModeData {
uint length;
}
struct ImageResource {
char[4] signature;
ushort id;
string name;
void[] data;
}
struct ResolutionInfo {
int hRes;
ushort hResUnit;
ushort widthUnit;
int vRes;
ushort vResUnit;
ushort heightUnit;
}
struct LayerInfo {
ushort count;
LayerRecord[] records;
}
enum BlendMode : char[4] {
pass_through = "pass",
normal = "norm",
dissolve = "diss",
darken = "dark",
multiply = "lite",
color_burn = "idiv",
linear_burn = "lbrn",
darker_color = "dkCl",
lighten = "lite",
screen = "scrn",
color_dodge = "div ",
linear_dodge = "lddg",
lighter_color = "lgCl",
soft_light = "over",
hard_light = "sLit",
vivid_light = "hLit",
linear_light = "vLit",
pin_light = "pLit",
hard_mix = "hMix",
difference = "diff",
exclusion = "smud",
subtract = "fsub",
divide = "fdiv",
hue = "hue ",
saturation = "sat ",
color = "colr",
luminosity = "lum ",
}
struct ChannelInformation {
short id;
uint dataLength;
ChannelImageData imageData;
}
struct LayerRecord {
uint top;
uint left;
uint bottom;
uint right;
ushort channels;
ChannelInformation[] channelInformations;
char[4] blendModeSignature;
BlendMode blendMode;
ubyte opacity;
bool clipping;
ubyte flags;
ubyte filler;
uint extradataLength;
// mask / adjustment
// blending ranges
string name;
}
enum Compression : ushort {
raw_data,
rle_compressed,
zip_without_prediction,
zip_with_prediction,
}
struct ChannelImageData {
Compression compression;
void[] data;
ubyte[][] decode() {
enforce(compression == Compression.rle_compressed, "Unsupported compressioin type");
auto rlelines = cast(ubyte[][])data;
auto lines = new ubyte[][rlelines.length];
foreach (y, rleline; rlelines) {
auto line = appender!(ubyte[]);
while (!rleline.empty) {
auto n = cast(byte)rleline.front;
rleline.popFront();
if (n >= 0 && n <= 127) {
foreach (i; 0 .. (n+1)) {
line.put(rleline.front);
rleline.popFront();
}
} else if (n <= -1 && n >= -127) {
foreach (i; 0 .. (1-n)) {
line.put(rleline.front);
}
rleline.popFront();
}
}
lines[y] = line.data;
}
return lines;
}
}
class PSD {
this(string filename) {
file.open(filename, "rb");
enforce(file.isOpen);
foreach (name; __traits(allMembers, PSDHeader)) {
alias T = typeof(__traits(getMember, PSDHeader, name));
__traits(getMember, header, name) = _read!T();
if (name == "signature") {
enforce(header.signature == cast(ubyte[])"8BPS");
}
}
colorModeData.length = _read!uint();
enforce(colorModeData.length == 0 || header.colorMode == ColorMode.Indexed);
enforce(colorModeData.length == 0, "Not Implemented Indexed Color Mode");
// Skip ImageResources
file.seek(_read!uint(), SEEK_CUR);
auto length = _read!uint();
auto end = length + file.tell();
auto infoLength = _read!uint();
layerInfo.count = _read!ushort();
layerInfo.records.length = layerInfo.count;
foreach (ref record; layerInfo.records) {
record.top = _read!uint();
record.left = _read!uint();
record.bottom = _read!uint();
record.right = _read!uint();
record.channels = _read!ushort();
record.channelInformations.length = record.channels;
foreach (ref channelInformation; record.channelInformations) {
channelInformation.id = _read!ushort();
channelInformation.dataLength = _read!uint();
}
record.blendModeSignature = _read!(char[4])();
enforce(record.blendModeSignature == "8BIM");
record.blendMode = _read!(BlendMode)();
record.opacity = _read!ubyte();
record.clipping = cast(bool)_read!ubyte();
record.flags = _read!ubyte();
record.filler = _read!ubyte();
record.extradataLength = _read!uint();
auto extraStart = file.tell();
// Skip Layer mask/ sdjustment layer data
file.seek(_read!uint(), SEEK_CUR);
// Skip Layer Blending ranges data
file.seek(_read!uint(), SEEK_CUR);
// name
record.name = _read!string();
// Skip Extra Data
file.seek(extraStart + record.extradataLength);
}
foreach (ref record; layerInfo.records) {
foreach (c, ref channel; record.channelInformations) {
auto dataEnd = file.tell() + channel.dataLength;
ChannelImageData channelImageData;
channelImageData.compression = _read!Compression();
enforce(channelImageData.compression == Compression.rle_compressed, "Not Implemented");
auto rows = record.bottom - record.top;
auto rowSizes = new ushort[rows];
foreach (ref rowSize; rowSizes) {
rowSize = _read!ushort();
}
ubyte[][] data;
foreach (rowSize; rowSizes) {
auto rowEnd = file.tell() + rowSize;
data ~= _read(rowSize);
assert(file.tell() == rowEnd);
}
assert(file.tell() == dataEnd);
channelImageData.data = data;
channel.imageData = channelImageData;
file.seek(dataEnd);
}
}
//assert(0);
}
File file;
PSDHeader header;
ColorModeData colorModeData;
ImageResource[] imageResources;
LayerInfo layerInfo;
private ubyte[] _read(size_t n) {
auto buf = new ubyte[n];
enforce(file.rawRead(buf).length == n);
return buf;
}
private T _read(T)() if(isDynamicArray!T) {
T dummy;
typeof(dummy.dup) buf;
static if (isSomeString!T) {
buf.length = _read!ubyte();
} else {
buf.length = _read!uint();
}
if (buf.length > 0) {
enforce(file.rawRead(buf).length == buf.length, buf.length.to!string());
}
static if (isSomeString!T) {
if (buf.length == 1 && buf[0] == 0) {
return null;
}
}
return cast(T)buf;
}
private T _read(T)() if(!isSomeString!T && !isDynamicArray!T) {
enum size = T.sizeof;
ubyte[size] buf;
enforce(file.rawRead(buf).length == size);
static if (isStaticArray!T && ElementEncodingType!T.sizeof == 1) {
return cast(T)buf;
} else {
return cast(T)buf.bigEndianToNative!(OriginalType!T)();
}
}
}
///
unittest {
import std.algorithm;
import std.range;
auto psd = new PSD("test.psd");
with (psd.header) {
assert(signature == [cast(ubyte)'8', 'B', 'P', 'S']);
assert(version_ == 1);
assert(reserved == [0, 0, 0, 0, 0, 0]);
assert(channels == 4);
assert(height == 1000);
assert(width == 1000);
}
assert(psd.colorModeData.length == 0);
assert(psd.layerInfo.records.length == 4);
foreach (layerRecord; psd.layerInfo.records) {
assert(layerRecord.channelInformations.length == 4);
foreach (channelInformation; layerRecord.channelInformations) {
auto data = channelInformation.imageData.decode();
assert(data.length == layerRecord.bottom - layerRecord.top);
foreach (line; data) {
assert(line.length == layerRecord.right - layerRecord.left);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment