Teknik Makale

Delphi'de PDF İşaretli İçeriğini Okuma ve Yazma

Etiketli bir PDF'te her sayfanın tepesindeki akan başlık, genellikle /Artifact <</Type /Pagination>> BDC … EMC içine, onun altındaki ilk paragraf ise /P <</MCID 0>> BDC … EMC içine oturur. İkisi de ISO 32000-1 §14.6 anlamında işaretli içeriktir. Yapı ağacından yalnızca paragrafa ulaşılır, çünkü yalnızca paragraf bir işaretli-içerik tanımlayıcısı taşır ve boyalı glifleri /H1 ya da /P gibi bir yapı öğesine bağlayan şey de işte o tek tam sayıdır. Bu yüzden etiketlemeyi denetleyen veya onaran kod, her sayfa nesnesinden üç şeyi okumak (etiketi, özellik listesini ve MCID'yi) ve sayıyı sonra o sayfadaki yapı öğelerinin gerçekte sahiplendiği MCID'lerle karşılaştırmak zorundadır

BDC işaretlerini ve özellik listelerini Delphi'den okumak

PDFium Component'te TPdf.PageObjectMarks(Index), Index konumundaki sayfa nesnesini saran her işareti TPdfContentMark kayıtlarından oluşan bir dizi olarak döndürür. Her kayıt etiketi Name içinde, özellik listesini taşıyan bir Params dizisini ve bir Handle'ı taşır. Ad, baştaki eğik çizgi olmadan gelir: bir /P dizisi P raporlar, bir artifact ise Artifact. İşaretli içerik dizileri iç içe geçebilir; /Sect içinde, sonra /P, sonra /Span içinde duran bir glif dizisi aynı nesne üzerinde üç kayıt raporlar, en dıştan içe doğru

ISO 32000-1 §14.6.2, BDC'nin özellik listesi işleneni için iki biçime izin verir: /P <</MCID 3>> BDC örneğindeki gibi satır içi bir sözlük ya da sayfa kaynaklarının /Properties alt sözlüğüne işaret eden /OC /oc1 BDC gibi bir ad. İsteğe bağlı içerik (§8.11.3.2) ikinci biçimle yazılır. PDFium, isimli biçimi sayfayı ayrıştırırken çözer; böylece her iki durumda da Params, dizi için geçerli olan sözlüğün girdilerini listeler

TPdf.PageObjectMarkedContentID(Index), yapı ağacının umursadığı soruyu yanıtlar. Nesnenin işaretlerinde bulunan MCID'yi, hiçbiri tam sayı bir /MCID taşımıyorsa -1 döndürür. Açılış örneğindeki başlık -1 döndürür ve bu doğrudur: §14.8.2.2 artifact'ları mantıksal yapının dışında tutar, yani bir artifact'ın bağlanacak hiçbir şeyi yoktur

Delphi'de PDF işaretli içerik anatomisinin şeması: BDC ile EMC arasında MCID özellik listesi taşıyan bir /P etiketi, TPdfContentMark kayıt alanları ve yapı ağacına MCID bağlantısı
Bir işaret, BDC ile EMC arasındaki bir etiket ve türü belirli özellik listesidir; MCID'si yapı ağacına katılan bir kayıt olarak bildirilir
var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber 1 tabanlıdır
  for I := 0 to Pdf.ObjectCount - 1 do    // sayfa nesnesi indeksleri 0 tabanlıdır
  begin
    Marks := Pdf.PageObjectMarks(I);
    if Length(Marks) = 0 then
      Continue;
    Memo1.Lines.Add(Format('object %d, MCID %d',
      [I, Pdf.PageObjectMarkedContentID(I)]));
    for M in Marks do                     // en dış işaret önce
    begin
      Memo1.Lines.Add('  mark ' + M.Name);
      for P in M.Params do
        case P.Kind of
          pmpInt:     Memo1.Lines.Add('    ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpFloat:   Memo1.Lines.Add('    ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpString:  Memo1.Lines.Add('    ' + P.Key + ' = ' + P.StringValue);
          pmpBlob:    Memo1.Lines.Add('    ' + P.Key + ' = ' +
                        IntToStr(Length(P.BlobValue)) + ' bytes');
          pmpUnknown: Memo1.Lines.Add('    ' + P.Key + ' = (no readable value)');
        end;
    end;
  end;
end;

pmpUnknown'un söyleyebileceği ve söyleyemeyeceği şeyler

Her TPdfContentMarkParam bir Key, bir Kind ve o türün seçtiği tek bir değer alanı taşır. Eşleme ezberde tutulabilecek kadar kısadır:

  • Tamsayı değerli bir sayı, IntValue atanmış ve FloatValue aynı sayıyla dolu şekilde pmpInt olur; /MCID 3 de başıboş bir 3.0 da tam sayı okunur
  • Kesirli kısmı olan bir sayı pmpFloat olur
  • PDF string'leri ve PDF adları ikisi de pmpString olur; UTF-16 bayt sırası işaretiyle (FE FF ya da FF FE) başlayan metin string'leri StringValue içine çözülür, böylece /ActualText okunabilir bir metin olarak gelir
  • Diğer değer türleri, PDFium onlar için ham bayt verdiğinde pmpBlob olarak gelir, aksi hâlde pmpUnknown kalır

pmpUnknown birbirinden ayırt edilemeyen iki durumu kapsar. Biri, PDFium'un FPDF_OBJECT_UNKNOWN raporladığı ve için hiç bayt vermediği bir değerdir. Diğeri, listede görünen ama geri okunamayan bir anahtardır: bozuk bir dosyada ASCII dışı baytlarla yazılmış bir anahtar listede yer alabilir ve yine de pmpUnknown olarak gelebilir. Kayıt her iki durumda da aynı görünür

pmpUnknown'u "kullanılabilir bir değer yok" olarak ele alın ve işareti kabul etmek yerine raporlayın. İş akışınız /Lang veya /ActualText bulunmasına bağlıysa, anahtarı tanıdığınız bir türle arayın; bilinmeyen bir türden asla "özellik yok" sonucunu çıkarmayın

Bir Handle, o an yüklü sayfa içindeki bir işareti tanımlar ve işaret kaldırıldığında, sayfa nesnesi yok edildiğinde ya da sayfa kaldırıldığında bayatlar. Sayfa değiştirmeden önce Name ve Params'ı kendi yapılarınıza kopyalayın. Liste görünümü ya da arka plan denetimi gibi o anki sayfadan uzun yaşamak zorunda olan veriler için ReadPageObjectsSnapshot, yalnızca adı ve parametreleri taşıyan, hiç tanıtıcı içermeyen TPdfContentMarkSnapshot kayıtları döndürür. Aynı kural sayfa nesnesi tanıtıcıları için genel olarak geçerlidir; dönüşümden sonra bayatlaşan sayfa nesnesi tanıtıcıları notları bunu ele alır

İşaret ekleme, güncelleme ve kaldırma

Düzenlemeyi dört çağrı karşılar. AddPageObjectMark(Index, Name, Params) nesne üzerinde yeni bir işaret oluşturur, parametreleri sırayla uygular ve yeni işaretin o nesnedeki indeksini döndürür; diğer üç çağrı bunu MarkIndex olarak alır. Adlar eğik çizgisiz girilir: 'P', 'Span'. Tek bir anahtarı değiştirmek için SetPageObjectMarkParam(Index, MarkIndex, Param) anahtarı ekler ya da değiştirir ve işaretin diğer tüm parametrelerine dokunmaz. Kaldırma çağrileri daha dolaysızdır: RemovePageObjectMarkParam(Index, MarkIndex, Key) bir anahtarı düşürür; RemovePageObjectMark(Index, MarkIndex) işaretin tamamını düşürür

AddPageObjectMark işareti ve parametrelerini tek adım olarak görür. Bir parametre reddedilirse (bir pmpUnknown türü, boş bir anahtar, boş bir blob), az önce oluşturduğu işaret, istisna yukarı sıçramadan önce yeniden kaldırılır; böylece nesne asla özellik listesinin yarısını taşıyan bir etiketle kalmaz

Bu çağrilerin her biri, dönerken sayfa içerik akışını yeniden kurar. Bu önemlidir, çünkü SaveAs içerik akışını olduğu gibi yazar ve onu PDFium'un nesne modelinden yeniden üretmez. Yalnızca bellekteki nesnede var olan bir işaret, kaydedilen dosyayı tek bir BDC işleci olmadan bırakırdı. Sayfa nesnelerini ayrıca ObjectHandle üzerinden ham PDFium importlarıyla düzenliyorsanız, kaydetmeden önce UpdatePage'i kendiniz çağırın

Delphi'de bir işaretli-içerik etiketi eklemenin şeması: AddPageObjectMark, SaveAs'tan önce sayfa içerik akışını yeniden kurar; yalnızca-nesne-modeli bir değişiklik ise kaydedilen dosyadan sessizce düşer
AddPageObjectMark içerik akışını yeniden kurar; böylece SaveAs etiketi kalıcılaşır ve nesne modelinde duran bir değişiklik asla dosyaya ulaşmaz

Akışın her çağrıda baştan kurulması birkaç düzenleme için sorun değil, birkaç yüz için savurgandır. Etiketsiz bir raporu sayfa sayfa etiketlemek ikinci tür iştir ve oradaki düzenlemeler BeginPageObjectEditTransaction ile CommitPageObjectEditTransaction içine ait olur. İşlem içinde her düzenleme sayfayı yalnızca değiştirilmiş olarak işaretler; commit içerik akışını bir kez üretir ve RollbackPageObjectEditTransaction sayfayı olduğu hâle geri koyar. Bir işlem tek sayfaya bağlıdır ve işlem açıkken PageNumber'ı başka bir sayfaya çekmek istisna fırlatır. İmzalı bir belgede başlatma çağrısı, AllowSignedDocument = True geçirmedikçe başlamayı reddeder; sayfa içeriğini düzenlemek zaten imzayı bozar

String parametreler bir karar daha ister. pmpString değerleri bayt string olarak yazılır; bu, /Lang (en-US) gibi ASCII değerler için tam isabet eder. Aksan, ligatür veya CJK karakterli değiştirme metni, FE FF önekiyle UTF-16BE kodlu bir PDF metin string'ine ait olur (§7.9.2.2). O baytları pmpBlob olarak geçirin: PDFium onları bir string nesnesi olarak saklar ve yukarıdaki okuyucu onları pmpString olarak geri StringValue içine çözer

// Kitaplığın parçası olmayan kendi yardımcınız: BOM'lu, UTF-16BE olarak PDF metin string'i
function Utf16TextString(const S: string): TBytes;
var
  I: Integer;
begin
  SetLength(Result, 2 + Length(S) * 2);
  Result[0] := $FE;
  Result[1] := $FF;
  for I := 1 to Length(S) do
  begin
    Result[I * 2] := Byte(Ord(S[I]) shr 8);
    Result[I * 2 + 1] := Byte(Ord(S[I]));
  end;
end;

procedure TagWord(Pdf: TPdf; ObjectIndex, Mcid: Integer; const Spoken: string);
var
  Params: TPdfContentMarkParams;
  Lang: TPdfContentMarkParam;
  MarkIndex: Integer;
begin
  Pdf.BeginPageObjectEditTransaction;
  try
    SetLength(Params, 2);
    Params[0].Key := 'MCID';
    Params[0].Kind := pmpInt;
    Params[0].IntValue := Mcid;
    Params[1].Key := 'ActualText';
    Params[1].Kind := pmpBlob;            // metin string olarak saklanır, pmpString olarak geri okunur
    Params[1].BlobValue := Utf16TextString(Spoken);
    MarkIndex := Pdf.AddPageObjectMark(ObjectIndex, 'Span', Params);

    Lang := Default(TPdfContentMarkParam);
    Lang.Key := 'Lang';
    Lang.Kind := pmpString;               // ASCII değer, düz bir bayt string uygun
    Lang.StringValue := 'de-DE';
    Pdf.SetPageObjectMarkParam(ObjectIndex, MarkIndex, Lang);
  except
    Pdf.RollbackPageObjectEditTransaction;
    raise;
  end;
  Pdf.CommitPageObjectEditTransaction;    // tüm toplu iş için tek içerik akışı yeniden kurulumu
end;

Commit, try bloğunun dışında oturur; bu bilinçli bir tercihtir. İçerik üretimi başarısız olursa CommitPageObjectEditTransaction, istisnayı yeniden fırlatmadan önce sayfayı kendisi geri yükler; ikinci bir rollback çağrısı ise gerçek hatanın üstüne yalnızca "No page-object edit transaction is active" fırlatırdı

Bir paragraf neden üç MCID 5 dizisiyle bitiyor?

Çünkü her AddPageObjectMark çağrısı, değerler aynı olsa bile yeni ve bağımsız bir işaret sözlüğü oluşturur. Satır başına bir tane olmak üzere üç metin nesnesi olarak dizilmiş ve her biri /MCID 5 geçen üç çağrıyla etiketlenmiş bir paragraf üç ayrı işaret taşır. PDFium içerik akışını yeniden üretirken ardışık nesneleri yalnızca aynı işareti paylaşıyorlarsa tek bir BDC/EMC çiftinin içinde tutar; karşılaştırma işaretin kimliği üzerinedir, eşit değerler sayılmaz. Kaydedilen sayfa sonra /P <</MCID 5>> BDC … EMC'yi üst üste üç kez taşır

ISO 32000-1 §14.7.4.2, MCID'yi kendi içerik akışı içinde bir işaretli içerik dizisini özgünce tanımlayan şey olarak tanımlar. Tek bir tanımlayıcıyı paylaşan üç dizi bunu karşılamaz ve MCID'leri sayfanın parent tree girdisi üzerinden çözen araçlar, yapı öğesinin üçünden hangisini kastettiğini tahmin etmeye kalır

SharePageObjectMark(TargetIndex, SourceIndex, SourceMarkIndex), kaynak işaretin kendi özellik sözlüğünü kopya kurmak yerine hedef nesneye ekler. İşareti ilk satıra bir kez ekleyin, ötekilerle paylaşın ve nesneleri boyama sırasında ardışık tutun; yeniden üretilen akış o zaman üç satırın hepsini tek bir diziye sarar

procedure TagParagraph(Pdf: TPdf; const LineObjects: array of Integer; Mcid: Integer);
// LineObjects: paragrafın satırlarının nesne indeksleri, boyama sırasında ardışık
var
  Params: TPdfContentMarkParams;
  MarkIndex, I: Integer;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := Mcid;

  Pdf.BeginPageObjectEditTransaction;
  try
    MarkIndex := Pdf.AddPageObjectMark(LineObjects[0], 'P', Params);
    for I := 1 to High(LineObjects) do
      Pdf.SharePageObjectMark(LineObjects[I], LineObjects[0], MarkIndex);
  except
    Pdf.RollbackPageObjectEditTransaction;
    raise;
  end;
  Pdf.CommitPageObjectEditTransaction;
end;

Paylaşmanın, var olan bir işareti yeni içeriğe kopyaladığınızda ikinci bir yararı vardır; örneğin yeniden çizilmiş bir dip çizgisi üzerine bir artifact'ı genişletmek gibi. PDFium'un yazabildiği parametre türleri integer, float, string ve bayt blob'dur; ad türü yoktur. pmpString olarak okunup AddPageObjectMark ile yeniden kurulan bir /Artifact <</Type /Pagination>>, §14.8.2.2'nin /Pagination adını beklediği yerde bir string olan (Pagination) çıkar. Paylaşılan işaret özgün sözlüğün kendisidir; adları ad olarak kalır. Kaynak işaret sayfada zaten var olduğunda her zaman paylaşın; Params'tan yalnızca hiç var olmamış bir işaret yazarken yeniden kurun

MCID'leri yapı ağacıyla eşlemek

Bir sayfa nesnesindeki MCID, ancak aynı sayfadaki bir yapı öğesi onu sahiplendiğinde bir anlam taşır. StructureElementsForPage[PageNumber], sayfanın öğelerini TPdfStructureElement kayıtları olarak döndürür ve üç alan MCID taşır: MarkedContentID (öğenin hiçbiri yoksa -1), birkaç diziye sahip olan öğeler için MarkedContentIDs ve işaretli içerik değil öğe olan çocukların yuvalarında -1 taşıyan ChildMarkedContentIDs. Bir denetim üçüne de bakmak zorundadır, çünkü bir yapı öğesinin /K girdisi çocuk öğelerle işaretli içerik referanslarını tek bir dizide karıştırabilir

Aşağıdaki denetim iki bulgu raporlar. MCID'sini hiçbir öğenin sahiplenmediği ya da hiç MCID'si olmayan metin ve resimler, yardımcı teknolojinin atlayacağı içeriktir. MCID'si sahiplenmiş bir Artifact ise §14.8.2.2 ile açıkça çelişir. Sayfa nesneleri ReadPageObjectsSnapshot'tan gelir; sonuçlar tanıtıcı taşımaz ve sayfa değişimini atlatabilir

// Kitaplığın parçası olmayan kendi yardımcınız
function McidClaimed(const Elements: TPdfStructureElements; Mcid: Integer): Boolean;
var
  E: TPdfStructureElement;
  Id: Integer;
begin
  for E in Elements do
  begin
    if E.MarkedContentID = Mcid then
      Exit(True);
    for Id in E.MarkedContentIDs do
      if Id = Mcid then
        Exit(True);
    for Id in E.ChildMarkedContentIDs do
      if Id = Mcid then
        Exit(True);
  end;
  Result := False;
end;

procedure AuditPage(Pdf: TPdf; PageNo: Integer; Log: TStrings);
var
  Snap: TPdfPageObjectsSnapshot;
  Status: TPdfPageObjectSnapshotStatus;
  Elements: TPdfStructureElements;
  Obj: TPdfPageObjectSnapshot;
  IsArtifact: Boolean;
  J: Integer;
begin
  Snap := Pdf.ReadPageObjectsSnapshot(PageNo,
    TPdfPageObjectSnapshotOptions.Full, Status);
  if not Status.Completed then
    Log.Add(Format('page %d: snapshot hit a limit, results are partial', [PageNo]));
  Elements := Pdf.StructureElementsForPage[PageNo];
  for Obj in Snap.Objects do
  begin
    IsArtifact := False;
    for J := 0 to High(Obj.Marks) do
      if Obj.Marks[J].Name = 'Artifact' then
        IsArtifact := True;
    if IsArtifact then
    begin
      if (Obj.MarkedContentID >= 0) and McidClaimed(Elements, Obj.MarkedContentID) then
        Log.Add(Format('object %d: artifact claimed by the structure tree', [Obj.Index]));
    end
    else if Obj.ObjectType in [otText, otImage] then
      if (Obj.MarkedContentID < 0) or not McidClaimed(Elements, Obj.MarkedContentID) then
        Log.Add(Format('object %d: content with no structure owner', [Obj.Index]));
  end;
end;

Çizgiler bilinçli olarak dışarıda bırakıldı. Dekoratif çizgiler ve tablo kenarlıkları çoğu zaman etiketsizdir ve hepsini tek tek işaretlemek metin bulgularını gömer. Form XObject'ler tersine dikkat ister: bir formun içindeki MCID, o formun kendi /StructParents ad alanına aittir; sayfadaki MCID 0 ile form içindeki MCID 0 farklı dizilerdir. etiketli Form XObject'lerde MCID ad alanları yazısı bunları doğru okumayı ele alır

İşaretler hâlâ etiketli bir belgenin yalnızca yarısıdır. ISO 32000-1 §14.7, öğeleri bu MCID'lere başvuran bir yapı ağacı, katalogda /Marked true taşıyan bir /MarkInfo sözlüğü ve ya standart olan ya da role map üzerinden standartlara eşlenen yapı türleri ister. Pdf.IsTagged, katalogun belgeyi etiketli ilan edip etmediğini söyler. MCID'sini hiçbir öğenin işaret etmediği bir /P işareti, yapı ağacının hiç anmadığı bir sayfada etiketli olduğunu iddia eden içerik üretir

PDFium Component şeması: Tek başına BDC işaretleri etiketli bir PDF yapmaz; ayrıca MCID'lere referans veren bir yapı ağacı, bir MarkInfo bildirimi ve standart rol adları gerekir
İşaretler denetimi ile sahipsiz onarımını destekler; kurallara uygun etiketli bir belge ayrıca bir yapı ağacı, MarkInfo ve standart rol adları ister

O işin yapı ağacı yarısı için, MCID denetiminin kaldığı yerden PDF/UA yapı ağacı doğrulaması yazısı devam eder. Etiketlerin varoluş amacı olan okuma deneyimi ise Delphi'de erişilebilir bir PDF okuyucu inşa etme notlarının konusudur

Burada kullanılan çağrılar (işaret okuma, düzenleme işlemleri, paylaşılan işaretler, sayfa nesnesi anlık görüntüleri ve yapı öğeleri), PDFium Component'in PDFium motoru üzerinden Delphi, C++Builder ve Lazarus projelerine verdiği aynı VCL API'nin parçasıdır. PDFium Component ürün sayfası erişilebilirlik ve sayfa düzenleme API'sinin geri kalanını kapsar