Teknik Makale

Ek Açıklama Silme Sonrası OBJR ve ParentTree Temizliği

Sayfa odaklı bir düzenleyiciyle etiketli bir PDF'ten Link ek açıklamasını kaldırın; sayfa sorunsuz çizer, oysa yapı ağacı gitmiş bir açıklama için hâlâ bir OBJR, bir ParentTree girdisi ve bir Link öğesi taşır. PDFium Component'teki RemovePdfPageAnnotations açıklamayı sayfadan çeker ve istenirse o yapı parçalarını da onunla birlikte kaldırır

Etiketli bir ek açıklama yapı ağacına nereye bağlanır

Etiketli bir ek açıklama dört nesnenin kavşağında durur ve temiz bir silme işleminin dördünü de gezmesi gerekir. ISO 32000-1 §14.7.4.3, bir yapı öğesinin içeriği bir ek açıklama gibi bütün bir PDF nesnesiyken, öğenin /K dizisinin ona bir nesne başvuru sözlüğüyle ad verdiğini söyler (Tablo 325): /Type /OBJR, açıklamayı işaret eden zorunlu dolaylı bir /Obj ve öğenin kendi sayfasını geçersiz kılan isteğe bağlı bir /Pg. Bu, ağaçtan sayfaya giden yönü kapsar

Etiketli bir Link ek açıklamasının PDF sayfası Annots dizisine, OBJR nesne başvurusuna, StructParent anahtarına ve ParentTree girdisine bağlandığı yerler: Delphi için PDFium Component
Etiketli bir ek açıklama dört nesneye bağlanır; temiz bir silme, sayfa başvurusunu, OBJR'yi, StructParent anahtarını ve ParentTree çiftini gezer

Ters yol §14.7.4.4'tür. Ebeveyn ağacı, içerik öğesi olan her nesne için bir girdi taşıyan bir sayı ağacıdır (§7.9.7); girdi, o nesnenin taşıdığı tamsayıyla anahtarlanır. Bir ek açıklama /StructParent taşır ve onun ebeveyn ağacı değeri, sahibi olan öğeye giden tek bir dolaylı başvurudur; içerik akışında işaretli içerik taşıyan bir sayfa ise /StructParents taşır ve değeri MCID ile indekslenen bir dizidir. Tablo 326, herhangi bir nesnede iki anahtardan en fazla birine izin verir; çünkü bir nesne ya bütünüyle bir içerik öğesidir ya da işaretli içerik için bir kapsayıcıdır. Hiper bağlantılar için §14.8.4.4.2, bir Link öğesinin hem görünür sözcükleri işaretli içerik başvuruları (MCR) olarak hem de link açıklamasına giden bir OBJR olarak tutabilmesine izin verir. Açıklamayı silin; envanter şudur:

KonumTaşıdığı şeyTemiz bir kaldırmanın yaptığı
Sayfa /AnnotsAçıklamaya giden dolaylı başvuruBaşvuruyu düşürür; kalanlar sıralarını korur
Link öğesi /K/Obj ile /Pg taşıyan OBJROBJR içerik öğesini kaldırır
Ek açıklama sözlüğü/StructParent tamsayı anahtarıAnahtarı kaldırır; çünkü nesne artık bir içerik öğesi değildir
ParentTree /Nums yaprağıLink öğesine giden başvuruyla eşlenmiş anahtarAnahtar ve değer çiftini kaldırır
Yapı ağacı kökü/ParentTreeNextKeyDokunmaz

Hayalet bir link, açıklaması gittikten sonra nasıl hayatta kalır?

Sayfa düzeyinde bir düzenleyici bir ek açıklamayı /Annots içinden çıkararak siler ve iş oradan iki yoldan birine sapar. Araç açıklama nesnesini de serbest bırakıyorsa OBJR'nin /Obj'si artık tanımsız bir nesneyi işaret eder; §7.3.10, okuyucuya bunu null gibi işlemesini söyler ve Link öğesi, hiçbir şeye çözülmeyen bir içerik öğesinin sahibi olur. Araç nesneyi dosyada bırakıyorsa OBJR hâlâ çözülür, ama onun /Pg'si /Annots dizisi artık listelemeyen bir sayfayı adlandırır. Her iki durumda da Link öğesi işaretli içerik çocuklarını tutmaya devam eder; dolayısıyla "Download the datasheet" sözcükleri hâlâ bir link olarak etiketlidir ve okuma sırasında ağaçta yürüyen bir ekran okuyucu, sayfada hiçbir şeyin etkinleştiremeyeceği bir link duyurabilir. ISO 14289-1'e karşı doğrulama yapan PAC ve veraPDF gibi PDF/UA denetleyicileri, sayfanın listelediğiyle ağacın öne sürdüğünü karşılaştırır; dolayısıyla ikisi arasındaki bir uyumsuzluk, çizilen sayfa kusursuz görünürken bile orada su yüzüne çıkabilir

Sayfa odaklı bir silmeden sonra etiketli PDF'te hayalet link bırakan iki yol ve PDFium Component'ün Delphi'de bunları nasıl temizlediği
Açıklama nesnesi serbest bırakılsa da dosyada bırakılsa da link öğesi sözcüklerini korur; ekran okuyucular hiçbir yere gitmeyen bir link duyurabilir

İçerik silmenin de aynı sorunun kendine özgü bir sürümü vardır. /P <</MCID 4>> BDC … EMC içindeki metin nesnelerini kaldırıp operatörleri bırakın; sayfa, MCR'si hâlâ bir <P> öğesinde oturan boş bir işaretli içerik dizisi taşır ve okuyucu sözcüksiz bir paragraf alır. BDC'yi kaldırıp EMC'yi bırakın; içerik akışı baştan aşağı bozulur. §14.6, her işaretli içerik dizisinin tek bir içerik akışı içinde BDC ya da BMC ile eşleşen bir EMC arasında kapatılmasını ister; başıboş bir EMC, sayfadaki sonraki her diziyi yanlış açıcıyla eşleştirir

RemovePdfPageAnnotations ile ek açıklama kaldırma

FPdfDocumentStructure içindeki RemovePdfPageAnnotations; bir kaynak akış, sayfa ağacı sırasında sıfır tabanlı bir sayfa indeksi, kaldırılacak ek açıklamaların nesne numaraları, bir TPdfAnnotationRemovalOptions kaydı ile yeni dosya ve bir TPdfAnnotationRemovalReport için iki out parametresi alır. TPdfAnnotationRemovalOptions.Default ile yalnızca sayfaya dokunur: kısalan /Annots dizisini taşıyan sayfa sözlüğü (son açıklama da gidince /Annots anahtarı hiç kalmaz) artı değişmeyen katalog tek bir artımlı revizyonla geçer; böylece nesne numaraları, belge kimliği ve öbür tüm sayfa anahtarları hayatta kalır. Ek açıklama nesnesi gövdesini korur ve numarasını hâlâ elinde tutan bir çağıran onu okuyabilir. Yapı ağacı olduğu gibi bırakılır; bu da, o Link öğesini bir yedek açıklamaya yöneltmek üzere olduğunuz durumda etiketlemeyi bozulmadan tutar. Herhangi bir şey yazılmadan önce girdi denetlenir:

  • Her numara seçili sayfanın /Annots dizisinde görünmeli ve bir /Type /Annot nesnesi adlandırmalı; tek bir çağrıda iki kez listelenen numara reddedilir
  • Widget ek açıklamaları reddedilir; çünkü bir widget kendi form alanına aittir ve onu sayfadan sökmek, widget'ını hiçbir sayfanın göstermediği bir alan bırakır
  • Şifreli bir kaynak reddedilir; çünkü yapı düzenlemesi çözülmüş baytlara ihtiyaç duyar, dosyayı önceden çözün
  • MaxOutputBytes, MaxWork ve CancellationToken işi sınırlar; herhangi bir başarısızlık çıktısız False döndürür ve ErrorMessage, işin nerede durduğunu bildiren aşamayı adlandırır

Etiketli bir belge için yapı ağacı, hangi açıklama numaralarının önemli olduğunu zaten söyler. Bir ReadPdfDocumentStructure anlık görüntüsündeki her OBJR öğesi, başvurulan nesne numarasını taşır; sayfa numarası OBJR'nin kendi /Pg'sinden, o yoksa öğeninkinden gelir. Aşağıdaki yordam, bir sayfadaki bir Link öğesinin gönderme yaptığı her açıklamayı toplar ve iki temizlik seçeneği de açıkken kaldırır. Sayfa filtresini koruyun: başka bir sayfadan alınan numara tüm yığını reddettirir

uses
  System.SysUtils, System.Classes, System.IOUtils,
  FPdfCompress, FPdfDocumentStructure;

function RemoveTaggedLinks(const InName, OutName: string;
  PageIndex: Integer): TPdfAnnotationRemovalReport;
var
  Source: TMemoryStream;
  Pages: TPdfSparsePageDictionaries;
  ReadOptions: TPdfStructureEditOptions;
  Snapshot: TPdfDocumentStructureSnapshot;
  ReadReport: TPdfStructureEditReport;
  Item: TPdfDocumentStructureContentItem;
  Targets: array of Integer;
  Options: TPdfAnnotationRemovalOptions;
  Report: TPdfAnnotationRemovalReport;
  Output: TBytes;
  N, I, J: Integer;
  Known: Boolean;
begin
  Report := Default(TPdfAnnotationRemovalReport);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile(InName);
    if not ReadPdfSparsePageDictionaries(Source, Pages) or
       (PageIndex < 0) or (PageIndex > High(Pages)) then
      raise Exception.Create('Page index is outside the page tree');
    ReadOptions := TPdfStructureEditOptions.Default;
    if not ReadPdfDocumentStructure(Source, ReadOptions, Snapshot, ReadReport) then
      raise Exception.Create(ReadReport.ErrorMessage);

    // Bu sayfadaki bir Link öğesinin işaret ettiği her ek açıklama
    for N := 0 to High(Snapshot.Nodes) do
      if Snapshot.Nodes[N].RoleName = 'Link' then
        for I := 0 to High(Snapshot.Nodes[N].ContentItems) do
        begin
          Item := Snapshot.Nodes[N].ContentItems[I];
          if (Item.Kind <> psckObjectReference) or
             (Item.PageObjectNumber <> Pages[PageIndex].ObjectNumber) then
            Continue;
          Known := False;
          for J := 0 to High(Targets) do
            if Targets[J] = Item.ReferencedObjectNumber then Known := True;
          if not Known then
          begin
            SetLength(Targets, Length(Targets) + 1);
            Targets[High(Targets)] := Item.ReferencedObjectNumber;
          end;
        end;
    if Length(Targets) = 0 then
      Exit(Report);

    Options := TPdfAnnotationRemovalOptions.Default;
    Options.RemoveOwnedStructurePeers := True;    // OBJR öğeleri ve ParentTree girdileri
    Options.RemoveEmptiedStructureNodes := True;  // içi boş kalan öğeler
    if not RemovePdfPageAnnotations(Source, PageIndex, Targets, Options,
      Output, Report) then
      raise Exception.Create(Report.ErrorMessage);
    TFile.WriteAllBytes(OutName, Output);
    Result := Report;
  finally
    Source.Free;
  end;
end;

ParentTree anahtarına ne olur?

RemoveOwnedStructurePeers ayarlıyken, /Obj'si o sayfada kaldırılmış bir ek açıklama olan her OBJR, yapı düzenleyicisinden geçerek öğesinden ayrılır; bu, ters indeksi güncel tutar. Ek açıklamanın /StructParent'i altında saklanan anahtar ve değer çifti kendi /Nums yaprağından çıkarılır ve /StructParent anahtarının kendisi de ek açıklama sözlüğünden kaldırılır; çünkü Tablo 326 bu anahtarı yalnızca yapısal içerik öğesi olan nesnelerde ister. /ParentTreeNextKey düşürülmez. §14.7.4.4 yalnızca bunun kullanımdaki her anahtardan büyük olmasını ister, ki bir kaldırma bunu bozamaz; ayrıca sayı ağacı anahtarlarının bitişik olması gerekmez, dolayısıyla /ParentTreeNextKey'i izleyen bir yazıcı serbest kalan anahtarı bir daha asla dağıtmaz. Hayatta kalan her ek açıklama nesne numarasını, /Annots içindeki konumunu ve ParentTree anahtarını korur; işlev yazdığı sayfayı yeniden okur ve bir hayatta kalan kaybolduysa ya da yer değiştirdiyse sonucu reddeder. Beklenen: tek etiketli linki olan bir sayfada Report.RemovedAnnotations ile Report.RemovedObjectReferences ikisi de 1 olur; Report.SourceSha256 ile Report.OutputSha256 hangi baytların girip hangilerinin çıktığını kaydeder

Boşalan öğe de ne zaman gitmeli?

RemoveEmptiedStructureNodes, OBJR kaldırmasının içerik öğesiz ve çocuk öğesiz bıraktığı öğeleri, en derinden başlayarak kaldırır. Turlar hâlinde çalışır: her tur yapıyı yeniden okur, artık boş olan öğeleri kaldırır ve ebeveynlerini bir sonraki turun adaylarına ekler; böylece bir sarmalayıcı zinciri aşağıdan yukarı çökerken yapı ağacı kökü her zaman kalır. Bir yapışkan notu etiketleyen bir Annot öğesi genellikle OBJR'den başka bir şey tutmaz (§14.8.4.4.3 buna izin verir); dolayısıyla boşalır ve gider. Bir Link öğesi farklıdır. Görünür sözcüklerinin işaretli içerik başvurularını hâlâ sahiplenir; dolayısıyla boş değildir ve açıklamasız bir Link olarak hayatta kalır. RemovePdfPageAnnotations onu yeniden adlandırmaz; çünkü o sözcüklerin link olarak kalıp kalmaması yapısal değil editoryal bir sorudur. Salt bir silme için her iki seçeneği de açın, sonra OBJR'siz kalan her Link öğesini Span'a düşürün; yardımcı teknoloji o zaman o sözcükleri düz metin olarak okur ve hiçbir yere gitmeyen bir link önermez

Boşalan yapı öğeleri için karar akışı: bir Annot öğesi tur tur çöker, bir Link öğesi hayatta kalır ve PDFium Component onu Span'a düşürebilir
Boşalan sarmalayıcılar kök kalırken en derinden çöker; OBJR'siz kalan bir Link, Span'a düşürülebilir ya da bir yedek açıklama için tutulabilir
function DemoteUnlinkedLinks(const Data: TBytes; out Fixed: TBytes): Integer;
var
  Source: TBytesStream;
  Options: TPdfStructureEditOptions;
  Snapshot: TPdfDocumentStructureSnapshot;
  Ops: TPdfStructureEditOperations;
  Report: TPdfStructureEditReport;
  N: Integer;
begin
  Result := 0;
  Fixed := Data;
  Source := TBytesStream.Create(Data);
  try
    Options := TPdfStructureEditOptions.Default;
    if not ReadPdfDocumentStructure(Source, Options, Snapshot, Report) then
      raise Exception.Create(Report.ErrorMessage);
    for N := 0 to High(Snapshot.Nodes) do
      if (Snapshot.Nodes[N].RoleName = 'Link') and
         (Snapshot.Nodes[N].ObjectReferenceCount = 0) and
         (Length(Snapshot.Nodes[N].ContentItems) > 0) then
      begin
        SetLength(Ops, Length(Ops) + 1);
        Ops[High(Ops)] := Default(TPdfStructureEditOperation);
        Ops[High(Ops)].Kind := psekSetRole;
        Ops[High(Ops)].NodeID := Snapshot.Nodes[N].NodeID;
        Ops[High(Ops)].Value := 'Span';
      end;
    if Length(Ops) = 0 then Exit;
    // Anlık görüntü özeti, baytların arada değişmiş olmasına karşı korur
    if not EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops,
      Options, Fixed, Report) then
      raise Exception.Create(Report.ErrorMessage);
    Result := Length(Ops);
  finally
    Source.Free;
  end;
end;

Silme, bir değişimin yarısıysa varsayılanları koruyun. Yerinde bırakılan boşalmış bir öğe rolünü, başlığını, alternatif metnini ve öğe kimliğini hâlâ taşır; sonraki bir adım ona bir psekAddObjectReference işlemiyle yeni bir açıklama eklerse — bu işlem açıklamaya taze bir /StructParent anahtarı verir ve /ParentTreeNextKey'i, yeni MCR ve OBJR öğelerini ebeveyn ağacına kaydeden kuralların aynısıyla bir ilerletir — öğe okuma sırasındaki yerini korur. O öğeyi kaldırıp yeniden oluşturmak bu değerleri çöpe atar ve ona yeni bir nesne numarası verirdi

Etiketli içerik silmek: sahiplik BDC'den eşleşen EMC'ye kadar uzanır

Ek açıklamalar sayfa nesneleri değildir; metin, yol ve resimler farklı bir API'den geçer: FPdfStyledEdit içindeki toplu nesne düzenleyici, PlanStyledPdfObjectEdits ve ExecuteStyledPdfObjectEdits; bunların TPdfObjectEditOptions kaydı RemoveOwnedStructurePeers ile RemoveEmptiedStructureNodes'i aynı anlamda taşır. Etiketli bir işaretli içerik dizisi, ancak BDC'si ile eşleşen EMC'si arasındaki her nesne siliniyorken yapı girdisiyle birlikte gider. O durumda her iki operatör de içerik akışından ayrılır, MCR öğesinden ayrılır ve o MCID'in sayfanın ParentTree dizisindeki yuvası kapatılmak yerine null yapılır; çünkü sayfadaki öbür her MCID aynı diziye indekslenir. Hayatta kalan içeriği de saran bir dizi, MCR'siyle birlikte kalır. Kaldırılan MCID'i hayatta kalan içerik için yeniden kullanan bir sayfa düpedüz reddedilir; çünkü MCID'ler sayfanın içerik akışına göre kapsamlıdır ve yapı girdisini düşürmek hayatta kalanı başıboş bırakır. Seçenek False bırakılırsa toplu işlem bunun yerine kaldırma sınırındaki BDC ve EMC çiftlerini korur; böylece yapı ağacı çözülmesini sürdürür ve içinde hiçbir şey değişmez

Sahiplik, BDC'den eşleşen EMC'sinin sonuna kadar ölçülmek zorundadır. İlk sürümümüz bunu yalnızca BDC operatörünün bayt aralığı üzerinden ölçtü — ayrıştırılmış bir işaret konumunu tam orada kaydeder — ve hiç kapsanmış nesne bulamadı. İlgili bir hata, BDC'yi kaldırılma için işaretleyip eşini kaybetmektir; bu, tam da yukarıda anlatılan dengesiz akışı yazar, dolayısıyla düzenleyici bir çiftin iki yarısını da her zaman birlikte kaldırır. Aşağıdaki yordam, tek MCID taşıyan her üst düzey nesneyi siler. ExecuteStyledPdfObjectEdits, EditOptions.PreviewOnly ayarlı değilse Pdf'i sonuçla yeniden yükler; Report.RemovedStructurePeers ile Report.RemovedStructureNodes ağaçtan gideni sayar ve işlev o raporu döndürür

uses
  System.SysUtils, PDFium, FPdfStyledEdit;

function DeleteTaggedRun(Pdf: TPdf; PageIndex, Mcid: Integer): TPdfObjectEditReport;
var
  Ops: TPdfObjectEditOperations;
  NoSemanticEdits: TPdfObjectSemanticEdits;
  Options: TPdfObjectEditOptions;
  Plan: TPdfObjectEditPlan;
  Preview: TBytes;
  Report: TPdfObjectEditReport;
  I: Integer;
begin
  Pdf.PageNumber := PageIndex + 1;          // PageNumber birden başlar
  for I := 0 to Pdf.ObjectCount - 1 do
    if Pdf.PageObjectMarkedContentID(I) = Mcid then
    begin
      SetLength(Ops, Length(Ops) + 1);
      Ops[High(Ops)] := Default(TPdfObjectEditOperation);
      Ops[High(Ops)].Kind := poekRemove;
      Ops[High(Ops)].ObjectIndex := I;
    end;
  if Length(Ops) = 0 then
    raise Exception.CreateFmt('No object on page %d carries MCID %d',
      [PageIndex + 1, Mcid]);
  NoSemanticEdits := nil;
  Options := TPdfObjectEditOptions.Default;
  Options.RemoveOwnedStructurePeers := True;
  Options.RemoveEmptiedStructureNodes := True;
  Plan := PlanStyledPdfObjectEdits(Pdf, PageIndex, Ops, NoSemanticEdits, Options);
  if not ExecuteStyledPdfObjectEdits(Pdf, Plan, Options, Preview, Report) then
    raise Exception.Create(Report.ErrorMessage);
  Result := Report;
end;

Kaldırılmış bir ek açıklama, iki çağrıdan sonra da dosyadaki nesnesini işgal etmeye devam eder; çünkü geri çekilen yalnızca sayfa başvurusudur. OBJR gittikten sonra kataloğun grafiğinde ona ulaşan hiçbir şey kalmaz; dolayısıyla TPdf.SaveAsReachable, o başıboş nesneyi yeniden yazılmış bir kopyadan süpürebilir. Varsayılan seçeneklerle OBJR açıklamayı ulaşılabilir tutar ve süpürme de onu tutar. Silme kesinse kaldırmayı her iki yapı seçeneği de açıkken çalıştırın; Delphi için PDFium Component'ün geri yazdığı dosyadaki sayfa, yapı ağacı ve ebeveyn ağacı böylece hep aynı ek açıklama kümesini anlatır