PDF-huomautus (annotation) on sivulle kiinnitetty sanakirja (dictionary), ei sille piirretty merkki. ISO 32000-1 §12.5 määrittelee karkeasti kaksi tusinaa alatyyppiä (subtypes), ja jokainen kantaa /Subtype-merkintää, sivuavaruuden koordinaattien (page coordinates) suorakulmiota, lippujen (flags) joukkoa, ja yleensä ulkoasuvirtaa (appearance stream), joka päättää, mitä katseluohjelma (viewer) todellisuudessa maalaa (paints). Alatyypit eivät suinkaan tarkoita samaa asiaa ihmiselle, joka tarkastelee asiakirjaa. Korostus (Highlight) ja musteveto (Ink stroke) ovat kommentteja; linkki (Link) on navigointia; ponnahdusikkuna (Popup) on pieni ikkuna, joka aukeaa kun napsautat muistilappua (sticky note), ja se tallennetaan omana objektinaan, johon ylempi (parent) objekti osoittaa. Vastaukset (Replies) ovat kokonaisia Text-huomautuksia (Text annotations), jotka viittaavat kommenttiin, johon ne vastaavat, in-reply-to-merkinnän kautta. Sivutason huomautustaulukko (page-level annotation array) ei siis ole tarkastelijan (reviewer) kommenttiluettelo (comment list). Se on litteä säkki, joka sisältää kommentit, niitä yhdistävät putkistot (plumbing) ja useita asioita, joita yksikään tarkastelija ei kutsuisi kommentiksi laisinkaan. Paneeli, joka käsittelee taulukkoa kommenttiluettelona, on eri mieltä jokaisen muun katseluohjelman kanssa, jota asiakas käyttää
Huomautusten tarkastelutyönkulun (annotation review workflow) rakentaminen PDFium Component -komponentin, eli Delphi-, C++Builder- ja Lazarus-ympäristöjen PDFium-pohjaisen VCL/LCL-komponentin, päälle tarkoittaa keskittymistä niihin kohtiin, joissa raa'an taulukon ja inhimillisen näkymän (human view) välinen kuilu (gap) aiheuttaa ongelmia: laskemiseen (counting), indeksointiin (indexing), moottorin (engine) jo jäädyttämien (frozen) merkkien uudelleenvärjäämiseen (recoloring), poistamiseen jättämättä haamuja, ja omien merkkien lisäämiseen
Miksi laskutoimituksesi (count) ei koskaan täsmää Acrobatin kommenttiruudun kanssa
Avaa merkitty (marked-up) sopimus katseluohjelmassasi ja Acrobatissa vierekkäin, ja summat ovat harvoin samaa mieltä. Acrobat näyttää kuratoidun näkymän (curated view): vastauksiksi (reply threads) ryhmitelty merkintä (markup), omiin muistilappuihinsa (notes) taitetut ponnahdusikkunat (popups), ja ulkopuolelle jätetyt linkit (links) ja lomake-elementit (form widgets). Raaka taulukko (raw array) pitää kaiken erottelematta sisällään, joten naiivi laskutoimitus (naive count) antaa liian korkeita tuloksia joiltakin osin, ja liian alhaisia toisilta samanaikaisesti
Ponnahdusikkunat (popups) paisuttavat kokonaissummaa, koska jokainen muistilappu (sticky note) toimitetaan erillisen Popup-objektin kanssa ja molempien laskeminen tuplaa muistilapun. Vastaukset (replies) kutistavat sitä, jos suodatat (filter) näkyviä merkkejä, sillä vastaus on Text-huomautus (Text annotation), jota ei maalata, ennen kuin joku laajentaa keskusteluketjun (thread), ja sen pudottaminen hävittää keskustelun. Hidden- ja NoView-liput (flags) ottavat huomautuksen pois ruudulta ottamatta sitä ulos taulukosta, joten lipuille sokea laskutoimitus sisältää merkkejä, joita käyttäjä ei voi nähdä. Link-huomautukset istuvat samassa taulukossa kommenttien kanssa ja eivät kuulu laskutoimitukseen sen enempää kuin luetteloonkaan. Päätä laskentasääntö (counting rule) ennen kuin kirjoitat silmukan (loop), ja kirjoita päätös ylös, sillä "miksi paneelisi näyttää eri numeron kuin Acrobat" on ensimmäinen tiketti, jonka tarkasteluominaisuus (review feature) ansaitsee
Indeksoi kaikki kerran, äläkä koskaan jäsennä (re-parse) sivua uudelleen
Yksi suunnittelusääntö ohjaa kaikkea seuraavaa: tekijän (author), tyypin tai sivun perusteella suodattaminen (filtering) ei saa koskaan jäsentaa (re-parse) sivuobjekteja uudelleen. 300-sivuisessa asiakirjassa, jossa on runsaasti merkintöjä, uudelleenjäsentäminen jokaisen pudotusvalikon (dropdown) muutoksen yhteydessä muuttaa paneelin sellaiseksi, joka pätkii (stutters) sekunteja kerrallaan. Komponentti paljastaa (exposes) AnnotationCount- ja indeksoidun Annotation[]-ominaisuuden, jotka molemmat on rajattu (scoped) tällä hetkellä ladatulle sivulle, ja niiden palauttama TPdfAnnotation-tietue (record) kantaa sitä, mitä luettelonäkymä (list view) tarvitsee: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Oikea siirto on pyyhkäistä (sweep) jokainen sivu kerran avaushetkellä (at open time) ja pitää oma litteä indeksisi:
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];
// Keep reviewer-relevant subtypes only; record the page and
// index pair because all later edits are addressed by it
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;
Pari, joka on alleviivaamisen arvoinen, on (PageNo, i). Kaikkiin myöhempiin mutaatioihin, olipa kyse uudelleenvärjäyksestä (recolor) tai poistosta (delete), puututaan (addressed) sivunumeron ja huomautusindeksin perusteella, ja indeksi on hauras (fragile): huomautuksen poistaminen numeroi uudelleen kaiken sen jälkeen samalla sivulla. Suunnittele siis rakentavasi kyseisen sivun merkinnät uudelleen (rebuild the affected page's entries) minkä tahansa poiston jälkeen sen sijaan, että paikkaisit (patching) indeksinumerot paikoillaan. Uudelleenrakentaminen maksaa millisekunnin. Vanhentunut indeksi (stale index) taas poistaa väärän tarkastelijan kommentin, mikä on sellainen bugi, joka murentaa (erodes) luottamuksen koko ominaisuuteen
Keskusteluketjut (threading) ansaitsevat paikan indeksissä, vaikka ensimmäinen versiosi vain laskisi (counts) vastaukset (replies) sen sijaan, että se näyttäisi ne. Ryhmittele kohteet niiden yläviittauksen (parent reference) mukaan, kun sivu on auki, jotta paneeli voi myöhemmin taittaa (fold) ketjun samalla tavalla kuin Acrobat. Tuon ryhmittelyn rekonstruoiminen laiskasti (lazily) vierittämisen aikana kumoaa koko kerran indeksoinnin tarkoituksen, koska se avaa uudelleen sivuja, joista jo maksoit jäsennyksen. Geometria haluaa saman kurinalaisuuden. Jokaisen tietueen Rectangle on sivuavaruudessa (page-space), ja sen muuntaminen näkymäkoordinaateiksi (view coordinates) kuuluu yhteen jaettuun apuohjelmaan (shared helper), ei hajotettavaksi koodin sekaan. Paneeleihin kasvaa koordinaattibugeja, kun valinta (selection), osumatestaus (hit-testing) ja maalaaminen keksivät kukin oman zoomaus- ja kiertomatematiikkansa; ohjaa kaikki kolme yhden muunnoksen läpi, niin korostus, sen rivi luettelossa ja sen napsautuskohde (click target) pysyvät kiinnitettynä (pinned) samaan musteeseen
Merkinnän (markup) värjääminen uudelleen (recoloring) ja ulkoasuvirran (appearance-stream) veto-oikeus
Korostuksen muuttaminen keltaisesta meripihkan (amber) väriseksi kuulostaa yhden rivin jutulta (one-liner), ja joskus se on sitä. Kuju (catch) on ISO 32000-1 §12.5.5. Kun huomautus kantaa /AP -ulkoasuvirtaa, standardinmukainen (conforming) katseluohjelma maalaa tuon esirakennetun (pre-built) virran ja käsittelee sanakirjan (dictionary) värimerkintää kuolleena metadatana. Acrobat kirjoittaa ulkoasuvirrat (appearance streams) käytännössä kaikelle, mitä se luo, joten useimmat asiakkailta saapuvat huomautukset ovat jo tässä tilassa, eikä niin itsevarmasti asettamasi väri koskaan saavuta näyttöä (screen). Uudelleenvärjäys on lue-muokkaa-kirjoita -työtä (read-modify-write) Annotation[]-ominaisuuden kautta, ja komponentti on rehellinen konfliktista: kun moottori kieltäytyy antamasta sanakirjan (dictionary) värin ohittaa (override) sisäänrakennettua (baked-in) ulkoasua, kirjoitus (write) nostaa EPdfError-poikkeuksen
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
// The annotation owns a pre-rendered /AP stream; the dictionary
// color alone cannot change what viewers paint
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Ota tuo poikkeus (exception) kiinni (catch) joka kerta, ja käsittele sitä tietona (information) epäonnistumisen (failure) sijaan. Ohita varotoimi (guard), ja paneelisi näyttää iloisesti meripihkaa omassa luettelossaan samalla kun sivu jatkaa keltaisen maalaamista; käyttäjä ilmoittaa siitä viikkoja myöhemmin sanoin "katseluohjelmasi sivuuttaa muokkaukseni," ja sinä vietät iltapäivän epäonnistuen toistamaan sitä tiedostossa, jossa sattuu olemaan ilman ulkoasuvirtaa. Kun tiedät ulkoasun (appearance) olevan lukittu, sinulla on kaksi rehellistä vastinetta (responses): värjää uudelleen oma valintakerroksesi (selection overlay) huomautuksen sijaan, jotta tarkastelija näkee edes valitsemansa korostuksen (highlight), tai merkitse rivi ulkoasultaan lukituksi (appearance-locked), jotta kukaan ei odota muutoksen jäävän pysyväksi (stick)
Huomautusten poistaminen haamuja jättämättä
DeleteAnnotation poistaa objektin nykyisen sivun huomautuspuusta (annotation tree), mutta se jättää välimuistissa olevan sivun rasterin (cached page raster) rauhaan. Maalaa välittömästi kutsun jälkeen, ja poistettu korostus on edelleen näytöllä, istuen bittikartassa (bitmap), joka ei enää vastaa sen takana olevaa asiakirjamallia. Korjaus on käsitellä uudelleenrenderöinti (re-render) osana poistoa, ei askeleena, jonka kutsuja saattaisi unohtaa:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo); // indices after Item.Index shifted
Kaksi yksityiskohtaa tuossa lohkossa on helppo saada väärin. reAnnotations -valinnan (option) on oltava läsnä, tai uusi rasteri pudottaa jokaisen jäljellä olevan huomautuksen ja sivu näyttää siltä kuin olisit pyyhkinyt (wiped) koko kommenttijoukon yhden merkin sijaan. Ja Bmp.Free ei ole valinnainen: funktiomuotoinen RenderPage-ylilataus (overload) antaa bittikartan omistajuuden (bitmap ownership) kutsujalle (caller), joten puuttuva vapautus (missing free) vuotaa (leaks) kokonaisen sivun rasterin (full-page raster) jokaisella poistolla, minkä pitkää asiakirjaa käsittelevä tarkastelija muuttaa todelliseksi muistipaineeksi minuuteissa
Tarkastelijan (reviewer) merkkien lisääminen omasta käyttöliittymästäsi (UI)
Huomautusten luominen (creating) tapahtuu CreateAnnotation -kutsun kautta, joka ottaa täytetyn TPdfAnnotation -tietueen (subtype, rectangle, color, contents, author) ja liittää sen nykyiseen sivuun. Muistilappu (sticky note), alatyyppi (subtype) anText, on helppo tapaus: aseta sijainti (position), sisältö (contents) ja tekijä (author), ja olet valmis. Mustehuomautuksissa (Ink annotations) ihmiset jäävät kiinni. Tietueen suorakulmio rajaa (bounds) vain piirrosta; itse vedot (strokes) ovat taulukoita (arrays) pisteistä, jotka on liitettävä erikseen moottorin (engine) mustevedon kutsun, FPDFAnnot_AddInkStroke, kautta syötettynä FS_POINTF -tiedoilla, kaapattuna (captured) hiiren tai kynän syötteestä yksi veto kerrallaan. Rakenna mustehuomautus pelkästä suorakulmiosta eikä mistään muusta, ja saat tyhjän töherryksen (scribble), joka renderöityy tyhjänä tilana (blank space), joka näyttää moottorissa (engine) olevalta bugilta, ja on todellisuudessa puolivalmis huomautus (half-finished annotation)
Sopikaa tekijäkäytäntö (authorship policy) samalla hengenvedolla. Jokaisen käyttöliittymäsi (UI) luoman merkin pitäisi kantaa yhdenmukaista AuthorText-tekstiä, sillä tarkastelijaselain (reviewer filter), jonka rakennat ensi kuussa, on vain niin hyvä kuin nimet, jotka leimaat (stamp) kommentteihin tänään. Tyhjiä tai epäyhtenäisiä (inconsistent) tekijämerkkijonoja (author strings) ei voi korjata takautuvasti avaamatta jokaista tiedostoa uudelleen
Tarkastelun (review) saaminen ulos katseluohjelmasta (viewer)
Tarkasteludata (review data) ansaitsee ylläpitonsa, kun se voi poistua katseluohjelmasta, yhteenvetona (summary), jonka projektin johtaja lukee avaamatta tiedostoa, tai CSV-tiedostona, joka ruokkii (feeds) seurantataulukkoa (tracking sheet). Vie (export) jo rakentamastasi indeksistä, ei koskaan tuoreesta jäsennyksestä (parse), ja valitse vakaa tapa viitata takaisin kuhunkin merkkiin. Sivunumero (page number) paritettuna huomautuksen suorakulmion kanssa selviytyy edestakaisista matkoista (round-trips), joista taulukkoindeksi (array index) ei, koska seuraava poisto hiljaisesti numeroi indeksit uudelleen, ja CSV:si alkaa osoittaa vääriin kommentteihin
Säilyttämisen arvoinen rivi kantaa sivun, alatyypin (subtype), tekijän, luontiaikaleiman (creation timestamp), kun tiedosto sellaisen rekisteröi (records), sisältötekstin (contents text) ja tila-sarakkeen (status column), jonka omistat itse, etkä sellaista, jonka PDF tarjoaa. Samaa indeksointiajoa (indexing pass) on hyödyllistä käyttää aikaisemmin, sisäänoton (intake) aikana, kun asiakirja saapuu tiimin ulkopuolelta ja haluat tietää, mitä siinä on ennen kuin kukaan tarkastelee sitä. Artikkeli pdfium-component-pdf-intake-review-workbench.html, PDF-sisäänoton työpöytä (PDF intake workbench), käy läpi kyseisen lajittelun (triage), ja pdfium-component-form-field-navigation.html, lomakekenttien navigointi, kattaa (covers) peilikuvatapauksen (mirror-image problem): tietojen keräämistä varten rakennettujen asiakirjojen tarkastelemisen kommenttien sijaan
Yksi tapaus, jota taulukko ei näytä sinulle
Yksi vikatila (failure mode) ansaitsee lipun (flag), koska se näyttää bugilta koodissasi, vaikkei ole sitä. Asiakas ilmoittaa näkyvistä korostuksista (highlights) ympäri sivua, mutta paneelisi ei luettele mitään, ja AnnotationCount tulee takaisin nollana. Yleinen selitys (usual explanation) on, että merkit litistettiin (flattened) jossain ylävirrassa (upstream). Litistäminen (flattening) leipoo (bakes) huomautusten ulkoasut (annotation appearances) osaksi tavallista sivun sisältöä, joten korostuksista tulee osa sivun grafiikkaa (graphics), ja ne lakkaavat olemasta (stop existing) huomautusobjekteina (annotation objects) kokonaan. Jäljellä ei ole mitään, mitä huomautus-API voisi luetella (enumerate), värjätä uudelleen tai poistaa. Kun näet maalattuja merkintöjä (painted markup) nolla-laskennalla (zero count), lopeta bugin etsiminen luetelmasilmukastasi (enumeration loop) ja kysy, miten tiedosto tuotettiin
Tässä käytetty huomautuspinta (annotation surface), aina luettelemisesta (enumeration) ja luomisesta uudelleenvärjäämisen (recoloring), poistamisen ja renderöintiasetusten (render options) kautta, jotka pitävät näytön rehellisenä, toimitetaan PDFium Component -komponentin mukana Delphiä, C++Builderia ja Lazarusta/FPC:tä varten