HotPDF v2.743.0 fladgør PDF-annotationer, der ikke har nogen /AP-appearance-stream, i stedet for lydløst at springe dem over. FlattenLoadedAnnotations sender nu en widget uden appearance gennem EnsureLoadedFieldAppearanceStream og bygger et Form XObject for markup uden appearance ud fra annotationens egne egenskaber, så værdier, der er skrevet i en /NeedAppearances-formular, overlever i sidens content i stedet for at forsvinde under flattening. Fejlen, der tvang denne ændring frem, ligner et no-op. En kunde sender en udfyldt ansøgningsformular, printet til PDF fra en browser. Du indlæser den i HotPDF, kalder FlattenLoadedAnnotations, får 0 tilbage, gemmer og leverer et dokument med tomme bokse dér, hvor ansøgeren skrev et navn og et beløb. Intet rejste en fejl, intet blev logget. Værdierne lå i filen hele tiden i hvert felts /V-post, og flatten-passet gik lige forbi dem, fordi ingen af disse widgets havde en appearance-stream, der kunne bages ind
Hvorfor mister en browser-printet formular de indtastede værdier ved flattening?
Fordi en /NeedAppearances-formular gemmer værdien uden at gemme et billede af værdien. ISO 32000-1 12.7.2 tillader, at en interaktiv formular sætter /NeedAppearances true i AcroForm-ordbogen, hvilket fortæller vieweren, at den ved åbning skal konstruere hvert felts visuelle overflade ud fra /V, /DA og /Q. Producenter, der genererer formularer billigt — browser-printstier, server-side-fillere og nogle scanning-frontends — tager imod tilbuddet og skriver slet ingen /AP. Flattening, som defineret af appearance-algoritmen i ISO 32000-1 12.5.5, er et transskriptionsjob: Tag annotationens normale appearance-stream, map dens /BBox til dens /Rect, kald den fra sidens content-stream med en Do-operator, og slet derefter annotationen. Uden en kildestream er der intet at transskribere. Den oprindelige HotPDF-implementering fra v2.386.0 behandlede det som "skip", hvilket kan forsvares isoleret og er katastrofalt samlet set: De dokumenter, der mest sandsynligt har brug for flattening, er dem, der mindst sandsynligt har appearances. Det samme hul slugte markup — en Highlight fra et reviewværktøj, en Square fra en redline-gennemgang og en Ink-signatur — når producenten lod vieweren tegne den
Hvor kobler HotPDF syntesen ind i FlattenLoadedAnnotations?
Hookpunktet ligger bevidst sent: efter at appearance-opslaget fejler og ikke før. FlattenLoadedAnnotations spørger stadig først GetLoadedAnnotationAppearanceStream efter den normale appearance, og en annotation, der allerede har en, bages ind præcis som i v2.386.0. Kun et nil-resultat på en annotation med en ikke-degenereret /Rect og uden hidden-flag går ind i syntesestien. Rækkefølgen betyder noget: En dokumentforfatter, der gjorde sig umage med at skrive en /AP, får sine egne bytes tilbage og ikke en HotPDF-rekonstruktion af 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);
// spørg igen: generatoren har tilføjet /AP /N til widgetten
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Herfra deler de to annotationfamilier sig. En widget opløses tilbage til sit ejende felt gennem GetLoadedFormFieldIndexForAnnotation og afleveres til EnsureLoadedFieldAppearanceStream, feltets appearance-generator, som har været i dette Delphi PDF-bibliotek siden v2.328.0. At genbruge den i stedet for at skrive en anden felt-renderer er hele pointen — den dækker allerede Type0-fonte, linjeombrydning, quadding, checkbox- og radio-/AS-tilstande samt /MK-rotation, den samme mekanik som bag tilføjelse af AcroForm-felter til en allerede indlæst PDF. Alt andet går til markup-syntetisatoren. For kalderen ændrer intet sig: Det samme one-line-flatten-kald returnerer nu et tal forskelligt fra nul på dokumenter, der tidligere returnerede nul
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgets og markup uden AP syntetiseres og bages derefter ind
Flattened:= Doc.FlattenLoadedAnnotations; // alle sider, alle 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;
Hvorfor lander QuadPoints og InkList det forkerte sted?
Fordi disse koordinater ligger i sidens user space, mens den syntetiserede appearance-stream tegner i sit eget /BBox-space, og de to origoer ikke er det samme punkt. ISO 32000-1 Tabel 176 definerer /QuadPoints for text-markup-annotationer i default user space, og Tabel 174 gør det samme for line-annotationens /L-endepunkter; /InkList følger den samme konvention. HotPDF giver den syntetiserede form en /BBox på [0 0 W H], hvis origo ligger ved det nederste venstre hjørne af /Rect. Derfor skal hvert punkt fra /QuadPoints, /L eller /InkList translateres med det negerede /Rect-nederste venstre hjørne, før det skrives ind i content-streamen. Gør du det forkert, tegner en highlight på en linje 700 points oppe på siden 700 points over sin egen boks, hvilket i praksis betyder, at den tegner ingen steder. Rettelsen er én subtraktion pr. koordinat, og den kombineres med den cm, som bake-operationen udsender bagefter — den matrix mapper /BBox tilbage til /Rect, så de to trin ophæver hinanden til korrekt absolut geometri
// /L-endepunkter er page user space (ISO 32000-1 Table 174); formens
// BBox-origo sidder ved /Rect's nederste venstre hjørne, så skift 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;
Hvad tegner den syntetiserede markup-appearance faktisk?
Markup-syntetisatoren læser kun annotationens dictionary og intet andet, hvilket holder outputtet forudsigeligt og ærligt om, hvad den ikke kan vide. FreeText og Stamp tegner /Contents med den font og farve, der er parsed ud af /DA, justeret efter /Q med 2 pt padding. Square og Circle tegner en re eller en firebuers Bezier-omridslinje, stroked i /C, udfyldt med /IC, når den findes, med bredden fra /BS /W. Line og Ink stroker deres vertices. Highlight udfylder hver quad, mens Underline, StrikeOut og Squiggly stroker en linje ved quadens bund, ved quadens midtpunkt eller som en zigzag på ét punkt. En /CA under 1 bliver til en ExtGState med en ca-post, der refereres som /GSA gs i begyndelsen af streamen
Tekstencoding afgøres ud fra AcroForm /DR /Font-posten, der navngives af /DA. Hvis den fonts /Subtype er Type0, skriver HotPDF strengen som en UTF-16BE-hexliteral med FEFF-byte order mark; ellers skriver den en escaped literal-streng, hvor parenteser og backslashes escapes, og bytes over 126 skrives i oktal. Tf-operatoren fra /DA udsendes før BT, hvilket er lovligt, fordi teksttilstanden fortsætter på tværs af tekstobjektets grænse, og det sparer at skille /DA-strengen ad. To begrænsninger bør siges klart. Linjebredde til wrapping og quadding estimeres med en half-em/full-em-heuristik i stedet for ægte fontmetrics, så justering på en proportional font er tæt på, men ikke præcis. Og en subtype uden noget, der kan syntetiseres — Popup, Link eller en Stamp, hvis eneste indhold er et ikonnavn — giver nil og lades urørt, præcis som før
Det midlertidige /Annots-skift, der straffer en hjælpsom oprydning
FlattenOneWidget, den per-widget-vej, som FlattenLoadedFormFields bruger, er en aliasing-fælde, som enhver ændring inde i det delte flatten-loop skal respektere. Den erstatter midlertidigt sidens /Annots-værdi med et array med ét element, så det generiske flatten-pass arbejder på én widget, og gendanner derefter den oprindelige PHPDFDictionaryItem-pointer i en finally-blok. Gendannelsen skriver tilbage i en dictionary-slot, som den fangede før kaldet
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; // dangling hvis den indre løkke frigav dette item
TemporaryAnnots.Free;
end;
Tilføj en fornuftig oprydning inde i den delte indre løkke — en DeleteValue('Annots'), når arrayet bliver tomt, så den gemte side ikke bærer et tomt overflødigt array — og det kald frigiver netop det dictionary-item, som DictItem peger på. finally skriver derefter gennem en dangling pointer, og processen dør med "Invalid pointer operation". To eksisterende tests fangede det straks, hvilket er den eneste grund til, at dette er en fodnote og ikke en supportsag. Reglen generaliserer: Før du tilføjer oprydning til en delt løkke, skal du kontrollere kaldere for alias- eller swap-kontrakter. Et tomt /Annots-array er en kosmetisk skønhedsfejl og er ikke værd at bytte væk for en garanti om pointerens levetid
Hvad forbliver ubagt, og hvad koster flattening?
Skjulte annotationer udelukkes med vilje. En annotation, hvis /F-heltal har bitposition 2 sat, er skjult ifølge ISO 32000-1 12.5.3, og når den samtidig ikke har nogen /AP, er det fristende at syntetisere en og bage den ind som resten. Det ville være en fejl med sikkerhedskonsekvenser: Hvis en usynlig note bages ind i sideindholdet, bliver den synlig for alle, der åbner filen. HotPDF lader disse annotationer ligge præcis, hvor de er, og tæller dem ikke i returværdien. Vær lige så tydelig over for dine brugere om prisen for dem, der faktisk bages ind. Flattening er irreversibel — annotationen slettes fra sidens /Annots-array, og dens visuelle udtryk er nu sideindhold, så der er ikke længere nogen redigering af feltværdien, ingen kommentartråd, ingen toggling af /AS-tilstand og ingen måde at gendanne de strukturerede data på uden originalfilen. Fladgør en kopi, behold originalen, og brug det kun, når dokumentet holder op med at være en formular og bliver en record. Hvis dit problem er XFA-baseret i stedet for appearance-løst, er den separate XFA til AcroForm-flattening i HotPDF det rigtige sted at starte, og hvis du stadig bygger formularen, dækker noterne om tilkobling af AcroForm-felthandlinger og validering skrivesiden
Én verificeringsdetalje, fordi den ellers koster dig en eftermiddag. ExtractLoadedPageGlyphs går ikke ned i Form XObjects, og en indbagt appearance lever inde i ét — side-content-streamen indeholder kun en q ... cm /FlatAn<n> Do Q-sekvens. Glyfudtrækning fra en fladgjort side rapporterer derfor ingenting, og det er korrekt adfærd og ikke en tabt bake-operation. Verificér enten på byte-niveau ved at kontrollere resourcenavnet /FlatAn, Do-kaldet og /Subtype /Form, eller gennem rendering-pipelinen, som faktisk udvider XObjects
Annotation-flattening ligner tre linjer transskription, lige indtil du møder de dokumenter, folk faktisk genererer. Hvis du arbejder med udfyldte formularer, review-markup eller arkivoutput i Delphi eller C++Builder, er det værd at læse, hvordan HotPDF Delphi PDF-komponenten håndterer den indlæste dokumentside af AcroForms og annotationer, før du bygger din egen appearance-generator oven på den