Технічна стаття

Читання та запис позначеного вмісту PDF у Delphi

Позначений вміст — це механізм, який ISO 32000-1 §14.6 визначає для тегування вмісту сторінки, і на ньому побудовані як тегований PDF, так і PDF/UA. PDFium Component виставляє його безпосередньо: PageObjectMarks читає кожну позначку BDC та її список властивостей з об'єкта сторінки, AddPageObjectMark пише одну, RemovePageObjectMark видаляє одну, а PageObjectMarkedContentID повідомляє MCID, що пов'язує вміст із деревом структури

Доки дерево структури не можна приєднати назад до вмісту, який воно описує, інструменти доступності — це вгадування. Дерево структури каже «це заголовок»; MCID каже, якими позначками на якій сторінці цей заголовок фактично є. Обидві половини мають бути читабельні, перш ніж застосунок зможе перевірити, відремонтувати або звітувати про тегування

Що таке позначка, у байтах?

Оператор BDC з іменем позначки та необов'язковим списком властивостей, закритий EMC. У потоці вмісту він виглядає як /P <</MCID 3>> BDC ... EMC: позначка /P називає роль, словник несе властивості, а все між операторами — це позначений вміст. Об'єкт сторінки всередині цього проміжку несе позначку, що й повертає PDFium і що PDFium Component перетворює на запис

TPdfContentMark тримає дескриптор, позначку Name та масив TPdfContentMarkParam. Кожен параметр має Key, Kind та одне змістовне поле значення, обране цим типом: pmpInt, pmpFloat, pmpString або pmpBlob. Тип приходить із власного звіту PDFium про тип, а не від того, який getter трапилося успішно викликати, — це різниця між читанням списку властивостей і вгадуванням його

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Чому pmpUnknown означає дві різні речі

pmpUnknown повертається, коли PDFium звітує FPDF_OBJECT_UNKNOWN, а PDFium також повертає його для ключа, якого не існує. Ці два випадки неможливо розрізнити на цьому рівні, і вдавати інше було б гірше, ніж сказати про це

Практичний наслідок для вашого коду: ставтеся до pmpUnknown як «тут немає придатного значення», а не як до типу, який ви все одно могли б декодувати. Якщо властивість важлива для вашого робочого процесу, перевірте її наявність із типом, який ви розпізнаєте, і не виводьте відсутність із невідомого — позначка, чий список властивостей ви не можете прочитати, це позначка, про яку варто звітувати, а не тихо прийняти

Запис позначки — це знімок, а не дескриптор, яким ви володієте

Поле Handle належить бібліотеці. Воно стає простроченим у момент, коли позначку видалено, об'єкт сторінки знищено або сторінку вивантажено, тож запис є знімком лише для читання з коротким життям. Закешуйте його через перемикання сторінки — і ви тримаєте вказівник у пам'ять, яку рушій уже повернув

Це та сама дисципліна, що застосовується до дескрипторів об'єктів сторінки загалом у PDFium, і вона ловить людей у тому самому місці: список, заповнений записами позначок, користувач переходить на іншу сторінку, і збій, що виглядає непов'язаним із навігацією. Скопіюйте значення, які вам потрібні — ім'я, ключі, числа — і відпустіть дескриптор. Нотатки про дескриптори об'єктів сторінки, що старіють після трансформації описують загальне правило і те, як воно кусає в інших місцях

Додавання позначки та крок збереження, який легко пропустити

AddPageObjectMark приймає індекс об'єкта сторінки, ім'я позначки та повний набір параметрів. Параметри пишуться як набір, а не латуються по одному ключу, тому в TPdfContentMarkParam немає сантинелів Has* — випадок «оновити одне поле наявного запису», який вони б охороняли, не виникає

Частина, яку варто висловити прямо: додавання позначки перебудовує потік вмісту сторінки, тож позначка переживає збереження. Це мало бути явним, бо SaveAs не регенерує вміст сам — зміна, що жила лише в об'єктній моделі, буде відкинута, а збережений файл виглядатиме точно як той, з якого ви почали. Якщо ви коли-небудь додавали щось до сторінки PDFium і виявляли це відсутнім у виводі, зазвичай це причина

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Що це робить і не робить із документа

Лише позначки не роблять тегований PDF. Відповідний тегований документ потребує дерева структури, чиї елементи посилаються на ці MCID, запису /MarkInfo, що оголошує документ позначеним, та імен ролей, які означають те, що каже стандарт. Запис позначки /P з MCID, на який не вказує жоден елемент структури, дає вам вміст, що заявляє про тегування, і дерево структури, яке про це ніколи не згадує

Там, де позначений вміст справді заробляє своє місце на цьому рівні, це перевірка та відновлення: аудит того, які об'єкти сторінки теговані, пошук артефактів, які мали б бути позначені як такі, або зіставлення MCID із деревом структури, щоб знайти сиріт. Щодо половини дерева структури цієї роботи дивіться нарис валідації дерева структури PDF/UA, а щодо враження від читання, для якого зрештою існують теги, нотатки про побудову доступного читача PDF у Delphi

PDFium Component надає застосункам Delphi, C++Builder та Lazarus високорівневий VCL API поверх рушія PDFium, із позначеним вмістом, деревами структури та валідацією доступності, доступними зі звичайного Pascal-коду — дивіться сторінку продукту PDFium Component щодо повної поверхні API