Technisch artikel

Delphi PDF-annotatie-review met PDFium Component

Een PDF-annotatie is een dictionary die aan een pagina hangt, geen teken die erop getekend is. ISO 32000-1 §12.5 definieert ruwweg twee dozijn subtypes, en elk draagt een /Subtype, een rechthoek in paginacoördinaten, een set flags, en meestal een appearance-stream die beslist wat een viewer werkelijk verft. De subtypes betekenen niet allemaal hetzelfde voor iemand die een document reviewt. Een Highlight en een Ink-streek zijn commentaar; een Link is navigatie; een Popup is het kleine venster dat opent wanneer u op een memopunt klikt, opgeslagen als zijn eigen object en aangewezen door een ouder. Antwoorden zijn volwaardige Text-annotaties die verwijzen naar het commentaar dat ze beantwoorden via een in-reply-to-vermelding. Dus de annotatie-array op paginaniveau is niet de commentarenlijst van de reviewer. Het is een platte zak met commentaren, de leidingen die ze verbinden, en verschillende dingen die geen enkele reviewer een commentaar zou noemen. Een paneel dat de array als commentarenlijst behandelt is het oneens met elke andere viewer die de klant draait

Een annotatie-review-workflow bouwen op PDFium Component, de op PDFium gebaseerde VCL/LCL-component voor Delphi, C++Builder en Lazarus, betekent concentreren op de punten waar die kloof tussen de ruwe array en de menselijke weergave problemen veroorzaakt: tellen, indexeren, herkleuren van markeringen die de engine al bevroren heeft, verwijderen zonder geesten achter te laten, en eigen markeringen toevoegen

Diagram dat toont hoe een Delphi PDFium-reviewpaneel de rauwe pagina-annotatiearray van commentaren, popups, antwoorden en links filtert in de gecureerde commentarenlijst die de reviewer ziet
De pagina-annotatie-array mengt commentaren met popups, antwoorden, links en verborgen markeringen, dus een review-paneel heeft een telregel nodig voordat het een totaal toont

Waarom uw telling nooit klopt met het commentarenpaneel van Acrobat

Open een gemarkeerd contract in uw viewer en in Acrobat naast elkaar en de totalen komen zelden overeen. Acrobat toont een gecureerde weergave: markup gegroepeerd in antwoordthreads, populs samengevoegd in de notities waartoe ze behoren, links en form-widgets weggelaten. De ruwe array houdt alles ongedifferentieerd vast, dus een naïeve telling loopt op sommige manieren hoog en op andere tegelijk laag

Populs blazen het totaal op, omdat elk memopunt met een apart Popup-object meekomt en beide tellen de notitie verdubbelt. Antwoorden laten het zakken als u op zichtbare markeringen filtert, want een antwoord is een Text-annotatie waarbij niets geschilderd is tot iemand de thread uitklapt, en het weggooien ervan verliest de discussie. De flags Hidden en NoView halen een annotatie van het scherm zonder hem uit de array te halen, dus een flag-blinde telling omvat markeringen die de gebruiker niet kan zien. Link-annotaties zitten in dezelfde array als de commentaren en horen noch in de telling noch in de lijst thuis. Beslis de telregel voordat u de lus schrijft, en schrijf de beslissing op, want "waarom toont uw paneel een ander aantal dan Acrobat" is het eerste ticket dat een review-feature verdient

Indexeer alles eenmaal, parse dan nooit meer een pagina

Eén ontwerpregel stuurt alles wat volgt: filteren op auteur, type of pagina mag nooit pagina-objecten herparsen. Op een document van 300 pagina's met zware markup verandert herparsen bij elke dropdown-wissel het paneel in iets dat secondenlang hapert. De component stelt AnnotationCount en de geïndexeerde eigenschap Annotation[] bloot, beide scoped tot de momenteel geladen pagina, en het record TPdfAnnotation dat ze teruggeven draagt wat een listview nodig heeft: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. De juiste zet is bij openen elke pagina eenmaal af te lopen en uw eigen platte index bij te houden:

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];
      // Houd alleen de subtypes die voor de reviewer relevant zijn; noteer het paar
      // pagina en index, want alle latere bewerkingen adresseren daarop
      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;

Het paar dat onderstreept verdient is (PageNo, i). Elke latere mutatie, of het nu een herkleuring of een verwijdering is, wordt geadresseerd met paginanummer plus annotatie-index, en de index is breekbaar: een annotatie verwijderen hernoemt alles wat er op die pagina op volgt. Plan daarom na elke verwijdering de items van de betreffende pagina opnieuw op te bouwen in plaats van indexnummers in-place te patchen. De herbouw kost een milliseconde. Een stalen index verwijdert daarentegen het commentaar van de verkeerde reviewer, het soort bug dat vertrouwen in de hele feature ondermijnt

Threading verdient een plek in de index, zelfs als uw eerste release alleen antwoorden telt in plaats van ze te tonen. Groepeer items op hun ouderverwijzing terwijl u de pagina open hebt, zodat het paneel later een thread kan samenvouwen zoals Acrobat doet. Dat groeperen lui reconstrueren tijdens het scrollen verslaat het hele punt van eenmaal indexeren, omdat het pagina's heropent die u al betaald hebt te parsen. Geometrie wil dezelfde discipline. De Rectangle in elk record is in paginaruimte, en die omzetten naar view-coördinaten hoort in één gedeelde helper, niet verspreid door de code. Panelen groeien coördinaatbugs wanneer selectie, hit-testing en schilderen elk hun eigen zoom- en rotatierekenkunde uitvinden; leid alle drie door één conversie en een markering, zijn rij in de lijst, en zijn klikdoel blijven gepind op dezelfde inkt

Markup herkleuren en de appearance-stream-veto

Een markering van geel naar amber veranderen klinkt als een one-liner, en soms is het dat. De adder is ISO 32000-1 §12.5.5. Wanneer een annotatie een /AP-appearance-stream draagt, verft een conforming-viewer die voorgebouwde stream en behandelt de kleuringang in de dictionary als dode metadata. Acrobat schrijft appearance-streams voor in wezen alles wat het aanmaakt, dus de meeste annotaties die van klanten binnenkomen staan al in deze staat, en de kleur die u zo zelfverzekerd instelt bereikt nooit het scherm. Herkleuren is een read-modify-write via de eigenschap Annotation[], en de component is eerlijk over het conflict: wanneer de engine weigert een dictionary-kleur een ingebakken appearance te laten overstemmen, raiset de schrijfbewerking EPdfError

Diagram van het read-modify-write-herkleurpad in een Delphi PDFium-component waarin een ingebakken appearance stream de woordenboekkleur vetoert en EPdfError opwerpt
Wanneer een annotatie een vooraf gebouwde /AP-stream draagt, weigert de engine de dictionary-kleur en geeft EPdfError, dus het paneel herkleurt zijn eigen overlay of markeert de rij als appearance-locked
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // De annotation heeft een voorgerenderde /AP-stream; de kleur in de
    // dictionary alleen verandert niet wat viewers tekenen
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Vang die exception telkens, en behandel haar als informatie in plaats van falen. Sla de bewaking over en uw paneel toont vrolijk amber in zijn eigen lijst terwijl de pagina geel blijft schilderen; de gebruiker dient het weken later in als "uw viewer negeert mijn bewerkingen," en u verspilt een middag eraan om het niet te kunnen reproduceren op een bestand dat toevallig geen appearance-stream heeft. Zodra u weet dat de appearance vergrendeld is, hebt u twee eerlijke antwoorden: herkleur uw eigen selectie-overlay in plaats van de annotatie, zodat de reviewer in ieder geval de markering ziet die hij koos, of markeer de rij als appearance-vergrendeld zodat niemand verwacht dat de wijziging beklijft

Annotaties verwijderen zonder geesten achter te laten

DeleteAnnotation verwijdert het object uit de annotatie-boom van de huidige pagina, maar laat de gecachte pagina-raster met rust. Schilder onmiddellijk na de aanroep en de verwijderde markering staat nog op het scherm, in een bitmap die niet langer overeenkomt met het documentmodel erachter. De oplossing is de re-render als onderdeel van de verwijdering te behandelen, geen stap die de aanroeper zou kunnen vergeten:

Diagram van de driestaps-Delphi PDFium-verwijdercyclus die de annotatie verwijdert, de pagina hertekent met reAnnotations en de pagina-index herbouwt
Verwijderen raakt alleen de annotatieboom aan, dus het paneel moet opnieuw renderen met reAnnotations en de pagina-items herbouwen voordat de weergave en de index weer eerlijk zijn
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // gooit EPdfError bij mislukken
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage draagt het eigendom van de bitmap over aan de aanroeper
end;
RebuildPageEntries(Item.PageNo);  // indices na Item.Index zijn opgeschoven

Twee details in dat blok gaan makkelijk fout. De optie reAnnotations moet aanwezig zijn, anders laat de nieuwe raster elke overgebleven annotatie vallen en ziet de pagina eruit alsof u de hele commentareset wiste in plaats van één markering. En Bmp.Free is niet optioneel: de functievormige RenderPage-overload geeft bitmapeigendom aan de aanroeper, dus een ontbrekende free lekt een volledige pagina-raster bij elke verwijdering, wat een reviewer die een lang document doorwerkt binnen enkele minuten omzet in echte geheugendruk

Reviewmarkeringen toevoegen vanuit uw eigen UI

Annotaties aanmaken verloopt via CreateAnnotation, dat een gevuld record TPdfAnnotation (subtype, rechthoek, kleur, inhoud, auteur) neemt en het aan de huidige pagina hangt. Een memopunt, subtype anText, is het makkelijke geval: zet de positie, de inhoud en de auteur en u bent klaar. Ink-annotaties zijn waar mensen betrapt worden. De rechthoek van het record begrenst alleen de tekening; de streken zelf zijn arrays van punten die apart gehecht moeten worden via de ink-stroke-aanroep van de engine, FPDFAnnot_AddInkStroke gevoed met FS_POINTF-data, stuk voor stuk vastgelegd vanuit muis- of pen-input. Bouw een ink-annotatie uit een rechthoek en verder niets en u krijgt een lege krabbel die als witruimte rendert, wat eruitziet als een bug in de engine maar in werkelijkheid een half afgemaakte annotatie is

Regel het auteurschapbeleid in dezelfde adem. Elke markering die uw UI aanmaakt zou een consistente AuthorText moeten dragen, want het reviewerfilter dat u volgende maand bouwt is slechts zo goed als de namen die u vandaag op commentaren stempelt. Lege of inconsistente auteursstrings kunnen niet retroactief hersteld worden zonder elk bestand te heropenen

De review uit de viewer krijgen

Reviewdata verdient zijn plaats zodra die de viewer kan verlaten, als een samenvatting die de projectleider leest zonder het bestand te openen of een CSV dat een trackingsheet voedt. Exporteer uit de index die u al bouwde, nooit vanuit een verse parse, en kies een stabiele manier om naar elke markering terug te verwijzen. Een paginanummer gekoppeld aan de rechthoek van de annotatie overleeft round-trips die een array-index niet overleeft, want de volgende verwijdering hernoemt de indices stilletjes en uw CSV wijst naar de verkeerde commentaren

Een rij die bewaard moet worden draagt de pagina, het subtype, de auteur, het aanmaaktijdstempel wanneer het bestand er een registreert, de inhoudstekst, en een statuskolom die u bezit in plaats van een die de PDF levert. Dezelfde indexeringspass is eerder nuttig, tijdens intake, wanneer een document van buiten het team binnenkomt en u wilt weten wat erin zit voordat iemand het reviewt. Het artikel over de PDF-intakewerkbank loopt door die triage, en form-field-navigatie behandelt het spiegelbeeldprobleem: documenten reviewen die gebouwd zijn om data te verzamelen in plaats van commentaar

Eén geval dat de array u niet toont

Eén falingsmodus verdient een vlag omdat hij eruitziet als een defect in uw code en dat niet is. Een klant rapporteert zichtbare markeringen over een hele pagina, maar uw paneel toont niets, en AnnotationCount komt nul terug. De gebruikelijke verklaring is dat de markeringen ergens upstream geflattened zijn. Flatten bakken annotatie-appearances in gewone pagina-inhoud, dus de markeringen worden deel van de paginagrafiek en houden op te bestaan als annotatie-objecten. Er is niets over voor een annotatie-API om te enumereren, te herkleuren of te verwijderen. Wanneer u geschilderde markup ziet met een nultelling, stop dan met de bug te zoeken in uw enumeratielus en vraag hoe het bestand geproduceerd is

Het annotatie-oppervlak dat hier gebruikt wordt, van enumeratie en aanmaken via herkleuring, verwijdering en de render-opties die de weergave eerlijk houden, wordt meegeleverd met PDFium Component voor Delphi, C++Builder en Lazarus/FPC