業務で psd ファイルを解析する必要があったのでその調査した内容についてまとめます。
ファイルフォーマット仕様に関してはAdobeが公開しているのでこちらの情報に準じます。
各データのセクションについて
データはいくつかのセクションに分かれており、
・File Header セクション
・Color Mode Data セクション
・Image Resources セクション
・Layer and Mask Information セクション
・Image Data セクション
に分かれています。
Color Mode Dataセクション、Image Resources セクション、Image Dataセクションに関しては今回は使用しなかったので割愛します。
データ型
この記事では各種データ型サイズは以下のように定義します。
型名 | サイズ |
---|---|
byte | 1byte |
short | 2byte |
int | 4byte |
long | 8byte |
double | 8byte |
と定義します。
また、psd ファイルでは上記データ型は全てビッグエンディアンである事に注意してください。
また、文字列はPascal文字列とUnicode文字のどちらかが使われます。
PascalString
先頭の1バイトに文字列長が定義されている文字列です。
2バイト、もしくは4バイトでパディングすることがあります。
(パディング長はプロパティによって異なるので注意)
UnicodeString
先頭4バイトに文字数が含まれる ビッグエンディアンの2バイト文字列です。
この文字数はバイト数ではないので1文字2バイトで計算する必要がある点に注意してください。
Rect
レイヤーの描画範囲、マスクの範囲などで使われるデータ構造です。
データサイズ | プロパティ名 |
---|---|
int | Top |
int | Left |
int | Bottom |
int | Right |
File Header セクション
データサイズ | プロパティ名 | |
---|---|---|
byte[4] | Signature | 常に8BPS の4文字が含まれる |
short | Version | 常に 1 である事が期待される(ただしPSBファイルの場合は2である) |
byte[6] | Reserved | 常に全て0である(使用されない) |
short | NumberOfChannels | チャンネル数(範囲は 1~56 まで) |
int | Height | キャンバスサイズ縦(範囲は 1~30,000 まで) |
int | Width | キャンバスサイズ横(範囲は 1~30,000 まで) |
short | Depth | ビット深度(1, 8, 16, 32) |
short | ColorMode | カラーモード(定義は後述) |
カラーモード定義
enum として定義すると下記のようになります。
enum ColorMode
{
Bitmap = 0,
Grayscale = 1,
Indexed = 2,
RGB = 3,
CMYK = 4,
Multichannel = 7,
Duotone = 8,
Lab = 9
}
今回はRGBしか使用しなかったので本記事ではカラーモードは RGB であるという前提で書きます。
Color Mode Data セクション
データサイズ | プロパティ名 | |
---|---|---|
int | Length | セクション長さ |
カラーモードが Indexed, Duotoneである場合に使用されるらしいのですが、
使用しなかったので内容がよくわからないので本記事ではスキップします。
(Indexed, Duotone以外はこのセクションのサイズは 0 です)
Image Resources セクション
データサイズ | プロパティ名 | |
---|---|---|
int | Length | セクション長さ |
下記のような構造をしたデータブロックが連続して続いているセクションです。
今回は使用しなかったのでこちらの詳細はスキップします。
データサイズ | プロパティ名 | |
---|---|---|
byte[4] | Signature | 常に8BIM の4文字が含まれる |
short | ResourceID | リソースのIDが含まれる。詳細はこちら |
PascalString | Name | 2バイトでパディングされたPascal文字列。 null で合った場合も2バイト含まれる(中身は0) |
int | Length | Data のサイズです |
byte[Length] | Data | 各ResourceID別に格納されたデータのサイズ |
Layer and Mask Information セクション
レイヤー情報が細かく定義されているセクションです。
この記事では主にこのセクションについての説明を行います。
データサイズ | プロパティ名 | |
---|---|---|
int | Length | このセクション全体のサイズ |
LayerInfo | LayerInfo | レイヤーの基本情報が含まれる (次の項目で説明) |
GlobalLayerMaskInformation | GlobalLayerMaskInformation | 使っていないので本記事では説明なし |
LayerInfo
データサイズ | プロパティ名 | |
---|---|---|
int | Length | このデータブロックのサイズ |
short | LayerCount | レイヤー数 (※1 |
LayerRecord[] | LayerRecords | 各レイヤーの情報が含まれます。(次の項目で説明) |
ChannelImageData[] | ChannelImageData | LayerRecordsの各レイヤーの画素情報が順番に定義されています。 |
※1) このレイヤー数は負の数である場合があります。
その場合は絶対値を使用します。
どうやら一番最初のレイヤーのアルファチャンネルに透明度データが含まれる場合、
負の数になるようです。
LayerRecord
データサイズ | プロパティ名 | |
---|---|---|
Rect | Rect | レイヤーの範囲 |
short | ChannelCount | チャンネル数 |
Channel[ChannelCount] | Channels | チャンネル情報が含まれます。(次の項目で説明) |
byte[4] | BlendSignature | ブレンドモードのシグネチャ(常に8BIM ) |
byte[4] | BlendMode | 4文字で表すブレンドモード |
byte | Opacity | 透明度(0〜255) |
byte | Clipping | クリッピングフラグ |
byte | Flags | ビットフラグ |
ビット0 | 「透明性を保護」にチェックが入っている場合 1 そうでない場合は 0 |
|
ビット1 | レイヤーが非表示になっている場合は 1 表示状態である場合は 0 |
|
ビット2 | 廃止 | |
ビット3 | Photoshop 5.0 以降では 1、ビット 4 が有用な情報を持っているかどうかを示す。 | |
ビット4 | ドキュメントの外観とは無関係なピクセルデータであるか | |
byte | Filler | 常に 0 |
int | ExtraDataFieldLength | 次のデータブロックの長さ |
LayerMaskAdjustmentLayerData | Layer mask / adjustment layer data | レイヤーマスクや調整レイヤーの情報が含まれているらしい。 あまり使っていないので割愛 |
LayerBlendingRangesData | Layer blending ranges data | 使っていないので割愛 |
PascalString | LayerName | レイヤー名 (※1 4バイトパディング |
AdditionalLayerInformation[] | AdditionalLayerInformations | レイヤーに付属する情報 (レイヤー名やテキストレイヤーであればテキスト情報、レイヤーグループであるかどうかなど) |
※1) このレイヤー名は32文字までしか含まれず、フルネームを取得したい場合はAdditionalLayerInformationから 'luni' タグに含まれる名前を取得したほうが良いです。
ブレンドモード
Key | Blend Mode Name | |
---|---|---|
pass | pass | 通過 |
norm | normal | 通常 |
diss | dissolve | ディザ合成 |
dark | darken | 比較(暗) |
'mul ' | multiply | 乗算(mul + 半角スペース1文字 なので注意) |
idiv | color burn | 焼き込みカラー |
lbrn | linear burn | 焼き込み(リニア) |
dkCl | darker color | カラー比較(暗) |
lite | lighten | 比較(明) |
scrn | screen | スクリーン |
'div ' | color dodge | 覆い焼きカラー |
lddg | linear dodge | 覆い焼き(リニア)-加算 |
lgCl | lighter color | カラー比較(明) |
over | overlay | オーバーレイ |
sLit | soft light | ソフトライト |
hLit | hard light | ハードライト |
vLit | vivid light | ビビッドライト |
lLit | linear light | リニアライト |
pLit | pin light | ピンライト |
hMix | hard mix | ハードミックス |
diff | difference | 差の絶対値 |
smud | exclusion | 除外 |
fsub | subtract | 減算 |
fdiv | divide | 除算 |
'hue ' | hue | 色相 |
'sat ' | saturation | 彩度 |
colr | color | カラー |
'lum ' | luminosity | 輝度 |
わかりやすくまとめているサイトがありました。
http://applehaguki.blog55.fc2.com/blog-entry-103.html
Channel
レイヤーのチャンネル情報
RGBモードであればR, G, B, Transparencyの4種類のチャンネル情報が1つのレイヤーに含まれている。
データサイズ | プロパティ名 | |
---|---|---|
short | ChannelId | チャンネルID |
int | Length | チャンネルデータサイズ |
チャンネルID
enum ChannelId
{
Red = 0,
Green = 1,
Blue = 2,
Transparency = -1,
UserLayerMask = -2,
RealUserLayerMask = -3,
}
Channel Image Data部分
データサイズ | プロパティ名 | |
---|---|---|
short | Compression | 圧縮形式 |
byte[Length] | ImageData | チャンネルの画素情報 |
実際のチャンネルデータ(画素情報)は LayerRecords を全て読み終えた後の領域に存在する点に注意
(便宜上別名にしていますが ChannelImageData と Channel は同じものです。)
コードとして実装するとこのような実装になります。
public class LayerInfo
{
private LayerRecord[] m_layerRecords;
// LayerInfo の情報を読み取る
public void Read(IPsdReader reader)
{
// データブロックサイズを取得
int length = reader.ReadInt();
// レイヤー数を取得
short layerCount = Math.Abs(reader.ReadShort());
// LayerRecord を読み込む
m_layerRecords = new Layer[layerCount];
for (int i = 0; i < layerCount; ++i)
{
m_layerRecords[i] = new LayerRecord();
m_layerRecords[i].Read(reader);
}
// 全 LayerRecord を読み込んだら ChannelImage を読み取る
foreach (var layerRecord in m_layerRecords)
{
layerRecord.ReadChannelImage();
}
}
}
public class LayerRecord
{
private readonly Channel[] m_channels;
private readonly List<AdditionalLayerInformation> m_additionalLayerInformations = new();
public void Read(IPsdReader reader)
{
Rect = Rectangle.Read(reader); // Rect 情報読み取り
short channelCount = reader.ReadShort(); // チャンネル数
m_channels = new Channel[channelCount];
// チャンネル情報読み取り
for (int i = 0; i < m_channels.Length; ++i)
{
m_channels[i] = new Channel();
m_channels[i].Read(reader);
}
reader.Skip(4); // `8BIM`
BlendMode = reader.ReadString(4);
Opacity = reader.ReadByte();
reader.Skip(1); // Clipping
Flags = reader.ReadByte();
reader.Skip(1); // Filler
int extraDataLength = reader.ReadInt();
// Layer mask / adjustment layer data
int layerMaskAndAdjustmentDataLength = reader.ReadInt();
reader.skip(layerMaskAndAdjustmentDataLength);
// Layer blending ranges data.
int layerBlendingDataLength = reader.ReadInt();
reader.Skip(layerBlendingDataLength);
LayerName = reader.ReadPascalString(padding: 4);
// Additional Layer Information
while (AdditionalLayerInformation.TryReadBlock(this, reader, out AdditionalLayerInformation info))
{
m_additionalLayerInformations.Add(info);
}
}
// ImageData を読み取る
public void ReadChannelImage(IPsdReader reader)
{
foreach (var channel in m_channels)
{
channel.ReadChannelImage(reader);
}
}
}
public class Channel
{
public ChannelId ChannelId { get; private set; }
private readonly int m_dataLength;
public short Compression { get; private set; }
public byte[] ImageData {get; private set; }
public void Read(IPsdReader reader)
{
ChannelId = reader.ReadShort();
m_dataLength = reader.ReadInt();
}
public void ReadChannelImageData(IPsdReader reader)
{
Compression = (Compression)reader.ReadShort();
ImageData = reader.ReadBytes(m_dataLength - sizeof(Compression));
}
}
interface IPsdReader
{
byte ReadByte();
byte[] ReadBytes(long count);
short ReadShort();
int ReadInt();
long ReadLong();
double ReadDouble();
void Skip(int byteCount);
string ReadPascalString(int padding);
string ReadUnicodeString();
}
AdditionalLayerInformation
レイヤーに付属する情報ブロック
4文字の識別情報(Code)と紐づいて定義されています。
共通情報
データサイズ | プロパティ名 | |
---|---|---|
byte[4] | Signature | 常に 8BIM
|
byte[4] | Code | 識別情報 |
以下、識別情報別にそれぞれ定義されたデータを保持する |
識別情報
Code | 概要 | 補足 |
---|---|---|
luni | Unicode文字が含まれるブロック | レイヤー名が含まれる |
lnsr | レイヤーIDで使われるレイヤー名?を持つブロック | 使い方はよくわかってない |
lyid | レイヤーIDを持つブロック | 使い方はよくわかってない |
lsct | レイヤーグループ情報を持つブロック | Section Divider Settingと呼ばれている。 レイヤーグループとグループの終端となるレイヤー情報( </Layer group> というレイヤーがグループの終端に置かれる)に付加されている。 |
TySh | Type Tool Objectを持つブロック | テキストレイヤーの場合、テキストの情報が含まれる |
識別情報別のデータ構造
luni
データサイズ | |
---|---|
UnicodeString | Unicode文字列 |
lnsr
データサイズ | |
---|---|
int | Id for the layer name |
lyid
データサイズ | |
---|---|
int | layer id |
lsct
データサイズ | ||
---|---|---|
int | SectionDividerType | エディタ上でフォルダが展開されているか or 閉じているか、またはレイヤーグループの終了地点であるかどうか |
以下、Lengthが12以上の場合 | ||
byte[4] | Signature | 常に8BIM
|
byte[4] | BlendModeKey | ブレンドモード? |
以下、Lengthが16以上の場合 | ||
int | SubType | 使っていないのでよくわからない |
SectionDividerType
enum として定義すると以下のような感じ
// レイヤーグループの状態
enum SectionDividerType
{
AnyOtherType = 0,
// 展開されている
OpenFolder = 1,
// 閉じている
ClosedFolder = 2,
// グループの終了地点(</Layer Group>)である
BoundingSectionDivider = 3,
}
この場合、SectionDividerSettings は以下のような値になります。
TypeToolObjectは説明が長くなるので別の記事で書く予定です。
レイヤー順番
レイヤーの順番は一番下から順になっており、この場合は
wood
↓
</Layer group> (もしくは </Layer set> )
↓
img
↓
tape shadow
という順番になります。
構造を図にすると以下のような感じです。
Image Data セクション
このセクションには全てのレイヤーを統合した画像情報が含まれます。
色情報はChannelImageと異なり、Red, Green, Blueの順番で格納されているデータ
データブロックをスキップしたい場合
だいたいのデータブロックは先頭に4バイトのデータ長が含まれているので必要ないブロックに関しては取得したデータブロックサイズ分だけストリームを進めればスキップ可能