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

Четене и писане на маркирано съдържание на 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 възстановява content stream-а, така че 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, който също се нуждае от дърво на структурата, сочащо MCID, декларация MarkInfo и стандартни имена на роли
Марковете поддържат одит и поправка на сираци, докато съобразителен тагнат документ се нуждае още от структурно дърво, MarkInfo и стандартни ролеви имена

Там, където маркираното съдържание наистина печели стойността си на това ниво, е инспекция и поправка: одит кои обекти на страница са маркирани, намиране на артефакти, които е трябвало да бъдат маркирани като такива, или съпоставяне на MCID срещу дърво на структурата, за да се намерят сираките. За половината от тази работа, която е в дървото на структурата, вижте ръководството за валидация на структурно дърво на PDF/UA, а за изживяването на четене, за което марките в крайна сметка са — записките за изграждане на достъпен PDF четец в Delphi

PDFium Component дава на Delphi, C++Builder и Lazarus приложения API от високо ниво VCL върху двигателя PDFium, с маркирано съдържание, дървета на структурата и валидация на достъпност, достъпни от обикновен Pascal код — вижте страницата на продукта PDFium Component за пълната API повърхност