Структура и формат файла Photoshop. Часть 5: Image Data

2012-05-18 papirosnik Photoshop

Итак, последняя, заключительная статья цикла, венчающая все наши наработки в области парсинга psd, и позволяющая целиком прочитать содержащееся в psd изображение. По правде говоря, эта секция (Image Data) лично для меня представляет мало интереса, так как основная полезная информация содержится в слоях 4-й секции и описана соответственно, в предыдущей, четвертой статье. Но в той статье я умолчал про то, как изображение декодировать. Теперь восполним этот пробел. Кроме того, этот блок информации может понадобиться тем, кто желает получить составное изображение из фотошоповского документа, не вдаваясь в детали. Например, просмотровщику графических файлов совсем необязательно знать что-либо о слоях и режимах блендинга, масках и эффектах; достаточно прочитать эту последнюю секцию, которая содержит готовое композитное изображение. Также немаловажно отметить то, что эта секция может вообще отсутствовать в psd в том случае, если документ не сохранялся в режиме максимальной совместимости. Но узнать о её наличии или отсутствии можно лишь прочитав предыдущие блоки (или по крайне узнав их размеры и пропустив соответствующее число байт).

В отличие от предыдущих секций psd, эта не начинается четырьмя байтами, содержащими длину. Это, видимо, вызвано тем фактом, что секция последняя, и таковой уже останется до скончания формата. Все, что будет в дальнейшем добавляться в этот сложносочиненный формат, будет вставлено в третью и четвертую секции, предварено новыми тегами, отмечено новыми флагами и тому подобными атрибутами, что внесет еще большую сумятицу в и без того запутанный формат. Ну да ладно, это заботы еще не наступившие, поэтому будем разгребать то что есть.

Image Data начинается по деловитому, без лишних вступлений, сигнатур и маркеров, сразу с 2-х байтового поля, обозначающего метод сжатия композитного изображения (в скобках дается мой перевод оригинальных пояснений):

0 = Raw image data (необработанные данные изображения)

1 = RLE compressed the image data starts with the byte counts for all the scan lines (rows * channels), with each count stored as a two-byte value. The RLE compressed data follows, with each scan line compressed separately. The RLE compression is the same compression algorithm used by the Macintosh ROM routine PackBits , and the TIFF standard.
(RLE сжатие. Данные изображения начинаются с размера (в байтах) для каждой линии (высота картинки в пикселах, помноженная на количество каналов). Каждый счетчик размера представлен двумя байтами. Далее следуют строки изображения, сжатые алгоритмом RLE каждая по-отдельности. RLE сжатие — такое же, как принято Макинтошевской ROM утилитой PackBits, а также TIFF стандартом).

2 = ZIP without prediction. (Zip-сжатие без предсказания)

3 = ZIP with prediction. (Zip  c предсказанием)

Итак, прочитав эту пару байт (BigEndian!), выясняем что нас ждет дальше и как себя вести. В случае, если мы прочитали два ноля (Raw image Data), то дальше нас ждет чистый поток байт, описывающих пиксели в формате RRR GGG BBB и т.д. То есть, сначала идут байты для канала, содержащего красную компоненту цвета. Сколько байт приходиться на канал можно узнать перемножив ширину картинки на высоту (и на число бит на канал). Например, если у нас картинка размером 100*100 пикселов и режим RGB с 8-ю битами на канал (1 байт на цветовую компоненту), нам следует читать 10 тыщ байт,  обозначающих красную компоненту цвета, затем столько же зеленной, затем еще раз то же количество для синей составляющей. Если у нас 16 или 32 бита на канал, то читаем 20 или 40 тысяч соответственно. Хотя оригинальный фотошоповский документ утверждает что могут быть и другие составляющие (RRR GGG BBB, etc.), на практике я такого не встречал. Но все же правильно будет принимать количество этих блоков равным количеству каналов, которое мы выяснили в самом начале (в 1-й секции). Прочитав все составляющие цвета для каждого пиксела, объединяем их в 32-битное слово (добавив альфа канал, если надо) и выводим на экран готовую составную картинку (или пишем в файл, или пакуем в какой-то другой формат). В случае Raw image data все довольно прозаично.

Если же мы выяснили, что нас ждет  RLE compressed — тоже не стоит особо отчаиваться. Сразу же приступаем к чтению двухбайтовых слов. Слов этих будет столько, сколько строк в изображении умножить на количество каналов. Если взять наш предыдущий пример (100*100 RGB 8 бит), то надо прочитать 300 двухбайтовых слов. Первые сто относятся к строкам с красной составляющей, следующие к зеленой, и последние 100 — к синей. Каждая строка сжимается отдельно и это двухбайтовое слово показывает размер сжатых данных. Т.е., если сложить все эти 300 слов — то мы получим размер сжатой картинки. По правде говоря, он нам особо и не нужен, но для контроля того, что все мы читаем правильно — не помешает.
Разузнав длину каждой сжатой строки можно приступить к декодированию. RLE распаковка происходит так:
1. Читаем очередной байт.
2. Если он меньше, чем 128, то значит он указывает сколько данных (+1) следует далее в незапакованном виде. Просто копируем полученное число следующих байт в наш выходной буфер.
2. Если же байт оказался больше чем 128 (отрицательный байт), то берем его инвертированное значение +1 (т.е 257 минус наш прочитанный байт). Это будет также длина. Но теперь она показывает, сколько раз надо размножить следующий байт. Читаем его и размножаем нужное число раз в выходной буфер.
3. Если прочитанный байт оказался равным 128, то ничего не делаем, переходим к шагу 1. Этот случай вероятно зарезервирован для дальнейших расширений RLE (которые уже видимо никогда не наступят).
4. Теперь, если в выходном буфере уже оказалось столько байт, сколько составляет наша ширина картинки (или если мы прочитали из входного буфера уже столько байт, сколько нам предначертано было выше двухбайтовым счетчиком для данной строки), то все — строка окончена.

Еще сжатые данные для каждой строки вроде могут содержать ноль для выравнивания на границу четности. Не помню. Я просто плюсую суммарный 2-х байтный счетчик размера блока для заданной цветовой компоненты (канала) и получаю начало следующего блока. Вот мой код распаковки RLE данных:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    case Channel.CompressionType.RLE:
	_pixelData = new byte[PixelSize];
	//unpack Channel data
	for (int y = 0; y < h; ++y)
	{
		int dest = (int)(y * rb);
		int count = 0;
		while (count < rleSize[y])
		{
			byte len = buf[ind++];
			++count;
			if (len < 128)
                        {
                                ++len;
 				Buffer.BlockCopy(buf, ind, _pixelData, dest, len);
                                ind += len;
                                dest += len;
                                count += len;
                        }
                        else if (len > 128)
			{
				len = (byte)(1 + 256 - len);
				byte what = buf[ind++];
				++count;
				for (int i = 0; i < len; ++i)
					_pixelData[dest++] = what;
			}
		}
	}
	index += totalRleData;
	break;

В результате мы снова получаем несколько блоков для каждого канала, компилируем их попиксельно (не забываем добавить альфу, если надо) и имеем готовую картинку из сжатого RLE изображения.

В случае Zip without Prediction также все еще не очень печально. Здесь снова нет никаких предварительных размеров — сразу же за маркером режима сжатия следуют данные, запакованные алгоритмом deflate. Вручную распаковать эти данные можно, но сложно. Проще воспользоваться имеющимися библиотеками (zlib, ZInputStream, или Deflate из System.IO.Compression в C#). Натравить их на входной буфер, указать выходной и — пожинать плоды автоматизации. В случае штатного декомпрессора на платформе .Net есть какие-то траблы. Что-то он там то-ли пропускает два байта, то-ли наоборот лишних требует. Не помню. Распаковывать с его помощью у меня получилось, но нужны были дополнительные телодвижения. По-моему, надо было к входному буферу прибавлять два байта смещения. В багрепортре Майкрософт про это написано. Ну да ладно, баги опричников Била остаются на их совести и к нашей борьбе с адобовским форматом отношения не имеют.

В случае же Zip with Prediction получается натуральный вынос мозга. Что это за предикация и с чем её едят — у меня сведений в голове оказалось почему то мизер. И даже любимый гугл молчит, как партизан, по этому вопросу. В итоге, после некоторой переписки с сильными мира програмистского, путем подсматривания в закрома кода на гитхабе и в результате некоторых умозаключений в воспаленном от бессоницы мозгу, родилось что-то сносноприемлемое. Но вразумительного объяснения тому, как оно работает, я дать не могу. Если с предсказаниями в zip хоть что-то немного понятно, то как оно ложится на наши пиксельные данные, и почему случай с 16-тью битами отличается от случая с 8-ю и 32-мя битами — для меня так и осталось загадкой. Эмпирическим путем удалось найти тот единственный (в моем случае) алгоритм, который вроде корректно расшифровывает и трактует пиксели из zip with prediction при любом количестве бит на канал. Поэтому делюсь, если кому надо. Понятного тут мало — так что не стесняйтесь спрашивать. Кое-что смогу пояснить. Отрадно еще то, что в большинстве типичных psd (32-битный цвет, 8 бит на канал) чаще встречается RLЕ сжатие. Zip с предсказаниями и без них — это удел каких-то в моей практике неприменимых монструозных psd с такой глубиной цвета, что имеющееся железо не в силах передать. И в предыдущей, 4-й секции psd, изображения слоев хранятся точно так же, поканально, и чаще либо вообще без сжатия, либо с RLE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void UnzipWithoutPred(ref int index,
	byte[] srcBuf, int srcLen,
	ref byte[] destBuf, int destLen)
{
	MemoryStream ms = new MemoryStream(srcBuf, index, srcLen, false);
	zlib.ZInputStream zis = new zlib.ZInputStream(ms);
	int ind = 0;
	int len = 0;
	do
	{
		len = zis.read(destBuf, ind, destLen);
		if (len>0)
			ind += len;
		else
			break;
	} while (len > 0);
	zis.Close();
	index += srcLen;
}
 
// -----------------------------------------------------------------
void UnzipWithPred(ref int index,
	byte[] srcBuf, int srcLen,
	ref byte[] destBuf, int destLen,
	int rowSize, int colorDepth)
{
	UnzipWithoutPred(ref index, srcBuf, srcLen, ref destBuf, destLen);
	byte[] buf = destBuf;
	int len;
	int ind = 0;
	do
	{
		len = rowSize;
		if (colorDepth == 16)
		{
			while (--len > 0)
			{
				buf[ind + 2] += (byte)(buf[ind + 0] + ((buf[ind + 1] + buf[ind + 3]) >> 8));
				buf[ind + 3] += buf[ind + 1];
				ind += 2;
			}
			ind += 2;
			destLen -= rowSize + rowSize;
		}
		else
		{
			while (--len > 0)
			{
				buf[ind + 1] += buf[ind];
				++ind;
			}
			++ind;
			destLen -= rowSize;
		}
	} while (destLen > 0);
}

Ну все. Больше каких-либо режимов подлости в фотошоповском документе на данный момент не выявлено.
Распакованные байты комбинируются нехитрым образом в пикселы, и мы имеем картинку, содержащую в себе целиком все изображение из psd.
В результате такого длительного секса с psd форматом на свет появился этот инструмент: PsdSplitter
Пользуйтесь на радость себе и людям.

psd,


Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Powered by WordPress. Designed by elogi.