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
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ı,
IntValueatanmış veFloatValueaynı sayıyla dolu şekildepmpIntolur;/MCID 3de başıboş bir3.0da tam sayı okunur - Kesirli kısmı olan bir sayı
pmpFloatolur - PDF string'leri ve PDF adları ikisi de
pmpStringolur; UTF-16 bayt sırası işaretiyle (FE FFya daFF FE) başlayan metin string'leriStringValueiçine çözülür, böylece/ActualTextokunabilir bir metin olarak gelir - Diğer değer türleri, PDFium onlar için ham bayt verdiğinde
pmpBlobolarak gelir, aksi hâldepmpUnknownkalı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
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
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