HotPDF v2.743.0, /AP appearance stream taşımayan PDF annotation'larını sessizce atlamak yerine flatten eder. FlattenLoadedAnnotations artık appearance'ı olmayan bir widget'ı EnsureLoadedFieldAppearanceStream üzerinden yönlendirir ve appearance'sız markup için annotation'ın kendi özelliklerinden bir Form XObject oluşturur; böylece /NeedAppearances formuna yazılan değerler flatten zamanında kaybolmak yerine sayfa içeriğine girer. Bu değişikliği zorlayan hata bir no-op gibi görünür. Bir müşteri, tarayıcıdan PDF'e yazdırılmış doldurulmuş bir başvuru formu gönderir. HotPDF'e yükler, FlattenLoadedAnnotations çağırır, 0 döner, kaydeder ve başvuru sahibinin isim ile tutar yazdığı yerde boş kutular olan bir belge yayımlarsınız. Hiçbir şey yükselmez, hiçbir şey günlüğe yazılmaz. Değerler bütün zaman boyunca dosyada, her alanın /V girişinde oturuyordu; flatten geçişi, pişireceği appearance stream'i bu widget'ların hiçbiri taşımadığı için onların yanından yürüyüp geçti
Tarayıcıdan yazdırılan form flatten edilince yazılan değerler neden kaybolur?
Çünkü /NeedAppearances formu değeri, değerin resmini saklamadan saklar. ISO 32000-1 12.7.2, etkileşimli bir formun AcroForm dictionary içinde /NeedAppearances true ayarlamasına izin verir; bu, viewer'ın açılışta her field'ın visual surface'ini /V, /DA ve /Q değerlerinden kurmasını söyler. Ucuza form üreten producer'lar — tarayıcı yazdırma yolları, server-side filler'lar ve bazı tarama front end'leri — bu teklifi kullanır ve hiç /AP yazmaz. ISO 32000-1 12.5.5'teki appearance algorithm'in tanımladığı flattening bir transkripsiyon işidir: annotation'ın normal appearance stream'ini al, /BBox'unu /Rect'ine eşle, sayfa content stream'inden Do operator'ü ile çağır, sonra annotation'ı sil. Kaynak stream yoksa transcribe edilecek bir şey de yoktur. v2.386.0'dan beri eski HotPDF uygulaması bunu "skip" olarak ele alıyordu; tek başına savunulabilir, toplu halde felaket bir tercihti, çünkü flatten'e en çok ihtiyaç duyan belgeler appearance taşıma olasılığı en düşük olanlardır. Aynı boşluk markup'ı da yutuyordu: producer viewer'ın çizmesine güvendiğinde bir review aracının Highlight'ı, redline geçişinin Square'i veya bir Ink imzası kayboluyordu
HotPDF synthesis'i FlattenLoadedAnnotations içine nerede bağlıyor?
Kanca noktası bilerek geçtir: appearance lookup başarısız olduktan sonra, önce değil. FlattenLoadedAnnotations hâlâ önce normal appearance için GetLoadedAnnotationAppearanceStream ister ve zaten appearance taşıyan bir annotation v2.386.0'da olduğu gibi pişirilir. Yalnızca nil sonuç, degenerate olmayan bir /Rect ve hidden flag taşımayan bir annotation synthesis yoluna girer. Sıralama önemlidir: /AP yazma zahmetine giren belge yazarı kendi baytlarını geri alır, HotPDF'in onları yeniden kurmasını değil
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
if Subtype= 'Widget' then
begin
FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
if FieldIdx>= 0 then
EnsureLoadedFieldAppearanceStream(FieldIdx);
// yeniden sor: generator widget'a /AP /N bağladı
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Buradan sonra iki annotation ailesi ayrılır. Widget, GetLoadedFormFieldIndexForAnnotation ile sahibi olan field'a çözülür ve v2.328.0'dan beri bu Delphi PDF kütüphanesinde bulunan field appearance generator'ı EnsureLoadedFieldAppearanceStream'e verilir. İkinci bir field renderer yazmak yerine bunu yeniden kullanmanın amacı budur; Type0 font'larını, line wrapping'i, quadding'i, checkbox ve radio /AS durumlarını ve /MK rotation'ı zaten kapsar; zaten yüklenmiş bir PDF'e AcroForm alanları ekleme yolunun arkasındaki mekanizma da budur. Geri kalan her şey markup synthesizer'a gider. Çağıran açısından hiçbir şey değişmez: aynı tek satırlık flatten çağrısı artık daha önce sıfır döndüren belgelerde sıfır olmayan bir sayı döndürür
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: AP'siz widget ve markup sentezlenir, sonra pişirilir
Flattened:= Doc.FlattenLoadedAnnotations; // bütün sayfalar, bütün alt türler
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
QuadPoints ve InkList neden yanlış yere düşer?
Çünkü bu koordinatlar page user space içindedir, sentezlenen appearance stream ise kendi /BBox uzayında çizer ve iki origin aynı nokta değildir. ISO 32000-1 Table 176, text markup annotation'ları için /QuadPoints değerini default user space'te tanımlar; Table 174 de line annotation'ın /L uçları için aynısını yapar; /InkList de aynı kuralı izler. HotPDF, sentezlenen form'a origin'i /Rect'in lower-left köşesinde olan [0 0 W H] değerli bir /BBox verir. Bu nedenle /QuadPoints, /L veya /InkList'ten alınan her nokta content stream'e yazılmadan önce negatif /Rect lower-left ile ötelenmelidir. Bunu yanlış yaparsanız sayfadan 700 point yukarıdaki bir satırdaki highlight kendi kutusunun 700 point üstüne çizilir; pratikte hiçbir yere çizilmemiş olur. Düzeltme koordinat başına tek bir çıkarma işlemidir ve bake'in sonra ürettiği cm ile birleşir; bu matrix /BBox'u yeniden /Rect'e eşler ve iki adım doğru mutlak geometriyle birbirini götürür
// /L uçları page user space'tedir (ISO 32000-1 Table 174); formun
// BBox origin'i /Rect lower-left'tedir, bu yüzden -(RL, RB) ile kaydır
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
_FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
_FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;
Sentezlenen markup appearance gerçekte ne çizer?
Markup synthesizer yalnızca annotation dictionary'yi okur; bu, çıktıyı öngörülebilir tutar ve bilemeyeceği şeyler konusunda dürüst kalır. FreeText ve Stamp, /DA'dan ayrıştırılan font ve color ile, /Q hizalamasını kullanarak 2 pt padding ile /Contents çizer. Square ve Circle bir re veya dört yaylı Bezier outline'ını /C içinde stroke eder, /IC varsa doldurur ve genişliği /BS /W'den alır. Line ve Ink köşelerini stroke eder. Highlight her quad'ı doldurur; Underline, StrikeOut ve Squiggly sırasıyla quad altına, quad ortasına veya tek point'lik zigzag olarak bir çizgi stroke eder. 1'in altındaki bir /CA, stream'in başında /GSA gs olarak başvurulan, ca girişi taşıyan bir ExtGState olur
Text encoding, /DA'nın adlandırdığı AcroForm /DR /Font girişinden belirlenir. Bu fontun /Subtype değeri Type0 ise HotPDF string'i FEFF byte order mark ile UTF-16BE hex literal olarak yazar; değilse parantez ve backslash'leri escape ederek, 126'nın üzerindeki baytları octal yazarak escape edilmiş literal string çıkarır. /DA'daki Tf operator'ü BT'den önce yayımlanır; text state text object sınırında devam ettiği için bu yasaldır ve /DA string'ini parçalama işini kurtarır. İki sınırı açık söylemek gerekir. Wrapping ve quadding için line width gerçek font metrics yerine half-em / full-em heuristic ile tahmin edilir; bu nedenle proportional font üzerindeki hizalama yakın ama tam değildir. Ayrıca synthesize edilecek hiçbir şeyi olmayan bir subtype — Popup, Link veya tek içeriği icon name olan bir Stamp — nil üretir ve eskisi gibi dokunulmadan bırakılır
Yardımcı bir cleanup'ın cezalandırdığı geçici /Annots takası
FlattenOneWidget, FlattenLoadedFormFields tarafından kullanılan widget başına yol, ortak flatten loop'u içindeki her değişikliğin saygı duyması gereken bir aliasing tuzağıdır. Generic flatten geçişinin tek bir widget üzerinde çalışması için sayfanın /Annots değerini geçici olarak tek elemanlı bir array ile değiştirir, ardından özgün PHPDFDictionaryItem pointer'ını finally bloğunda geri yükler. Restore, çağrıdan önce yakaladığı bir dictionary slot'una geri yazar
DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
DictItem^.Value:= Item; // iç döngü bu item'ı serbest bıraktıysa dangling
TemporaryAnnots.Free;
end;
Ortak inner loop içine makul görünen bir tidy-up ekleyin — array boşaldığında kaydedilen sayfada artık anlamsız boş array kalmasın diye bir DeleteValue('Annots') — ve bu çağrı DictItem'ın işaret ettiği dictionary item'ın kendisini serbest bırakır. Ardından finally dangling pointer üzerinden yazar ve süreç "Invalid pointer operation" ile ölür. İki mevcut test bunu hemen yakaladı; bu yüzden bir support ticket değil bir dipnottur. Kural genellenebilir: paylaşılan bir loop'a cleanup eklemeden önce çağıranlarda alias veya swap sözleşmelerini kontrol edin. Geride kalan boş /Annots array'i kozmetik bir pürüzdür ve bir pointer yaşam süresi garantisiyle takas edilmeye değmez
Neler pişirilmeden kalır ve flattening neye mal olur?
Hidden annotation'lar bilerek dışarıda tutulur. /F integer'ı ISO 32000-1 12.5.3'e göre 2. bit pozisyonunu taşıyan bir annotation hidden'dır; /AP de yoksa onu sentezleyip diğerleri gibi pişirmek cazip gelir. Bu, güvenlik sonuçları olan bir hata olur: görünmez bir notu sayfa içeriğine pişirmek, dosyayı açan herkes için görünür kılar. HotPDF bu annotation'ları olduğu yerde bırakır ve dönüş değerinde saymaz. Pişirilenlerin bedelini de kullanıcılarınıza açıkça anlatın. Flattening geri döndürülemez: annotation sayfanın /Annots dizisinden silinir ve visual artık page content olur; dolayısıyla field value düzenleme, comment thread, /AS state toggle veya özgün dosya dışında structured data'yı geri alma yolu kalmaz. Bir kopyayı flatten edin, özgününü saklayın ve yalnızca belge form olmaktan çıkıp kayda dönüştüğünde başvurun. Sorununuz appearance'sız değil XFA-backed ise HotPDF'deki XFA'dan AcroForm'a flattening yolu ile başlayın; formu hâlâ kuruyorsanız AcroForm field action ve validation bağlama notları yazma tarafını kapsar
Bir verification caveat daha var; aksi halde bir öğleden saatinizi alır. ExtractLoadedPageGlyphs Form XObject'lere inmez ve baked appearance birinin içinde yaşar — page content stream yalnızca q ... cm /FlatAn<n> Do Q dizisini taşır. Dolayısıyla flattened sayfada glyph extraction hiçbir şey raporlamaz; bu kayıp bake değil, doğru davranıştır. Ya /FlatAn resource name, Do invocation ve /Subtype /Form için byte seviyesinde kontrol yaparak ya da XObject'leri genişleten rendering pipeline üzerinden doğrulayın
Annotation flattening, gerçek insanların ürettiği belgelerle karşılaşana kadar üç satırlık transkripsiyon gibi görünür. Delphi veya C++Builder'da doldurulmuş formlarla, review markup'ıyla veya arşiv çıktısıyla çalışıyorsanız, kendi appearance generator'ınızı üzerine kurmadan önce HotPDF Delphi PDF component'in loaded-document tarafındaki AcroForm ve annotation işlemlerini okumaya değer