У плана этажа, экспортированного архитектурным CAD-пакетом, на первой странице обычно лежит массив /VP с двумя вьюпортами, и каждый вьюпорт несёт запись /Measure, чтобы вьювер мог снимать метры прямо с чертежа. У листа карты GeoPDF тот же массив /VP и тот же ключ /Measure. План этажа — обычный PDF 1.6. Листу карты нужен PDF 1.7 плюс Adobe Extension Level 3. Проверка версии, останавливающаяся на имени ключа, потребует расширение для обоих, и план этажа будет отвергнут при сохранении с сообщением о геопространственном содержимом, которого в нём никогда не было
Один ключ /Measure, два разных словаря
Имя ключа говорит, что вьюпорт измерим; только словарь за ним говорит, как именно. Словари Measure появились в PDF 1.6, и ISO 32000-1 §12.9 определяет прямоугольный сорт: строка масштабного соотношения в /R, массивы форматов чисел в /X и /Y и дальнейшие массивы для расстояния (/D) и площади (/A). Геопространственные измерения пришли позже, как часть Adobe Extension Level 3 поверх PDF 1.7. Их метка — /Subtype /GEO, и они несут географическую систему координат в /GCS, опциональную систему координат отображения в /DCS, парные массивы точек в /GPTS и /LPTS и предпочтительные единицы отображения в /PDU — это структуры, по которым проходит статья чтение вьюпортов GeoPDF и массивов GPTS и LPTS в Delphi
Деталь, на которой спотыкаются, — умолчание. §12.9 даёт /Subtype значение по умолчанию RL, и многие CAD-экспортёры этим пользуются, опуская запись. Проверка, читающая отсутствующий subtype как "неизвестно, предполагай худшее", превращает каждый такой чертёж в геопространственный документ. Проверка, совпадающая по ключу /Measure, до вопроса о subtype вообще не доходит, потому что словарь вьюпорта, держащий ключ, стоит уровнем выше словаря измерений, который на него отвечает
Как preflight сохранения в PDF Library for Delphi выносит вердикт
PDF Library for Delphi (PDFlibPas) прогоняет preflight версии внутри SaveToFile до записи какого-либо вывода. Он посещает каждый косвенный объект, спускаясь во вложенные словари и массивы, и сверяет каждый узел с таблицей правил фич. Каждое правило называет минимальную версию PDF и, для фич-расширений, уровень расширения Adobe; правилу расширения может удовлетворить только PDF 1.7 вместе с подходящей записью /Extensions /ADBE в каталоге. Цель, с которой он сверяется, — версия документа или версия, зафиксированная вами через LockSaveVersion, плюс какой бы то ни было уровень расширения, объявленный каталогом. Первое правило, которое и совпадает, и превышает эту цель, заканчивает сохранение: SaveToFile возвращает 0, а LastErrorCode становится 602 — значением PDFLIB_ERROR_VERSION_COMPLIANCE
Один код ошибки не говорит, что чинить. Три ключа GetInformation доносят остальное: 311 — версия, которой требует провинившаяся фича (например 1.7 ExtensionLevel 3), 312 — имя фичи, а 313 — версия, на которой зафиксирована цель сохранения, пустая, когда ничего не зафиксировано
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: требуемая версия, 312: фича, которая её вызвала,
// 313: версия, на которой зафиксирована цель сохранения ('' если не зафиксирована)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Для измерительного содержимого таблица держит три правила и намеренно не держит правила по голому ключу /Measure. Страница с /VP или /UserUnit требует PDF 1.6. Словарь /PtData требует уровня расширения 3. Словарь измерений требует уровня расширения 3, когда говорит /Subtype /GEO или содержит что-то из /GCS, /DCS, /GPTS, /LPTS или /PDU. Поэтому вьюпорт, чей словарь измерений опускает /Subtype, и вьюпорт с явным /RL оба проходят на 1.6, тогда как та же страница на 1.5 отвергается из-за /VP. Добавьте массив /GPTS — и ответ становится 1.7 ExtensionLevel 3
Проверка содержимого консервативна намеренно. Прямоугольный словарь, таскающий вдобавок лишний /GCS или /PDU, считается геопространственным. Мы предпочли бы отвергнуть такой файл, чем пропустить его махнув рукой: в прямоугольной модели эти ключи не значат ничего, поэтому писал их либо геопространственный инструмент, либо что-то, повредившее файл, а GEO-осведомлённый вьювер вполне может попытаться их интерпретировать. Загрузчик столь же аккуратен в другую сторону. Его скан фич может поднять номер версии документа до 1.7, но он никогда не пишет декларацию /Extensions от имени входного файла — заявить расширение значит сделать утверждение о содержимом, и делать его должен ваш код или вызывающие авторинг вызовы
Проверка вьюпортов чертежа перед сохранением
Когда 312 называет геопространственную фичу, а вы уверены, что файл — простой чертёж, осмотрите словари измерений сами. Публичных вьюпортных вызовов достаточно. GetMeasureDictCoordinateSystem читает /Subtype и возвращает 1 для RL, 2 для GEO и 0, когда запись отсутствует — что по §12.9 тоже означает прямоугольный случай. Счётчики точечных массивов скажут, есть ли вообще геопривязка
// Сообщает о каждом словаре измерений в загруженном документе.
// Все вызовы здесь только читают; ни один не добавляет ключей в файл.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = опущен (RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
Двух вызовов в том цикле нет намеренно: GetMeasureDictGCSDict и GetMeasureDictDCSDict. Эти два — аксессоры get-or-create, построенные для авторинга. На словаре измерений без записи /GCS GetMeasureDictGCSDict добавляет пустую и заявляет для документа уровень расширения 3. Вызовите её как пробу на прямоугольном плане этажа — и словарь теперь по-настоящему содержит /GCS; последующее сохранение с блокировкой на 1.6 будет отвергнуто, и правильно, за содержимое, которое только что написала ваша диагностика. Если нужно знать, существует ли система координат, subtype и счётчики точечных массивов отвечают на это без побочных эффектов
Блокировка вывода на PDF 1.6
Зафиксированная версия держит вывод честным, когда у ваших потребителей есть потолок. TPDFlib.LockSaveVersion принимает строки от '1.0' до '1.7' и возвращает 0 на всё остальное. Она фиксирует версию документа и не даёт вызовам со стороны писателя поднимать её самовольно, но preflight сохранения всё равно сверяется с зафиксированным значением. CAD-чертёж, зафиксированный на 1.6, сохраняется чисто. Настоящий GeoPDF, зафиксированный на 1.6, получает 602 — и это ответ, который вам нужен, ведь альтернатива — файл, чей заголовок обещает меньше, чем требует содержимое
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Настоящее содержимое выше 1.6, например GEO-словарь измерений
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
Pdf.UnlockSaveVersion;
end;
Прочитайте 311 и 312, прежде чем трогать версию. Если фича — настоящее геопространственное содержимое, у вас два честных выбора: оставить документ разблокированным на 1.7 с заявленным расширением или выбросить геопространственные данные из копии, уходящей потребителю только с 1.6. Поднимать блокировку лишь чтобы ошибка исчезла, — вариант, которого следует избегать. Он прячет настоящий вопрос — осилит ли downstream-читатель то, что вы отгружаете? Когда вы строите геопространственное содержимое через API, сеттеры SetMeasureDict* вроде SetMeasureDictCoordinateSystem и SetMeasureDictGPTSCount сами заявляют уровень расширения 3, так что созданные файлы приходят на preflight уже помеченными. Та же привычка ограниченных проверок, опирающихся на улики, видна в статье preflight режима автора PDF/E-1 для инженерных документов, где CAD-чертежи встречаются со стандартом соответствия вместо номера версии
Почему скан правил читает таблицу на месте
Цена preflight — грубо узлы объектов, умноженные на правила, а счёт узлов включает каждое число и каждое имя внутри каждого массива. Поэтому накладные расходы на правило значат больше, чем кажется. Каждое правило в таблице — запись с двумя полями AnsiString: строка совпадения и имя фичи. Копирование такой записи в локальную переменную заставляет Delphi или FPC поднять два счётчика ссылок и освободить то, что локальная переменная держала раньше. Сделанное раз на правило на узел, это означает оплату строковой бухгалтерии даже для правил, неприменимых к целевой версии и выбрасываемых на следующей же строке
Таблица правил заполняется однажды при инициализации юнита и после этого не меняется, поэтому скан передаёт записи таблицы своему коду сопоставления через const-параметры-записи и читает их там, где они живут. const-параметр-запись передаётся по ссылке, так что проверка правила не трогает ни одно из строковых полей, и счётчики ссылок остаются где были. Эффект сосредоточен на пути сопоставления правил. Реальное сохранение также декодирует объекты, гоняет отложенную детекцию фич и сериализует вывод, и эти стадии от способа передачи правил не зависят. Порядок правил, пороги и диагностика первой ошибки в 311 и 312 от этого не зависят, и ни один результат правила не кэшируется между сохранениями, так что документ, который вы правите и сохраняете дважды, оба раза проверяется полностью
Тот же шаблон стоит проверить в собственном коде на Delphi всюду, где цикл идёт по таблице записей, содержащих строки, интерфейсы или динамические массивы:
type
TMyRule = record
Name: string; // управляемое поле: копии стоят работы по счётчикам ссылок
Threshold: Integer;
end;
// Код читателя, показанный как шаблон
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: без копии, без трафика счётчиков ссылок
end;
// В горячем цикле передавайте запись таблицы напрямую вместо
// предварительного присваивания в локальную запись
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Измерительные вьюпорты, геопространственные словари, блокировка версии и проверка соответствия при сохранении поставляются в PDF Library for Delphi, доступной для проектов на Delphi, C++Builder и Lazarus