Teknisk artikel

Delphi PDF-annoteringsgennemgang med PDFium-komponent

En PDF-annotation er en dictionary knyttet til en side, ikke et mærke tegnet på den. ISO 32000-1 §12.5 definerer omtrent to dusin subtyper, og hver bærer en /Subtype, et rektangel i sidekoordinater, et sæt flag og som regel en appearance stream, der afgør, hvad en fremviser rent faktisk maler. Subtyperne betyder ikke alle det samme for en person, der gennemgår et dokument. Et Highlight og et Ink-strøg er kommentarer; et Link er navigation; en Popup er det lille vindue, der åbner, når man klikker på en post-it, gemt som sit eget objekt og peget på af en forælder. Svar er fuldgyldige Text-annotationer, der refererer til den kommentar, de besvarer, gennem en in-reply-to-post. Så sideniveau-annotationsarrayet er ikke reviewerens liste over kommentarer. Det er en flad pose, der indeholder kommentarer, rørføringen der forbinder dem, og adskillige ting, ingen reviewer overhovedet ville kalde en kommentar. Et panel, der behandler arrayet som kommentarlisten, vil være uenig med enhver anden fremviser, kunden kører

At bygge en annotations-review-workflow på PDFium Component, den PDFium-baserede VCL/LCL-komponent til Delphi, C++Builder og Lazarus, betyder at koncentrere sig om de punkter, hvor det gab mellem det rå array og det menneskelige overblik giver problemer: optælling, indeksering, omfarvning af mærker, motoren allerede har frosset, sletning uden at efterlade spøgelser, og tilføjelse af egne mærker

Diagram, der viser, hvordan et Delphi PDFium reviewpanel filtrerer det rå sideannoteringsarray af kommentarer, popups, svar og links ind i den kuraterede kommentarliste, som revieweren ser
Sidens annoteringsarray blander kommentarer med popups, svar, links og skjulte markeringer, så et review-panel behøver en tælleregel, før det viser en total

Hvorfor dit antal aldrig matcher Acrobats kommentarpanel

Åbn en markeret kontrakt i din fremviser og i Acrobat side om side, og totalerne stemmer sjældent overens. Acrobat viser et kurateret overblik: markup grupperet i svartråde, popups foldet ind i de noter, de hører til, links og formularwidgets udeladt. Det rå array holder det hele udifferentieret, så en naiv optælling ligger for højt på nogle måder og for lavt på andre samtidig

Popups opblæser totalen, fordi hver post-it følger med et separat Popup-objekt, og at tælle begge fordobler noten. Svar undertrykker den, hvis du filtrerer på synlige mærker, da et svar er en Text-annotation med intet malet, før nogen udvider tråden, og at droppe det mister diskussionen. Flagene Hidden og NoView tager en annotation af skærmen uden at tage den ud af arrayet, så en flag-blind optælling inkluderer mærker, brugeren ikke kan se. Link-annotationer sidder i det samme array som kommentarerne og hører hverken hjemme i optællingen eller på listen. Beslut optællingsreglen, før du skriver løkken, og skriv beslutningen ned, for "why does your panel show a different number than Acrobat" er den første sag, en review-funktion tjener sig

Indekser alt én gang, og genparser aldrig en side

Én designregel driver alt, hvad der følger: filtrering efter forfatter, type eller side må aldrig genparse sideobjekter. På et 300-siders dokument med kraftig markup forvandler genparsing ved hvert dropdown-skift panelet til noget, der hakker i sekunder ad gangen. Komponenten eksponerer AnnotationCount og den indekserede Annotation[]-egenskab, begge afgrænset til den aktuelt indlæste side, og TPdfAnnotation-recorden, de afleverer, bærer det, en listevisning har brug for: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Det rigtige træk er at feje hver side igennem én gang ved åbningstid og holde dit eget flade indeks:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Behold kun reviewer-relevante subtyper; registrer side- og
      // indeks-par, fordi alle senere redigeringer adresseres med det
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

Parret, det er værd at understrege, er (PageNo, i). Hver senere mutation, hvad enten det er en omfarvning eller en sletning, adresseres med sidenummer plus annotationsindeks, og indekset er skrøbeligt: at fjerne en annotation omnummererer alt efter den på den side. Så planlæg at genopbygge den berørte sides poster efter enhver sletning i stedet for at lappe indeksnumre på plads. Genopbygningen koster ét millisekund. Et forældet indeks sletter derimod den forkerte reviewers kommentar, hvilket er den slags bug, der eroderer tilliden til hele funktionen

Trådning fortjener en plads i indekset, selv hvis din første udgivelse kun tæller svar frem for at vise dem. Grupper elementer efter deres forælder-reference, mens du har siden åben, så panelet senere kan folde en tråd, ligesom Acrobat gør. At genopbygge den gruppering dovent under scrolling ødelægger hele pointen med at indeksere én gang, fordi det genåbner sider, du allerede har betalt for at parse. Geometri kræver den samme disciplin. Rectangle i hver record er i sideplan, og at konvertere det til view-koordinater hører hjemme i én delt hjælpefunktion, ikke spredt gennem koden. Paneler avler koordinatbugs, når markering, hit-testing og maling hver især opfinder deres egen zoom- og rotationsmatematik; rout alle tre gennem én enkelt konvertering, og en fremhævning, dens række på listen og dens klikmål forbliver fastgjort til det samme blæk

Omfarvning af markup og appearance-stream-vetoet

At ændre en fremhævning fra gul til ravgul lyder som en énliner, og nogle gange er det det. Fælden er ISO 32000-1 §12.5.5. Når en annotation bærer en /AP appearance stream, maler en konform fremviser den forudbyggede stream og behandler farveposten i dictionary'en som død metadata. Acrobat skriver appearance streams for stort set alt, den opretter, så de fleste annotationer, der kommer fra kunder, er allerede i denne tilstand, og farven, du så selvsikkert satte, når aldrig skærmen. Omfarvning er en read-modify-write gennem Annotation[]-egenskaben, og komponenten er ærlig om konflikten: når motoren nægter at lade en dictionary-farve overtrumfe en indbagt appearance, rejser skrivningen EPdfError

Diagram over read-modify-write-omfarvningsstien i en Delphi PDFium-komponent, hvor en bagt appearance stream nedlægger veto mod ordbogsfarven og rejser EPdfError
Når en annotering bærer en forudbygget /AP-stream, nægter motoren ordbogens farve og udløser EPdfError, så panelet genfarver sin egen overlay eller markerer rækken som appearance-låst
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // ravgul
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // Annotationen ejer en forudrenderet /AP-stream; dictionary-
    // farven alene kan ikke ændre, hvad fremvisere maler
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Fang den undtagelse hver eneste gang, og behandl den som information frem for fejl. Spring beskyttelsen over, og dit panel viser muntert ravgul på sin egen liste, mens siden fortsætter med at male gul; brugeren melder det uger senere som "your viewer ignores my edits," og du bruger en eftermiddag på forgæves at forsøge at genskabe det på en fil, der tilfældigvis ikke har nogen appearance stream. Når du ved, appearance'en er låst, har du to ærlige svar: omfarv dit eget markeringsoverlay i stedet for annotationen, så reviewer i det mindste ser den fremhævning, de valgte, eller marker rækken som appearance-låst, så ingen forventer, at ændringen holder

Sletning af annotationer uden at efterlade spøgelser

DeleteAnnotation fjerner objektet fra den aktuelle sides annotationstræ, men den lader den cachede sideraster være i fred. Mal straks efter kaldet, og den slettede fremhævning er stadig på skærmen, siddende i et bitmap, der ikke længere matcher dokumentmodellen bag den. Løsningen er at behandle genrenderingen som en del af sletningen, ikke et trin, den kaldende kode måske glemmer:

Diagram over den tretrins Delphi PDFium sletningscyklus, der fjerner annoteringen, renderer siden igen med reAnnotations og genopbygger sideindekset
Sletning rører kun annoteringstræet, så panelet må re-rendere med reAnnotations og genopbygge side-posterne, før visning og indeks er ærlige igen
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // rejser EPdfError ved fejl
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage overdrager bitmap-ejerskab til den kaldende kode
end;
RebuildPageEntries(Item.PageNo);  // indekser efter Item.Index er rykket

To detaljer i den blok er lette at få galt. reAnnotations-tilvalget skal være til stede, ellers dropper den nye raster hver eneste tilbageværende annotation, og siden ser ud, som om du har visket hele kommentarsættet ud i stedet for ét mærke. Og Bmp.Free er ikke valgfri: den funktionsagtige RenderPage-overload overdrager bitmap-ejerskab til den kaldende kode, så en manglende free lækker en helsides raster ved hver eneste sletning, hvilket en reviewer, der arbejder sig gennem et langt dokument, vil forvandle til reelt hukommelsespres inden for få minutter

Tilføjelse af reviewer-mærker fra dit eget UI

Oprettelse af annotationer går gennem CreateAnnotation, som tager en udfyldt TPdfAnnotation-record (subtype, rektangel, farve, indhold, forfatter) og knytter den til den aktuelle side. En post-it, subtype anText, er det nemme tilfælde: sæt positionen, indholdet og forfatteren, og du er færdig. Ink-annotationer er, hvor folk bliver fanget. Recordens rektangel afgrænser kun tegningen; selve strøgene er arrays af punkter, der skal tilføjes separat gennem motorens ink-strøg-kald, FPDFAnnot_AddInkStroke fodret med FS_POINTF-data, opfanget fra mus- eller penneinput ét strøg ad gangen. Byg en ink-annotation ud fra et rektangel og intet andet, og du får en tom krusedulle, der renderer som tomt rum, hvilket ligner en bug i motoren og i virkeligheden er en halvfærdig annotation

Fastlæg forfatterskabspolitikken i samme åndedrag. Hvert mærke, dit UI opretter, bør bære en konsistent AuthorText, fordi det reviewer-filter, du bygger næste måned, kun er så godt som de navne, du stempler på kommentarer i dag. Tomme eller inkonsistente forfatterstrenge kan ikke repareres retroaktivt uden at genåbne hver fil

Få reviewet ud af fremviseren

Review-data tjener sin plads, når det kan forlade fremviseren, som en oversigt, projektlederen læser uden at åbne filen, eller en CSV, der fodrer et sporingsark. Eksporter fra det indeks, du allerede har bygget, aldrig fra en frisk parsing, og vælg en stabil måde at referere tilbage til hvert mærke på. Et sidenummer parret med annotationens rektangel overlever rundture, et arrayindeks ikke gør, fordi den næste sletning stiltiende omnummererer indekserne, og din CSV begynder at pege på de forkerte kommentarer

En række, det er værd at beholde, bærer siden, subtypen, forfatteren, oprettelsestidsstemplet, når filen registrerer ét, indholdsteksten og en statuskolonne, du ejer, frem for én, PDF'en leverer. Den samme indekseringsgennemgang er nyttig tidligere, under indtag, når et dokument ankommer udefra teamet, og du vil vide, hvad der er i det, før nogen gennemgår det. Artiklen om PDF-indtagsworkbenchen gennemgår den triage, og navigation i formularfelter dækker spejlbilledproblemet: gennemgang af dokumenter bygget til at indsamle data frem for kommentarer

Ét tilfælde, arrayet ikke vil vise dig

Én fejltilstand fortjener et flag, fordi den ligner en defekt i din kode og ikke er det. En kunde rapporterer synlige fremhævninger over hele en side, men dit panel lister intet, og AnnotationCount kommer tilbage med nul. Den sædvanlige forklaring er, at mærkerne blev fladet et sted opstrøms. Flattening bager annotations-appearances ind i almindeligt sideindhold, så fremhævningerne bliver en del af sidegrafikken og holder helt op med at eksistere som annotationsobjekter. Der er intet tilbage for et annotations-API at opremse, omfarve eller slette. Når du ser malet markup med et nultal, så hold op med at lede efter bugen i din opremsningsløkke, og spørg, hvordan filen blev produceret

Annotationsfladen, der bruges her, fra opremsning og oprettelse gennem omfarvning, sletning og de render-tilvalg, der holder visningen ærlig, følger med PDFium Component til Delphi, C++Builder og Lazarus/FPC