Created
January 26, 2015 09:38
-
-
Save esnya/b2bc953bd6e606edaa81 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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