Кто разбирается во внутренностях PDF, подскажите

yolki

Я тут делаю небольшой FAQ по поводу рисования картинок в формате PDF.
полученный PDF почему-то читается не всеми.
evince, foxit - читают,
ghostscript, adobereader - не читают
это обычный текстовый файл, можно посмотреть блокнотом.
рисунок - две окружности и кардиоида.

yolki

собственно, mini pdf FAQ:
Mini PDF FAQ
------------
Основные сведения
=================
PDF-файл начинается с сигнатуры %PDF-a.b
где a.b - версия формата, например 1.4
PDF-файл заканчивается маркером %%EOF
для того, чтобы некоторые программы не подумали, что обрабатываемый файл чисто
текстовый, второй строкой в PDF-файле ставят что-то вроде такого:
%Çì<8f>¢
или другие не-ASCII символы
Структура PDF-документа
=======================
По сути PDF-файл является текстовым файлом, однако в нём бывают бинарные
потоки. Строки, начинающиеся с процента (% считаются комментариями и
игнорируются (кроме специальных случаев, например %%EOF).
Формат данных в PDF-файле весьма похож на программу на некотором языке
программирования. Это верно, хотя язык этот весьма специфический.
В этом языке присутствуют объекты следующих типов:
* Булевые: true или false
* Целые: 341
* Вещественные: 3.1415
* Строковые, строки в PDF заключаются в круглые скобки: (Hello world!)
* Имена объектов, начинаются со слеша: /Length /Name /Version
* Шестнадцетиричные последовательности: <19AEB0D0CF11E>
* Массивы, заключаются в квадратные скобки: [ 3.14 -8 (Ralph) [ (Cotton) /Eye
 (Joe) ] ] - вложенные массивы тоже возможны, массивы могут состоять из
 разнородных значений
* Словари, массив пар <имя> <значение>, обрамляются двойными << >>:
<< /Type /Example
/Subtype /DictionaryExample
/Version 0 . 01
/IntegerItem 12
/StringItem ( a string )
/Subdictionary << /Item1 0 . 4
/Item2 true
/LastItem ( not ! )
/VeryLastItem ( OK )
>>
>>
* Потоки (streams):
<< Словарь >>
stream
...
endstream
Словарь для потока должен содержать по крайней мере:
/Length - целое число, длина потока.
Дополнительные параметры в словаре потока:
/Filter - указывает, что к потоку нужно применить фильтр. Например,
/Filter /FlateDecode - поток был сжат алгоритмом Deflate (см. zlib) и его нужно
распаковать.
* Неявные объекты (*indirect object*):
N M obj
...
endobj
Здесь N и M --- целые числа: N - идентификатор объекта, M - его поколение или
версия (в английской документации используется термин ``generation'').
На неявный обхект можно сослаться при помощи конструкции N M R, например:
6 0 R
В целом структуру PDF-файла можно описать так:
[Заголовок]
[тело]
[xref]
[трейлер]
[%%EOF]
Заголоввок --- это %PDF-n.m
Тело состоит из последовательности неявных объектов.
В секции xref содержится информация, где начинается каждый неявный объект
(типа содержания). В трейлере указывается на корневой объект, который и
определяет весь документ.
Фрагмент документа:
5 0 obj
<</Length 6 0 R/Filter /FlateDecode>>
stream
x<9c>+T0Ð3T0^@A(<9d><9c>Ë¥^_d®<90>^Ìeh PÎe àÎ^E^RÎ^Er^M^L^TÀD^NW^FW0P4<9d>+<90>^K^@§»^L£
endstream
endobj
6 0 obj
57
endobj
Здесь приведены два неявных объекта, (5 0) и (6 0). 5 0 --- это объект, содержащий
поток. Объект состоит из словаря (в котором указана длина потока и необходимый
фильтр) и собственно потока.
В объекте 6 0 содержится одно целое число - 57, это длина потока. На неё есть
ссылка в объекте 5 0: /Length 6 0 R .
Секция xref выглядит следующим образом:
xref
0 10
0000000000 65535 f
0000000350 00000 n
0000001879 00000 n
0000000291 00000 n
0000000160 00000 n
0000000015 00000 n
0000000142 00000 n
0000000414 00000 n
0000000455 00000 n
0000000484 00000 n
Здесь указывается: 0 - номер первого объекта в секции, 10 - количество
объектов в секции, следующие объекты имеют последовательные номера (начиная с
0). Далее идут адреса объектов в файле. Первым идёт специальный ``нулевой''
объект, с поколением 65535 и признаком 'f'.
Формат адресов такой:
NNNNNNNNNN GGGGG t
где NNN...NNN - 10-значное число, указывающее позицию в файле, откуда
начинается описание объекта, GGGGG - поколение объекта, t указывает, является
ли объект действующим неявным объектом ('n') или он освобождён, ``удалён''
('f').
Секция трейлера состоит из слова 'trailer' и последующего словаря:
trailer
<< /Size 10 /Root 1 0 R /Info 2 0 R
/ID [<ECBE2A649574C9AE3B290F2F003D1249><ECBE2A649574C9AE3B290F2F003D1249>]
>>
после словаря записывается слово 'startxref' и целое число, означающее позицию
начала секции xref в файле:
startxref
2002
В словаре трейлера должны присутствовать следующие значения:
/Size NNN --- указывает количество элементов в таблице xref
/Root <ссылка> --- ссылается на корневой объект документа
дополнительные, необязательные элементы:
/Info <ссылка> --- ссылается на информацию о документе (кто делал, когда, при
помощи чего и т.п.)
/ID массив из двух hex-значений --- необязательное поле, но очень
рекомендуется его указывать. Некоторый идентфикатор документа.
Структура объектов PDF-документа
================================
В трейлере указывается ссылка на корневой объект документа, например:
/Root 1 0 R
данный объект называется Каталогом документа (Document Catalog). Этот объект
должен содержать словарь, например:
1 0 obj
<<
/Type /Catalog
/Pages 3 0 R
/Metadata 9 0 R
>>
endobj
В этом словаре должны быть следующие поля:
/Type /Catalog --- указывает, что данный словарь является Каталогом.
/Pages <ссылка> --- ссылка на объект, содержащий описание страниц
Необязательное поле /Metadata <ссылка> --- ссылается на метаинформацию
(например, туда следует записывать кто делал PDF-документ, какой программой,
когда и т.п.). Это поле дополняет информацию из /Info.
Объект Pages --- это словарь, имеет следующую структуру:
3 0 obj
<<
/Type /Pages
/Kids [ 4 0 R ]
/Count 1
>>
endobj
Указывается тип (/Type /Pages указывается массив ссылок на страницы ---
/Kids [ массив ], указывается количество страниц --- /Count 1.
В этом примере одна страница.
Объекты, указанные в массиве /Kids имеют следующую структуру:
4 0 obj
<<
/Type /Page
/MediaBox [0 0 100 100]
/Parent 3 0 R
/Resources
  <<
    /ProcSet [ /PDF ]
    /ExtGState 8 0 R
  >>
/Contents 5 0 R
>>
endobj
Это словарь, обязательные поля которого:
/Type /Page --- указывает, что тип объекта --- страница
/MediaBox <массив из 4 чисел> --- указывает границы страницы в пунктах (1
пункт = 1/72 дюйма)
/Parent <ссылка> --- указывает родительский на объект типа Pages.
/Resources << словарь >> --- дополнительные ресурсы для страницы
/Contents <ссылка> --- ссылка на поток, содержащий команды отрисовки страницы.
Секция /Resources содержит словарь, в котором описаны дополнительные ресурсы,
необходимые для отрисовки страницы. Здесь, в частности, содержатся ссылки на
шрифты. Например:
/Resources
<<
   /Font
   <<
/F3 7 0 R
/F5 9 0 R
/F7 11 0 R
   >>
   ...
>>
(Отрисовка текста выходит за рамки этого mini-FAQ.)
Одно из полей соваря /Resources --- это массив /ProcSet. В нём
указывается набор процедур и функций, используемых для отрисовки страницы.
Употребляются следующие наборы (``Sets''):
/PDF --- для

okis

Вопрос не совсем в тему (во внутренностях не очень шарю): цель писать сразу pdf? Может стоит написать скрипт asymptote, или сделать svg, а из него потом pdf?

slonishka

из ps-а логичней наверное только. ps - вообще классика жанра.

yolki

про ps знаю, ага.
цель - именно pdf.
ладно, забьём. сделаю для eps

dsl-san1

Разобрался. .
Спецификацию брал отсюда:
 Adobe PDF Reference Archives | Adobe Developer Connection
 прямая ссылка на pdf.
Основная ошибка: на странице 154 в таблице 3.30 сказано:

TABLE 3.30 Entries in a resource dictionary








KEYTYPEVALUE

ExtGState
dictionary(Optional) A dictionary that maps resource names to graphics state parameter dictionaries (see Section 4.3.4, “Graphics State Parameter Dictionaries”).

На странице 223 есть пример:

20 0 obj% Resource dictionary for page
<< /ProcSet [ /PDF /Text ]
/Font << /F1 25 0 R >>
/ExtGState << /GS1 30 0 R
/GS2 35 0 R
>>
>>
endobj
30 0 obj% First graphics state parameter dictionary
<< /Type /ExtGState
/SA true
/TR 31 0 R
>>
endobj

Соответственно, в твоем файле можно

3 0 obj << /Type /Page /MediaBox [ 0 0 400 400 ] /Parent 2 0 R
/Resources << /ProcSet [ /PDF ] /ExtGState 4 0 R >>
/Contents 5 0 R >> endobj

заменить на

3 0 obj << /Type /Page /MediaBox [ 0 0 400 400 ] /Parent 2 0 R
/Resources << /ProcSet [ /PDF ] /ExtGState << /GS1 4 0 R >> >>
/Contents 5 0 R >> endobj

и, конечно, исправить оффсеты того, что дальше по тексту. Имена ресурсов (здесь - GS1) можно, как я понимаю, брать до определенной степени произвольно, в дальнейшем они вызываются оператором gs.
Вторая ошибка, менее серьезная, в xref. На странице 94 сказано:
Following this line are the cross-reference entries themselves, one per line. Each entry is exactly 20 bytes long, including the end-of-line marker.
...
If the file’s end-of-line marker is a single character (either a carriage return or a line feed it is preceded by a single space; if the marker is 2 characters (both a carriage return and a line feed it is not preceded by a space.
У тебя однобайтовый (LF) перенос строки, поэтому я добавил пробелы. Если оставить как есть, то ghostscript выдает предупреждение об ошибке в xref, а Adobe Reader при закрытии предлагает сохранить файл (и при согласии сохраняет, накидав метаданных, ужав потоки...). Файл, однако, просматривается без проблем.
И последнее, что подметил, но не исправлял. В твоем файле есть строка

4 0 obj << /Type /ExtGState /OPM 1 /ColorSpace /DeviceRGB>> endobj

Согласно таблице 4.8 на страницах 220-223 параметра с именем ColorSpace в словаре типа ExtGState не предусмотрено. Однако он есть в вышестоящем словаре ресурсов (согласно таблице 3.30 на странице 154 и ему, как и уже упомянутому ExtGState, должен соответствовать словарь... Так как открывается нормально, то программы могут просто игнорировать неизвестный параметр, если же он важен для корректного отображения, стоит исправить.
Ты, похоже, по более раннему стандарту писал. Возможно, в файле есть и еще какие-то расхождения с нынешним.

yolki

о! большое спасибо!
я писал по PDF reference 1.7
да, до 20-байтового xref я позднее дочитал, когда более внимательно анализировал, что же оно хочет.

dsl-san1

Я тоже 1.7 имел в виду (по ссылке на pdf видно, только сейчас заметил, что на веб-странице и другие версии есть). Впрочем, если исправить только версию pdf в начале файла, открываться он не начинает. Интересно, Evince и Foxit просто пропускают некорректно сформированный параметр или же понимают его?

chriselwart

Я в свое время весьма долго игрался с docbook и замечательно делал из него pdf-ки. Тоже вроде как весьма красивое и простое решение.

yolki

в DocBook можно картинки рисовать, по данным?

chriselwart

В принципе да.
Я правда использовал его для оформления документации по ГОСТ 19. Но рамочки весьма зачетные получались.

yolki

А можешь в двух словах указать, куда копать, чтобы построить например такое:
есть массив данных функции f(x,y заданной на некоторой двумерной области.
ролик на ютубе
требуется построить её "изображение", плюс линии уровня.
ну чтобы получилось нечто вроде такого :

это изображения разных функций (они получены из перпендикулярных уравнений но у меня нет под рукой нормальных, а готовить влом.

bav46

мб глупый вопрос, а принципиально рисовать в сам пдф, он разве картинки не умеет содержать?

yolki

вопрос тут слегка в другом.
вот у тебя ворох чисел - как отрисовать картинки?
запощенная картинка получена именно генерацией PDF-документа из данных

bav46

вот у тебя ворох чисел - как отрисовать картинки?
я бы рисовал в битмап но никак не в пдф

tokuchu

я бы рисовал в битмап но никак не в пдф
Ну вектор всё же лучше. Хотя мне казалось, что для картинок есть что-то более удобное. А его уже можно в pdf сконвертить наверняка.
Кстати, там же вроде в cairo что-то подобное экспорту в pdf есть, т.е. как я понял там можно нарисовать картинку и сохранить или отобразить это во много что, в том числе и в pdf.

yolki

фи, в растр.
не-не, векторные лучше.
тем более, чтобы в растр рисовать нужно больше машграфа взботнуть (Брезенхем, Гуро и т.п.)

chriselwart

Боюсь, что не подскажу, потому что не знаю. Я бы такое тупо в матлабе нарисовал и экспортнул.

kill-still

тем более, чтобы в растр рисовать нужно больше машграфа взботнуть (Брезенхем, Гуро и т.п.)
а взботнуть GDI не проще ли будет?

yolki

ты предлагаешь на винапи на девайсконтексте рисовать? и потом сделать битмап?
фи-буэ...
и что, GDI позволяет одним оператором по Гуро красить?
в PDF - ага, можно.
задаём цепочку вершин с указанием цвета в каждой, и одной командочкой закрашиваем.
формат примерно такой:
x y r g b
x y r g b
x y r g b
...
fill

tokuchu

и что, GDI позволяет одним оператором по Гуро красить?
Ну с GDI он загнул, наверное. А других либ разве нет, которые такое уже умеют. Я вон cairo упоминал. Я сам не пользовался, но у меня подозрение есть, что она как раз для подобных применений и нужна. :)

yolki

Заливка непрерывным изменяемым цветом по Гуро
=============================================
Данный способ заливки применяется в случае, если требуется залить непрерывным
цветным градиентом произвольную область. Заливка осуществляется по
элементам-треугольникам. Это так называемый четвёртый тип заливки (Type 4
shading).
Для этого потребуется определение шаблона заливки (shading pattern) и
собственно заливки (shading).
Словарь шаблона должен быть таким:

12 0 obj
<<
/PatternType 2
/Shading 13 0 R
/Matrix [ 1 0 0 1 0 0 ]
>> endobj

Для заливки определяется следующий словарь:
[/code]
13 0 obj
<<
/ShadingType 4
/ColorSpace /DeviceRGB
/Decode [ minx maxx miny maxy minr maxr ming maxg minb maxb ]
/BitsPerCoordinate 24
/BitsPerComponent 16
/BitsPerFlag 8
/Length 39
stream
...
endstream
>> endobj
[/code]
Здесь minx,maxx, miny,maxy --- минимальные и максимальные координаты
треугольников. min[rgb] max[rgb] --- минимальные и максимальные значения
компонентов цветности. Обычно массив Decode выглядит так:
/Decode [ -16384 16384 -16384 16384 0 1 0 1 0 1 ]
/BitsPerCoordinate --- задаёт количество бит, используемых для представления
координат.
/BitsPerComponent --- задаёт количество бит, используемых для представления
компонент цветности
/BitsPerFlag --- задаёт количество бит, используемых для флага треугольника.
Флаг указывает способ образования треугольников.
0 --- новый треугольник из трёх вершин: a b c
1 --- добавляется одна новая вершина d, две недостающие берутся из предыдущего
определения треугольника (b c d)
2 --- одна новая вершина d, но берутся другие вершины: a c d
/Length --- определяет длину потока.
в потоке stream ... endstream записаны двоичные данные, определяющие
координаты и цвета вершин треугольников в следующем формате:
f x y r g b
f x y r g b
Например, поток:

00 80 c8 00 80 c8 00 ff ff ff ff 00 00 00 80 00
00 80 00 00 00 00 ff ff ff ff 00 82 58 00 80 00
00 ff ff 00 00 ff ff

Задаёт такие значения:
0 200 200 1 1 0
0 0 0 0 1 1
0 600 0 1 0 1
На координату приходится 24 бита или 3 байта. Минимальное число --- 0x000000
соответствует -16384, максимальное число --- 0xffffff соответствует 16384.
Поэтому числу 0x80c800 соответствует 200, а 0x825800 --- 600.
Чтобы применить такую заливку необходимо создать словарь со ссылкой на неё:

20 0 obj
<<
/MyShading 12 0 R
>>
endobj

и в потоке отрисовки страницы вызвать закраску всей страницы выбранным
способом:

/MyShading scn
0 0 600 850 re
f

Ниже приведён пример программы, генерирующий PDF-файл, в котором полоса
раскрашена в цвета, похожие на спектр.
*file:spectrpdf.c*

#include <stdio.h>
#include <math.h>

#define OBJ_FREE 0
#define OBJ_CATALOG 1
#define OBJ_PAGES 2
#define OBJ_PAGE 3
#define OBJ_EXTGSTATE 4
#define OBJ_CONTENTS 5
#define OBJ_CONTENTLENGTH 6
#define OBJ_PATTERN 7
#define OBJ_SHADING 8
#define OBJ_SHADESTREAMLEN 9
#define OBJ_PATTERNREF 10

#define OBJECT_COUNT 11

// Функция записывает команды отрисовки страницы
// Возвращает количество записанных байтов
int WritePageStream(FILE *fp);
// Функция записывает поток координат полосы со спектром
// Возвращает количество записанных байтов
int WriteSpectreBar(FILE*fp);
int main(void)
{
int ObjectStarts[OBJECT_COUNT];
int StreamLength,i,xrefstart;
FILE *fp;

ObjectStarts[0]=0;
// Открываем файл для записи
fp=fopen("spectrum.pdf","w");
fprintf(fp,"%%PDF-1.4\n");
fprintf(fp,"%%%c%c%c%c\n",0xc7,0xec,0x8f,0xa2);
// Записываем ссылку на способ заливки
ObjectStarts[OBJ_PATTERNREF]=ftell(fp);
fprintf(fp,"10 0 obj << /MyShading 7 0 R >> endobj\n");
// Сохраняем начало объекта DocumentCatalog
ObjectStarts[OBJ_CATALOG]=ftell(fp);
fprintf(fp,"1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n");
// Сохраняем позицию начала объекта Pages
ObjectStarts[OBJ_PAGES]=ftell(fp);
fprintf(fp,"2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj\n");
// Сохраняем позицию начала объекта Page
ObjectStarts[OBJ_PAGE]=ftell(fp);
fprintf(fp,"3 0 obj << /Type /Page /MediaBox [ 0 0 400 400 ] /Parent 2 0 R\n");
fprintf(fp,"/Resources << /ProcSet [ /PDF ] /ExtGState << /GS1 4 0 R >> /Pattern << /MyPat 7 0 R>> /Shading << /MyShading 8 0 R >> >>\n");
fprintf(fp,"/Contents 5 0 R >> endobj\n");
// Сохраняем позицию начала объекта EXTGSTATE
ObjectStarts[OBJ_EXTGSTATE]=ftell(fp);
fprintf(fp,"4 0 obj << /Type /ExtGState /OPM 1 >> endobj\n");
// Сохраняем позицию начала объекта CONTENTS
ObjectStarts[OBJ_CONTENTS]=ftell(fp);
fprintf(fp,"5 0 obj << /Length 6 0 R >>\n");
fprintf(fp,"stream\n");
StreamLength=WritePageStream(fp);
fprintf(fp,"\nendstream\nendobj\n");
// Сохраняем позицию начала объекта CONTENT_LENGTH
ObjectStarts[OBJ_CONTENTLENGTH]=ftell(fp);
// Записываем длину потока
fprintf(fp,"6 0 obj %d endobj\n",StreamLength);
// Создаём шаблон заливки
ObjectStarts[OBJ_PATTERN]=ftell(fp);
fprintf(fp,"7 0 obj << /PatternType 2 /Shading 8 0 R >> endobj\n");
ObjectStarts[OBJ_SHADING]=ftell(fp);
fprintf(fp,"8 0 obj << /ShadingType 4 /ColorSpace /DeviceRGB /Decode [0 65535 0 65535 0 1 0 1 0 1 ]\n");
fprintf(fp,"/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Length 9 0 R>>\n");
fprintf(fp,"stream\n");
StreamLength=WriteSpectreBar(fp);
fprintf(fp,"\nendstream\nendobj\n");
ObjectStarts[OBJ_SHADESTREAMLEN]=ftell(fp);
// Записываем длину потока
fprintf(fp,"9 0 obj %d endobj\n",StreamLength);
// Записываем xref
xrefstart=ftell(fp);
fprintf(fp,"xref\n0 %d\n", OBJECT_COUNT);
for(i=0;i<OBJECT_COUNT;i++)
{
fprintf(fp,"%010d %05d %c \n",ObjectStarts[i],i==0?65535:0,i==0?'f':'n');
}
// Записываем trailer
fprintf(fp,"trailer\n");
fprintf(fp,"<< /Size %d /Root 1 0 R >>\n", OBJECT_COUNT);
// Записываем startxref
fprintf(fp,"startxref\n%d\n",xrefstart);
// Записываем маркер %%EOF
fprintf(fp,"%%%%EOF\n");
fclose(fp);
}

int WritePageStream(FILE *fp)
{
int curpos=ftell(fp);
// Для начала сохраним текущее состояние, зададим параметры масштаба
fprintf(fp,"q\n1 0 0 1 0 0 cm\n");
fprintf(fp,"/GS1 gs\n");
// Устанавливаем заливку
fprintf(fp,"/Pattern cs /MyPat scn\n");
// Закрашиваем всю страницу
fprintf(fp,"0 0 400 400 re\n"); // re --- прямоугольник
fprintf(fp,"f\n");
fprintf(fp,"Q\n");
curpos=ftell(fp)-curpos;
return curpos;
}

//
// Запись координат треугольников спектра
// Вся страница: 400х400 пунктов, полоска: 400х50, посередине страницы, с 200..250
// цвета:
// каждый (1,0,0) 0
// охотник (1,0.5,0) 50
// желает (1,1,0) 100
// знать (0,1,0) 150
// где (0,1,1) 200
// сидит (0,0,1) 250
// фазан (1,0,1) 300
//
// треугольники задём так:
// a ---- c ---- e y
// | / | /| ....
// | / | / |
// |/ | / |
// b ---- d ---- f z
// всего 14 точек

// ширина треугольника --- 50, длина спектра -- 350
#pragma pac

Serab

Все-таки хочется тогда поднять еще раз вопрос , почему бы не воспользоваться специально для этого созданными инструментами? Классика — metapost, поновее, довольно круто — asymptote, ну да, они через ps (по-другому не делал но разве это так плохо?

yolki

У меня самый простой ответ такой: гляделка PDF более распространена, чем PS.
У меня есть аналогичный FAQ по постскрипту, но мне его оформлять лень.
Наверное всё-таки оформлю, по-моему текст в PS добавить проще, чем в PDF - меньше заморочек со шрифтами.
в метапосте не всё так гладко, в ассимптоте тоже ;)

tokuchu

У меня самый простой ответ такой: гляделка PDF более распространена, чем PS.
ps2pdf :)

conv3rsje

Присоединяюсь к 'у.
Посмотри на досуге на cairo, штука весьма почетная.
Кроме пдфа умеет еще рисовать в svg.
Я, правда, пользовался ей только для рисования на экране
Но вполне удовлетворен
Оставить комментарий
Имя или ник:
Комментарий: