HotPDF v2.743.0 plattar ut PDF-anteckningar som saknar en /AP-utseendeström i stället för att tyst hoppa över dem. FlattenLoadedAnnotations skickar nu en widget utan utseende genom EnsureLoadedFieldAppearanceStream och bygger ett Form XObject för markeringar utan utseende från annotationens egna egenskaper, så att värdena som skrivits in i ett /NeedAppearances-formulär överlever i sidinnehållet i stället för att försvinna vid utplattningen. Felet som tvingade fram ändringen ser ut som ett tomt anrop. En kund skickar ett ifyllt ansökningsformulär som skrivits ut till PDF från en webbläsare. Du läser in det i HotPDF, anropar FlattenLoadedAnnotations, får tillbaka 0, sparar och levererar ett dokument med tomma rutor där den sökande skrev ett namn och ett belopp. Inget undantag, inget i loggen. Värdena fanns i filen hela tiden, i varje fälts /V-post, och utplattningspasset gick rakt förbi dem eftersom ingen av dessa widgetar bar på en utseendeström att baka in
Varför försvinner de ifyllda värdena när ett webbläsarutskrivet formulär plattas ut?
För att ett formulär med /NeedAppearances lagrar värdet utan att lagra en bild av värdet. ISO 32000-1 12.7.2 låter ett interaktivt formulär ange /NeedAppearances true i AcroForm-ordboken, vilket säger åt visaren att konstruera varje fälts visuella yta vid öppningen från /V, /DA och /Q. Producenter som genererar formulär billigt — webbläsarens utskriftsvägar, serverbaserade ifyllare och vissa skanningsfronter — tar det erbjudandet och skriver inget /AP alls. Utplattning, som definieras av utseendealgoritmen i ISO 32000-1 12.5.5, är ett transkriptionsarbete: ta annotationens normala utseendeström, mappa dess /BBox till dess /Rect, anropa den från sidans innehållsström med en Do-operator och ta sedan bort annotationen. Utan källström finns inget att transkribera. Den ursprungliga HotPDF-implementationen från v2.386.0 behandlade detta som "skip", vilket är försvarbart isolerat och katastrofalt i större skala: de dokument som mest sannolikt behöver utplattas är de som minst sannolikt bär på utseenden. Samma lucka slukade markeringar — en Highlight från ett granskningsverktyg, en Square från en redline-genomgång och en Ink-signatur — när producenten litade på att visaren skulle rita dem
Var kopplar HotPDF in syntesen i FlattenLoadedAnnotations?
Kopplingspunkten är avsiktligt sen: efter att uppslagningen av utseendet misslyckas, inte före. FlattenLoadedAnnotations frågar fortfarande först GetLoadedAnnotationAppearanceStream efter det normala utseendet, och en annotation som redan har ett utseende bakas in exakt som i v2.386.0. Endast ett nil-resultat på en annotation med en icke-degenererad /Rect och utan dold flagga går in i syntesvägen. Ordningen spelar roll: en dokumentförfattare som tog sig besväret att skriva ett /AP får tillbaka sina egna byte, inte en HotPDF-rekonstruktion av dem
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);
// fråga igen: generatorn har lagt till /AP /N i widgeten
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Därifrån delar sig de två annotationfamiljerna. En widget löses tillbaka till sitt ägande fält genom GetLoadedFormFieldIndexForAnnotation och lämnas till EnsureLoadedFieldAppearanceStream, fältutseendegeneratorn som har funnits i detta Delphi PDF-bibliotek sedan v2.328.0. Att återanvända den i stället för att skriva en andra fältåtergivare är hela poängen — den täcker redan Type0-typsnitt, radbrytning, textjustering, kryssrute- och radiotillstånd via /AS samt rotation med /MK, samma maskineri som ligger bakom att lägga till AcroForm-fält i en redan inläst PDF. Allt annat går till markeringarnas syntetiserare. För anroparen ändras ingenting: samma utplattningsanrop på en rad returnerar nu ett värde som inte är noll för dokument som tidigare gav noll
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgetar och markeringar utan AP syntetiseras och bakas sedan in
Flattened:= Doc.FlattenLoadedAnnotations; // alla sidor, alla subtyper
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Varför hamnar QuadPoints och InkList på fel plats?
För att dessa koordinater finns i sidans användarutrymme medan den syntetiserade utseendeströmmen ritar i sitt eget /BBox-utrymme, och de två origo ligger inte på samma punkt. ISO 32000-1 tabell 176 definierar /QuadPoints för textmarkeringsannotationer i standardanvändarutrymmet, och tabell 174 gör samma sak för linjeannotationens /L-ändpunkter; /InkList följer samma konvention. HotPDF ger det syntetiserade formuläret en /BBox på [0 0 W H] vars origo ligger i det nedre vänstra hörnet av /Rect. Därför måste varje punkt som hämtas från /QuadPoints, /L eller /InkList flyttas med det negerade nedre vänstra hörnet från /Rect innan den skrivs i innehållsströmmen. Gör du fel hamnar en markering på en rad 700 punkter upp på sidan 700 punkter ovanför sin egen ruta, vilket i praktiken betyder att den ritas ingenstans. Korrigeringen är en subtraktion per koordinat, och den samverkar med cm som bakningen avger efteråt — den matrisen mappar tillbaka /BBox till /Rect, så de två stegen tar ut varandra till korrekt absolut geometri
// /L-ändpunkter är i sidans användarutrymme (ISO 32000-1 tabell 174); formulärets
// BBox-origo ligger vid det nedre vänstra hörnet av /Rect, så flytta med -(RL, RB)
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;
Vad ritar det syntetiserade markeringutseendet faktiskt?
Markeringarnas syntetiserare läser annotationens ordbok och inget annat, vilket håller utdata förutsägbara och ärligt om vad den inte kan veta. FreeText och Stamp ritar /Contents med typsnittet och färgen som parsats ur /DA, justerade enligt /Q, med 2 punkters marginal. Square och Circle ritar en re- eller fyrbågig Bezier-kontur som streckas i /C, fylls med /IC när den finns och använder bredden från /BS /W. Line och Ink streckar sina hörn. Highlight fyller varje fyrhörning, medan Underline, StrikeOut och Squiggly streckar en linje vid fyrhörningens botten, vid dess mittpunkt eller som en sicksack på en punkt. En /CA under 1 blir en ExtGState med en ca-post, refererad som /GSA gs i början av strömmen
Textkodningen avgörs från AcroForms /DR /Font-post som namnges av /DA. Om det typsnittets /Subtype är Type0 skriver HotPDF strängen som en UTF-16BE-hexliteral med byteordningsmarkören FEFF; annars skriver den en escaped literalsträng, där parenteser och backslash escapes och byte över 126 skrivs oktalt. Tf-operatorn från /DA avges före BT, vilket är lagligt eftersom texttillståndet lever vidare över textobjektgränsen, och det sparar att ta isär /DA-strängen. Två begränsningar bör sägas tydligt. Linjebredden för radbrytning och textjustering uppskattas med en halv-em/full-em-heuristik i stället för verkliga typsnittsmetriker, så justering på ett proportionellt typsnitt är nära men inte exakt. Och en subtyp utan något syntetiserbart — Popup, Link eller en Stamp vars enda innehåll är ett ikonnamn — ger nil och lämnas orörd, precis som tidigare
Det tillfälliga /Annots-bytet som straffar en hjälpsam städning
FlattenOneWidget, vägen per widget som används av FlattenLoadedFormFields, är en aliaseringsfälla som varje ändring i den delade utplattningsloopen måste respektera. Den ersätter tillfälligt sidans /Annots-värde med en array med ett element så att det generella utplattningspasset arbetar på en enda widget, och återställer sedan den ursprungliga PHPDFDictionaryItem-pekaren i ett finally-block. Återställningen skriver tillbaka till en ordboksplats som den fångade före anropet
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; // dinglande om den inre loopen frigjorde detta objekt
TemporaryAnnots.Free;
end;
Lägg till en rimlig städning i den delade inre loopen — en DeleteValue('Annots') när arrayen blir tom, så att den sparade sidan inte bär på någon överbliven tom array — och det anropet frigör själva ordboksposten DictItem pekar på. finally-blocket skriver då genom en dinglande pekare och processen dör med "Invalid pointer operation". Två befintliga tester fångade det omedelbart, vilket är den enda anledningen till att detta är en fotnot och inte ett supportärende. Regeln generaliserar: innan du lägger till städning i en delad loop, kontrollera anroparna efter alias- eller byteskontrakt. En överbliven tom /Annots-array är en kosmetisk skavank och inte värd att byta mot en garanti för pekarlivstid
Vad förblir obakat, och vad kostar utplattningen?
Dolda annotationer utesluts avsiktligt. En annotation vars heltal /F har bitposition 2 satt är dold enligt ISO 32000-1 12.5.3, och när den dessutom saknar /AP finns en verklig frestelse att syntetisera ett och baka in det som resten. Det vore ett fel med säkerhetskonsekvenser: att baka in en osynlig anteckning i sidinnehållet gör den synlig för alla som öppnar filen. HotPDF lämnar dessa annotationer exakt där de är och räknar inte med dem i returvärdet. Var lika tydlig med användarna om priset för dem som faktiskt bakas in. Utplattning kan inte göras ogjord — annotationen tas bort från sidans /Annots-array och dess visuella resultat är nu sidinnehåll, så det går inte längre att redigera fältvärdet, ingen kommentarstråd finns kvar, inget /AS-tillstånd kan växlas och strukturdatan kan inte återställas utan originalfilen. Platta ut en kopia, behåll originalet och använd utplattning först när dokumentet slutar vara ett formulär och blir ett dokumentationsunderlag. Om problemet i stället är XFA-baserat är den separata XFA-till-AcroForm-utplattningsvägen i HotPDF den du ska börja med, och om du fortfarande bygger formuläret täcker anteckningarna om koppling av AcroForm-fältåtgärder och validering skrivsidan
En verifieringsdetalj, eftersom den annars kostar dig en eftermiddag. ExtractLoadedPageGlyphs går inte ner i Form XObjects, och ett bakat utseende bor i ett sådant — sidans innehållsström innehåller bara en sekvens q ... cm /FlatAn<n> Do Q. Glyfextrahering från en utplattad sida rapporterar därför ingenting, och det är korrekt beteende snarare än en förlorad bakning. Verifiera antingen på bytenivå genom att kontrollera resursnamnet /FlatAn, Do-anropet och /Subtype /Form, eller genom renderingspipelinen, som faktiskt expanderar XObjects
Annotationutplattning ser ut som tre rader transkription ända tills du möter dokumenten som människor faktiskt genererar. Om du arbetar med ifyllda formulär, granskningsmarkeringar eller arkiveringsutdata i Delphi eller C++Builder är det värt att läsa hur HotPDF Delphi PDF-komponenten hanterar AcroForms och annotationer på inlästa dokument innan du bygger en egen utseendegenerator ovanpå den