HotPDF v2.743.0 flater ut PDF-annotasjoner uten en /AP-utseendestrøm i stedet for å hoppe stille over dem. FlattenLoadedAnnotations sender nå en widget uten utseende gjennom EnsureLoadedFieldAppearanceStream og bygger et Form XObject for annotasjoner uten utseende fra annotasjonens egne egenskaper, slik at verdiene som er skrevet inn i et /NeedAppearances-skjema overlever i sideinnholdet i stedet for å forsvinne ved flattening. Feilen som tvang frem denne endringen, ser ut som en no-op. En kunde sender et utfylt søknadsskjema som ble skrevet ut til PDF fra en nettleser. Du laster det i HotPDF, kaller FlattenLoadedAnnotations, får tilbake 0, lagrer og leverer et dokument med tomme bokser der søkeren skrev inn et navn og et beløp. Ingenting kastet, ingenting ble logget. Verdiene lå i filen hele tiden, i hvert felts /V-oppføring, og flatten-passet gikk rett forbi dem fordi ingen av disse widgetene hadde en utseendestrøm å bake inn
Hvorfor forsvinner de innskrevne verdiene når et nettleserutskrevet skjema flates ut?
Fordi et /NeedAppearances-skjema lagrer verdien uten å lagre et bilde av verdien. ISO 32000-1 12.7.2 lar et interaktivt skjema sette /NeedAppearances true i AcroForm-ordboken, noe som ber viseren bygge hvert felts visuelle overflate ved åpning fra /V, /DA og /Q. Produsenter som lager skjemaer billig — nettleserens utskriftssti, serverbaserte utfyllere og enkelte skannefronter — benytter seg av dette og skriver ingen /AP i det hele tatt. Flattening, slik det er definert av utseendealgoritmen i ISO 32000-1 12.5.5, er en transkripsjonsjobb: Ta annotasjonens normale utseendestrøm, map dens /BBox til dens /Rect, kall den fra sideinnholdsstrømmen med en Do-operator og slett deretter annotasjonen. Uten en kildestrøm finnes det ingenting å transkribere. Den opprinnelige HotPDF-implementasjonen fra v2.386.0 behandlet dette som "skip", noe som er forsvarlig isolert sett og katastrofalt samlet: Dokumentene som mest sannsynlig trenger flattening, er de som minst sannsynlig har utseender. Det samme hullet slukte markup — en Highlight fra et gjennomgangsverktøy, en Square fra en redline-runde eller en Ink-signatur — når produsenten stolte på at viseren skulle tegne den
Hvor kobler HotPDF syntesen inn i FlattenLoadedAnnotations?
Koblingspunktet er bevisst sent: etter at utseendeoppslaget feiler, ikke før. FlattenLoadedAnnotations ber fortsatt først GetLoadedAnnotationAppearanceStream om det normale utseendet, og en annotasjon som allerede har ett, bakes nøyaktig som i v2.386.0. Bare et nil-resultat på en annotasjon med en ikke-degenerert /Rect og uten skjult flagg går inn i syntesestien. Rekkefølgen betyr noe: En dokumentforfatter som tok seg bryet med å skrive en /AP, får sine egne byte tilbake, ikke en HotPDF-rekonstruksjon 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);
// spør igjen: generatoren har lagt /AP /N til widgeten
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Derfra deler de to annotasjonsfamiliene seg. En widget løses tilbake til det eierfeltet gjennom GetLoadedFormFieldIndexForAnnotation og sendes til EnsureLoadedFieldAppearanceStream, felt-utseendegeneratoren som har vært i dette Delphi PDF-biblioteket siden v2.328.0. Å gjenbruke den i stedet for å skrive en ny felt-renderer er hele poenget — den dekker allerede Type0-fonter, linjebryting, quadding, checkbox- og radio-tilstander i /AS og /MK-rotasjon, den samme maskineriet som ligger bak å legge AcroForm-felt til en allerede innlastet PDF. Alt annet går til markup-syntesen. For caller-en endres ingenting: Det samme flatten-kallet på én linje returnerer nå et tall ulikt null for dokumenter som tidligere returnerte null
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgeter og markup uten AP syntetiseres og bakes deretter inn
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 havner QuadPoints og InkList på feil sted?
Fordi disse koordinatene er i sidens user space, mens den syntetiserte utseendestrømmen tegner i sitt eget /BBox-rom, og de to origoene ikke er samme punkt. ISO 32000-1 Table 176 definerer /QuadPoints for tekstmarkup-annotasjoner i standard user space, og Table 174 gjør det samme for /L-endepunktene til linjeannotasjonen; /InkList følger samme konvensjon. HotPDF gir det syntetiserte skjemaet en /BBox på [0 0 W H] der origo ligger ved nedre venstre hjørne av /Rect. Derfor må hvert punkt som hentes fra /QuadPoints, /L eller /InkList, flyttes med den negerte nedre venstre delen av /Rect før det skrives inn i innholdsstrømmen. Gjør du dette feil og en markering på en linje 700 punkt opp på siden tegnes 700 punkt over sin egen boks, noe som i praksis betyr at den tegnes ingen steder. Rettelsen er én subtraksjon per koordinat, og den komponeres med cm-en som bakingen skriver etterpå — den matrisen mapper /BBox tilbake på /Rect, så de to trinnene kansellerer hverandre til korrekt absolutt geometri
// /L-endepunkter er i sidens user space (ISO 32000-1 Table 174); formens
// BBox-origo ligger ved nedre venstre hjørne av /Rect, så flytt 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;
Hva tegner det syntetiserte markup-utseendet egentlig?
Markup-syntesen leser annotasjonsordboken og ingenting annet, noe som gjør output-en forutsigbar og ærlig om det den ikke kan vite. FreeText og Stamp tegner /Contents med fonten og fargen som parses ut av /DA, justert etter /Q, med 2 punkt padding. Square og Circle tegner en re- eller en fire-buet Bezier-omrisslinje strøket i /C, fylt med /IC når den finnes, med bredden fra /BS /W. Line og Ink stryker toppunktene sine. Highlight fyller hver firkant, mens Underline, StrikeOut og Squiggly stryker en linje ved firkantens bunn, ved firkantens midtpunkt eller som en siksak på ett punkt. En /CA under 1 blir til en ExtGState med en ca-oppføring, referert som /GSA gs på starten av strømmen
Tekstkodingen bestemmes fra AcroForm-oppføringen /DR /Font som /DA navngir. Hvis den fontens /Subtype er Type0, skriver HotPDF strengen som en UTF-16BE-hex-literal med FEFF-byteordremarkøren. Ellers skriver den en escaped literal-streng, med parenteser og backslashes escaped og byte over 126 skrevet oktalt. Tf-operatoren fra /DA skrives før BT, noe som er lovlig fordi teksttilstanden fortsetter over tekstobjektgrensen, og det sparer å plukke /DA-strengen fra hverandre. To begrensninger bør sies tydelig. Linjebredden for bryting og quadding anslås med en halv-em/full-em-heuristikk i stedet for reelle fontmetrikker, så justering med en proporsjonal font er nær, men ikke nøyaktig. Og en subtype uten noe som kan syntetiseres — Popup, Link eller en Stamp hvis eneste innhold er et ikonnavn — gir nil og blir stående urørt, nøyaktig som før
Det midlertidige /Annots-bytte som straffer hjelpsom opprydding
FlattenOneWidget, stien per widget som brukes av FlattenLoadedFormFields, er en aliasfelle som enhver endring inne i den delte flatten-løkken må respektere. Den erstatter midlertidig sidens /Annots-verdi med en matrise med ett element slik at den generiske flatten-runden arbeider på én widget, og gjenoppretter deretter den opprinnelige PHPDFDictionaryItem-pekeren i en finally-blokk. Gjenopprettingen skriver tilbake til en ordboksslot den fanget før kallet
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; // hengende hvis den indre løkken frigjorde dette elementet
TemporaryAnnots.Free;
end;
Legg til en rimelig ryddig opprydding inne i den delte indre løkken — en DeleteValue('Annots') når matrisen blir tom, slik at den lagrede siden ikke har en tom restmatrise — og det kallet frigjør selve ordbokselementet DictItem peker på. finally-blokken skriver deretter gjennom en dinglende peker, og prosessen dør med "Invalid pointer operation". To eksisterende tester fanget det umiddelbart, og det er den eneste grunnen til at dette er en fotnote og ikke en supporthenvendelse. Regelen generaliserer: Før du legger opprydding til en delt løkke, sjekk caller-ene for alias- eller byttekontrakter. En tom /Annots-matrise som blir liggende, er en kosmetisk skavank og ikke verdt å bytte mot en garanti for pekerens levetid
Hva forblir ubaket, og hva koster flattening?
Skjulte annotasjoner utelates med hensikt. En annotasjon hvis /F-heltall har bitposisjon 2 satt, er skjult i henhold til ISO 32000-1 12.5.3, og når den også mangler /AP, er det fristende å syntetisere ett og bake det inn som resten. Det ville vært en feil med sikkerhetskonsekvenser: Å bake en usynlig merknad inn i sideinnholdet gjør den synlig for alle som åpner filen. HotPDF lar disse annotasjonene være akkurat der de er og teller dem ikke i returverdien. Vær like tydelig med brukerne dine om prisen for dem som faktisk bakes inn. Flattening kan ikke reverseres — annotasjonen slettes fra sidens /Annots-matrise, og det visuelle er nå sideinnhold, så det finnes ingen videre redigering av feltverdien, ingen kommentartråd, ingen veksling av /AS-tilstand og ingen måte å gjenopprette de strukturerte dataene på uten originalfilen. Flatten en kopi, behold originalen og bruk det bare der dokumentet slutter å være et skjema og blir en post. Hvis problemet ditt er XFA-basert i stedet for uten utseende, er den separate XFA til AcroForm-flaten i HotPDF stedet å starte, og hvis du fortsatt bygger skjemaet, dekker notatene om kobling av AcroForm-felthandlinger og validering skrivesiden
Én verifikasjonsadvarsel, fordi den ellers vil koste deg en ettermiddag. ExtractLoadedPageGlyphs går ikke ned i Form XObjects, og et bakt utseende ligger inne i ett — sideinnholdsstrømmen inneholder bare en sekvens av typen q ... cm /FlatAn<n> Do Q. Glyph-uthenting på en flatet side rapporterer derfor ingenting, og det er korrekt oppførsel, ikke en tapt bakejobb. Verifiser enten på bytenivå ved å sjekke ressursnavnet /FlatAn, Do-kallet og /Subtype /Form, eller gjennom renderingspipelinen, som faktisk utvider XObjects
Annotasjonsflattening ser ut som tre linjer med transkripsjon helt til du møter dokumentene folk faktisk genererer. Hvis du arbeider med utfylte skjemaer, gjennomgangsmarkup eller arkivutdata i Delphi eller C++Builder, er det verdt å lese hvordan HotPDF Delphi PDF-komponenten håndterer den innlastede dokumentsiden av AcroForms og annotasjoner, før du bygger din egen utseendegenerator oppå den