Логотип EPS, размещённый на странице PDF, может выйти с пропавшим контуром, одноцветный знак может напечататься собственным негативом, а Æ может обернуться á. Каждый симптом отмечает место, где семантика графики PostScript и PDF расходится, поэтому импорт EPS в PDFlibPas для Delphi интерпретирует программу PostScript и выписывает каждую разницу нативными операторами PDF
Интерпретировать программу или пропустить её насквозь
PostScript может сидеть внутри PDF двумя способами, и лишь один из них что-то показывает на экране. ISO 32000-1 §8.8.2 определяет PostScript XObject — поток /Subtype /PS, чьи байты копируются, не интерпретируясь, в печатное задание только когда устройство вывода — PostScript-принтер; везде в остальном он не действует, и обзор §8.8 говорит, что PostScript XObject использовать не следует. PDF/A исключает их вовсе. TPDFlib.NewPostScriptXObject и DrawPostScriptXObject пишут ту сквозную форму, тогда как ImportEPSFromFile и DrawPostScript прогоняют программу через интерпретатор и выпускают обычные пути, инлайн-изображения и текст. Рисунки гоняйте через интерпретатор, а PS XObject придержите для фрагмента, нацеленного на RIP конкретной печатной комнаты — растровый процессор перед печатной машиной или производственным принтером. Интерпретация EPS — ещё и работа иная, чем векторный импорт EMF, где вход — список GDI-записей на повторение; файл EPS — программа
procedure AddPrintRoomHook(PDF: TPDFlib);
var
PSRef: Integer;
begin
// Хранится дословно как поток /Subtype /PS и упоминается через Do.
// Исполняется только на PostScript-принтере; вьюверы не рисуют ничего
PSRef := PDF.NewPostScriptXObject(
'userdict /PrintRoomMark known { PrintRoomMark } if');
if PSRef <> 0 then
PDF.DrawPostScriptXObject(PSRef);
end;
| Поведение PostScript | Поведение PDF | Результат буквального перевода |
|---|---|---|
gsave / grestore сохраняют и восстанавливают текущий путь и текущую точку | q / Q путь не трогают, а отрисовка его завершает (§8.5.1) | Контур или продолжающий сегмент после grestore исчезает |
clip сохраняет текущий путь для позднейшей отрисовки | За W следует оператор отрисовки, обычно n, который завершает путь | Обводке после clip нечего рисовать |
ImageMatrix отображает пространство пользователя в пространство изображения | Каждое изображение заполняет единичный квадрат, первая строка наверху (§8.9.4) | Изображения снизу-вверх или повёрнутые ложатся отражёнными или смещёнными |
true imagemask рисует биты 1 | Трафарет с дефолтным Decode [0 1] рисует биты 0 (§8.9.6.2) | Одноцветный логотип печатается собственным негативом |
| Романские шрифты Standard-14 по умолчанию на StandardEncoding | Имя StandardEncoding особого смысла не имеет (§9.6.6.1) | Акценты, кавычки и лигатуры меняют буквы |
Комментарии %%BoundingBox и %%HiResBoundingBox определяют область рисунка | Размещение — какая матрица ни выйдет из писателя | Рисунок съезжает или масштабируется мимо задуманного бокса |
Куда делся контур после grestore?
Контур исчез, потому что PDF путь никогда не сохранял. ISO 32000-1 §8.5.1 формулирует это одним предложением: «В PDF (в отличие от PostScript) текущий путь не является частью состояния графики», и после отрисовки пути текущего пути нет, пока m или re не начнёт новый. Производители PostScript опираются на противоположное правило. Обычный способ нарисовать заполненную фигуру с контуром — gsave fill grestore stroke: заливка потребляет сохранённую копию пути, grestore возвращает путь, и обводка рисует ту же геометрию. Отобразите gsave в q, fill в f и grestore в Q — и обводка приходит с пустым путём, так что фигура заполнена, а контура нет. lineto после grestore, продолжающий восстановленный путь, проваливается так же — как пропавший сегмент
uses
SysUtils, PDFlibrary;
const
// Заполненный треугольник с контуром: заливка идёт внутри gsave/grestore,
// а обводка после grestore переиспользует восстановленный путь
TriangleEPS =
'%!PS-Adobe-3.0 EPSF-3.0'#10 +
'%%BoundingBox: 0 0 200 120'#10 +
'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
'gsave 0.85 setgray fill grestore'#10 +
'3 setlinewidth 0 setgray stroke'#10;
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // Left и Top отмеряются от верхне-левого угла
// Width и Height по 0 сохраняют размер ограничивающего бокса 200 x 120
if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
PDF.SaveToFile('triangle.pdf')
else
Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
// Ожидается: светло-серый треугольник с чёрным контуром в 3 единицы.
// Буквальный перевод q/f/Q/S заполняет треугольник и теряет контур
finally
PDF.Free;
end;
end.
PDFlibPas держит собственный путь PostScript, отдельно от того, что в данный момент несёт поток содержимого PDF, и пишет путь PDF только когда что-то рисует или отсекает. Каждая точка фиксируется под матрицей преобразования, действовавшей в момент, когда исполнялись moveto или lineto, — как в PostScript, где позднейший scale или concat никогда не двигает точки, уже лежащие на пути. currentpoint отображает сохранённую точку обратно через текущую матрицу и потому отвечает в текущем пространстве пользователя. gsave записывает ссылку на конец цепочки пути плюс текущую точку, матрицу, шрифт и цветовое пространство, поэтому вложенные сохранения никогда не копируют пути целиком
Отсечение — то место, где две модели пути снова расходятся, и импорт следует здесь за PostScript: clip и eoclip выпускают W n для стороны PDF, но оставляют логический путь на месте для обводки, которая часто следует, тогда как rectclip, начинающий в PostScript свежий пустой путь, выпускает re W n и его очищает. Хранение пути ограничено 100 000 узлов, а повторения расходуют общий бюджет операций, так что враждебный файл не сможет зациклить gsave и stroke, чтобы раздуть вывод
Размещение изображений: ImageMatrix смотрит в другую сторону
Изображение EPS ложится отражённым, когда его ImageMatrix обращают как размещение, потому что тот описывает противоположное отображение. В PDF §8.9.4 привязывает границу изображения к единичному квадрату пространства пользователя с верхней строкой отсчётов наверху — неявная матрица изображения-в-пользователя [1/w 0 0 -1/h 0 1]. ImageMatrix PostScript отображает пространство пользователя в пространство изображения. Матрица размещения PDF поэтому — [w 0 0 -h 0 h], умноженная на обратную к ImageMatrix. Для обычной матрицы сверху-вниз [w 0 0 -h 0 h] произведение — тождество. Вот почему наивные конвертеры выглядят правыми в бытовом употреблении. Скан снизу-вверх, записанный с [w 0 0 h 0 0], даёт [1 0 0 -1 0 1] — вертикальное отражение внутри единичного квадрата, — а повёрнутым или скошенным матрицам особый случай не нужен. Когда конечный источник данных кончается рано, PDFlibPas рисует только полные строки, сохраняет исходный ImageMatrix и масштабирует по строкам, которые реально пришли, так что усечённое изображение заполняет верхнюю полосу своей области в верном масштабе; растяжение уцелевших строк на всю высоту дало бы искажённую картину, выглядящую целой
Какой бит рисует imagemask?
С true в качестве операнда полярности imagemask рисует биты 1, а в PDF это Decode [1 0]. ISO 32000-1 §8.9.6.2 определяет [0 1] как дефолт для маски изображения: отсчёт 0 метит страницу текущим цветом, а 1 оставляет фон в покое, и [1 0] это переворачивает. Булево PostScript говорит, какой отсчёт рисует. Прочтите его как «переверни дефолт PDF» — и логотип напечатается негативом, сплошным блоком с вырезанным из него знаком. PDFlibPas отображает true в [1 0], а false в [0 1], выпускает трафарет как инлайн-изображение с /IM true, чтобы тот рисовался тем цветом, который выставила программа, и в словарной форме принимает только те два массива Decode
Двенадцатибитные отсчёты прячут несоответствие поменьше. У PDF нет 12-битных изображений; §8.9.5.1 допускает 1, 2, 4, 8 и, с PDF 1.5, 16 бит на компонент. Сдвиг каждого отсчёта влево на четыре бита укладывает его в 16 бит без потерь, но наибольшее значение становится 65520, а не 65535, поэтому с дефолтным Decode каждый яркий отсчёт выходит примерно на 0,023 процента темнее. Импорт сохраняет сдвиг и растягивает верхнюю точку Decode на 65535/65520, что возвращает каждое нормализованное значение. 12-битное изображение само поднимает версию вывода до PDF 1.5; в режиме PDF/A или PDF/X, либо когда версия вывода заперта ниже 1.5, импорт останавливается с ошибкой 538 прежде, чем страница изменится
Почему Æ выходит как á?
Романский шрифт в PostScript использует StandardEncoding, пока программа его не перекодирует, а PDF назвать эту таблицу не может: ISO 32000-1 §9.6.6.1 не даёт имени особого смысла, оставляя WinAnsiEncoding, MacRomanEncoding и MacExpertEncoding предопределёнными вариантами. Таблицы приложения D.2 показывают, что ломается, когда коды передаются WinAnsi нетронутыми: восьмеричное 341 (0xE1) в StandardEncoding — AE, а в WinAnsi — aacute, поэтому Æ становится á; 0xAE — лигатура fi в одном и ® в другом; 0x27 — quoteright в StandardEncoding и прямая quotesingle в WinAnsi, что превращает типографские апострофы в пишуще-машинные. Для каждого романского шрифта Standard-14, который использует программа, PDFlibPas пишет приватный ресурс шрифта Type 1, чей словарь /Encoding имеет /BaseEncoding /WinAnsiEncoding и массив /Differences, называющий все 256 кодов в порядке StandardEncoding, поэтому ни один код не зависит от того, как вьювер заполнит свою базовую таблицу. Ширины приходят из той же кодировки, карта ToUnicode позволяет копированию и поиску возвращать Æ, а Symbol и ZapfDingbats сохраняют встроенные коды. Шрифт, который ваш код выбрал до импорта, остаётся нетронутым, ведь перекодировка кэшированного шрифта под EPS изменила бы текст, уже набранный им
Те ресурсы называют шрифты, не встраивая их, что сталкивается с профилями, требующими встраивания. В документе, настроенном на PDF/A, PDF/X или PDF/UA, или после SetEmbedAllFonts(1) текст EPS валится с ошибкой 538, и страница остаётся как была. Для архивного вывода более чистый путь — EPS, экспортированный с текстом, переведённым в кривые: он импортируется как простые пути в любом режиме; шрифтам, уже оставленным невстроенными в существующем файле, нужно вместо этого встраивание недостающих шрифтов для PDF/A. Текст EPS отображается на 14 стандартных шрифтов по их точным именам; рисунок, встраивающий собственную программу шрифта, импортируется чисто, как только его текст переведён в кривые, — заодно это удерживает локальный шрифт-замену от сдвига каждой строки
Ограничивающие боксы, DSC-комментарии и двоичные файлы EPS
Размещение масштабирует ограничивающий бокс рисунка в прямоугольник, который вы передаёте, поэтому бокс обязан приходить из правильного места. PDFlibPas читает %%BoundingBox и %%HiResBoundingBox только из настоящих строк DSC-комментариев — комментариев верхнего уровня, начинающих строку, — и корректный бокс высокого разрешения побеждает целочисленный. Простой поиск подстроки находит те ключевые слова и внутри литеральных строк, тел процедур и двоичных данных изображений. Файлы EPS несут все три. Значение заголовка (atend) откладывает числа до комментария в трейлере, читаемого, когда сканер до него добирается; файл вовсе без бокса откатывается к 612 на 792, а пустой или перевёрнутый бокс валится с ошибкой 124. Width и Height масштабируют бокс независимо: передавайте 0 для обоих, чтобы сохранить естественный размер, или передавайте оба в пропорции бокса, потому что один ненулевой растягивает рисунок только по одной оси
Двоичные данные решают точку входа. DrawPostScript принимает WideString — хорошо для PostScript, построенного в коде, — но сырые отсчёты изображений, данные Flate, читаемые через currentfile, и обёртка DOS EPS содержат байты, не выживающие в путешествии через юникодный текст. ImportEPSFromFile читает файл как сырые байты, распознаёт заголовок предпросмотра C5 D0 D3 C6, проверяет, что смещение и длина сегмента PostScript лежат внутри файла, и исполняет только тот сегмент, игнорируя предпросмотр TIFF или WMF. Оба вызова принимают не более 4 МиБ источника, требуют Options = 0, возвращают 1 или 0 и дописывают либо весь рисунок, либо ничего. Причина отказа сидит в LastErrorCode, который сбрасывается при чтении, поэтому снимите его один раз
uses
SysUtils, PDFlibrary;
function PlaceEPS(PDF: TPDFlib; const FileName: string;
Left, Top: Double): Boolean;
var
Code: Integer;
begin
// 0, 0: естественный размер из %%HiResBoundingBox или %%BoundingBox
Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
if Result then
Exit;
Code := PDF.LastErrorCode; // чтение очищает код, поэтому снимите его один раз
case Code of
402:
Writeln(FileName, ': file cannot be opened');
PDFLIB_ERROR_POSTSCRIPT_INVALID: // 124
Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED: // 538
Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
PDFLIB_ERROR_POSTSCRIPT_BUDGET: // 539
Writeln(FileName, ': source over 4 MiB or a work limit reached');
else
Writeln(FileName, ': import failed with code ', Code);
end;
end;
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1);
if PlaceEPS(PDF, 'logo.eps', 40, 40) then
PDF.SaveToFile('letterhead.pdf');
finally
PDF.Free;
end;
end.
Когда ошибка 538 называет оператор, реэкспортируйте рисунок из его редактора как простой EPS с текстом, переведённым в кривые: это обычно быстрее, чем охотиться за оператором, а рисунок в кривых размещается и в документе PDF/A или PDF/UA, собранном с PDFlibPas Delphi PDF Library, без конфликта шрифтов Standard-14