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

Читання та запис позначеного вмісту 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 трапилося успішно викликати, — це різниця між читанням списку властивостей і вгадуванням його

Діаграма анатомії розміченого вмісту PDF у Delphi: тег /P зі списком властивостей MCID між BDC і EMC, поля запису TPdfContentMark та зв'язок MCID з деревом структури
Позначка — це тег і типізований список властивостей між BDC і EMC, звітуваний як запис, чий MCID приєднується до дерева структури
var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber має відлік від 1
  for I := 0 to Pdf.ObjectCount - 1 do    // індекси об'єктів сторінки з відліком від 0
  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 і виявляли це відсутнім у виводі, зазвичай це причина

Діаграма додавання тега розміченого вмісту в Delphi: AddPageObjectMark перебудовує потік вмісту сторінки перед SaveAs, тоді як зміна лише в об'єктній моделі мовчки втрачається зі збереженого файлу
AddPageObjectMark перебудовує потік вмісту, тож SaveAs зберігає тег, а зміна, що зупинилася на моделі об'єктів, ніколи не доходить до файлу
var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // перебудовує потік вмісту
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

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

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

Діаграма PDFium Component: самі позначки BDC не роблять PDF розміченим — потрібні ще дерево структури з посиланнями на MCIDs, заява MarkInfo і стандартні назви ролей
Позначки підтримують аудит і лагодження сиріт, тоді як відповідний стандарту тегований документ ще потребує дерева структури, MarkInfo та стандартних назв ролей

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

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