Техническая статья

Валидация отсканированных документов PDF/raster в Delphi

PDF/R, стандартизированный как ISO 23504-1, — профиль PDF для отсканированных документов: каждая страница несёт ровно одно ленточное изображение и ничего более. PDFium Component проверяет его из Delphi, Lazarus и C++Builder через ValidatePdfRCompliance, читающую поток и возвращающую уровень соответствия плюс набор конкретных проблем

Профиль существует, потому что сканерам и системам документозахвата нужна была цель уже, чем PDF/A. Архивный PDF может содержать всё, что позволяет часть; растровый PDF намеренно обеднён, поэтому любой соответствующий читатель покажет его идентично, а любой соответствующий писатель сможет произвести его из скана без авторизующего движка

Что PDF/R запрещает из того, что PDF/A позволяет?

На практике — текст. Растровая страница несёт отсканированное изображение и ничего более, поэтому шрифтовой ресурс на странице — нарушение, сообщаемое как pvriFontForbidden согласно ISO 23504-1 §6.5.2. Это удивляет тех, кто добавляет невидимый слой OCR-текста для поиска — нормальную и полезную вещь в рабочем процессе PDF/A, — но это просто не PDF/R

Отношение страницы к изображению столь же строго. §6.5.1 делает каждую страницу ровно одним ленточным изображением, поэтому pvriPageImageMismatch срабатывает, когда число изображений не совпадает с числом страниц — страница без изображения и страница с двумя одинаково несоответствующи. А pvriBadMediaBox сообщает о странице, чья MediaBox не имеет формы [0 0 w h] (§6.5.3), потому что у скана нет причины сидеть со смещением начала координат

uses FPdfPdfr;

var
  Src: TFileStream;
  Res: TPdfRValidationResult;
begin
  Src := TFileStream.Create('scan-batch-0142.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfRCompliance(Src);
    if Res.IsCompliant then
      Memo1.Lines.Add('PDF/R-1 conformant')
    else
    begin
      if pvriFontForbidden in Res.Issues then
        Memo1.Lines.Add('A page names a font resource; a raster page carries no text');
      if pvriPageImageMismatch in Res.Issues then
        Memo1.Lines.Add('Image count does not match page count');
      if pvriForbiddenImageFilter in Res.Issues then
        Memo1.Lines.Add('A strip image uses an encoding outside the white list');
    end;
  finally
    Src.Free;
  end;
end;

Какие кодировки изображений разрешены

Четыре, и белый список короток по причине. §6.6 допускает /CCITTFaxDecode, /DCTDecode, /JPXDecode и /FlateDecode — бифаксиальный факс, JPEG, JPEG 2000 и lossless deflate, между собой покрывающие каждый значимый вывод сканера. Всё прочее сообщается как pvriForbiddenImageFilter, включая /LZWDecode, /RunLengthDecode, /ASCII85Decode, /ASCIIHexDecode, /JBIG2Decode и /Crypt

Два из этих отказов стоит понимать, а не заучивать. /JBIG2Decode сжимает бифаксиальные сканы крайне эффективно и вполне легален в PDF/A, но реконструкция его словаря символов может подменять визуально похожие глифы — задокументированный режим сбоя для отсканированных цифр, — а профиль, чья вся цель — точное растровое воспроизведение, не может допустить этот риск. ASCII-фильтры исключены по противоположной причине: они надувают файл, не добавляя ничего, что нужно растровому профилю

Структурные правила, срабатывающие до чтения любой страницы

PDF/R ограничивает и контейнер. pvriObjStmPresent сообщает о потоке /Type /ObjStm, который профиль запрещает категорически: потоки объектов усложняют простой последовательный разбор, который растровый читатель должен быть способен выполнять. pvriBadHeader сообщает о заголовке вне %PDF-1.4–1.7 и %PDF-2.0, а pvriEncryptVersionMismatch — о зашифрованном файле, чей заголовок не %PDF-2.0, согласно §6.2.3

Каталог и словарь Info не просто проверяются, а белым списком. pvriProhibitedCatalogEntry и pvriProhibitedInfoEntry срабатывают по записям вне разрешённого множества, а pvriInfoXmpMismatch — когда запись Info не согласуется с её XMP-эквивалентом. Отсутствие потока /Metadata каталога, отсутствие /ID трейлера и отсутствие маркера %PDF-raster-1.0 в конце файла также имеют свои собственные проблемы

Почему в записи опций сохранения нет Title и Author

TPdfRSaveOptions несёт Creator, Producer, CreationDate, ModDate, DocumentId и InstanceId и намеренно не имеет поля для Title, Author, Subject или Keywords. Эти четыре — записи, запрещённые §6.4.3, поэтому запись, выявляющая их, приглашала бы вызывающих писать несоответствующий файл через соответствующий API

Две булевы опции управляют очисткой при конвертации существующего PDF. StripInfoOptionalEntries по умолчанию True и удаляет Title, Author, Subject, Keywords и Trapped из исходного словаря Info. StripCatalogOptionalEntries также по умолчанию True и удаляет Names, Outlines, StructTreeRoot, OutputIntents, Lang и прочее, оставляя лишь белый список §6.3. Установите любую в False — и вы сохраните записи, потеряв соответствие, что иногда именно то, чего вызывающий подлинно хочет для внутреннего файла

var
  Opts: TPdfRSaveOptions;
  Src, Dest: TFileStream;
begin
  Opts := TPdfRSaveOptions.Default;
  Opts.Creator := 'Capture Station 4';
  Opts.Producer := 'PDFium Component';
  Src := TFileStream.Create('scan-in.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create('scan-pdfr.pdf', fmCreate);
    try
      InjectPdfRMarkers(Src, Dest, Opts);   // markers + metadata, not page content
    finally
      Dest.Free;
    end;
  finally
    Src.Free;
  end;
end;

Заметьте, чего внедрение маркеров не делает: оно добавляет метаданные и идентификацию и не может поставить содержимое страницы. Исходная страница, не несущая ленточного изображения, всё равно провалит pvriPageImageMismatch после внедрения, потому что недостающее изображение никогда не было проблемой метаданных

Где PDF/R уместен в конвейере документозахвата

Применяйте его там, где результат и есть скан, а точность — весь контракт: доказательная съёмка, захват чеков и платежных поручений, архивы инженерных чертежей с широкоформатного сканера. Берите PDF/A вместо него с того момента, как документу нужен искомый текст, разметка, встроенные вложения или что-либо ещё, что растровый профиль срезает

Распространённый и работоспособный подход — выпускать оба: оригинал PDF/R, который никогда не меняется, и производное PDF/A с OCR-слоем для поиска. Валидаторы независимы, поэтому один и тот же пакетный job может проверить каждый артефакт против заявленного им профиля. По архивной стороне этой пары см. заметки об архивном соответствии PDF/A и предпечатной валидации PDF/A, а для печатно-ориентированного вывода — разбор валидации печатно-готовых документов PDF/X

PDFium Component приносит движок PDFium в Delphi, C++Builder и Lazarus с VCL API и валидаторами соответствия для PDF/A, PDF/X, PDF/E, PDF/UA и PDF/R — на странице продукта PDFium Component перечислены поддерживаемые стандарты и версии IDE