Technical Article

Annullerbar progressiv PDF-gengivelse i Delphi (PDFium)

De fleste PDF-sider rasteriseres på få millisekunder, og du tænker aldrig over det. Så åbner en bruger en A1-ingeniørtegning, en side pakket med titusindvis af vektorstrøg, eller en plakat overfyldt med gennemsigtighedsgrupper og bløde masker, og det enkelte kald, der maler den, tager to eller tre sekunder. Hvis det kald kører på UI-tråden, stopper vinduet med at tegne igen, titellinjen bliver grå, og operativsystemet tilbyder at dræbe applikationen. Arbejdet er legitimt. Siden har virkelig brug for så lang tid. Fejlen er, at gengivelsen er ét udeleligt blokerende kald uden nogen måde at trække vejret på og uden nogen måde at stoppe

Denne artikel handler om præcis ét af disse to problemer: at annullere en lang enkeltsidet gengivelse uden at fryse UI'en. Brugeren klikkede på næste side, eller zoomede, eller lukkede dokumentet, og gengivelsen, der er undervejs, er nu spildt arbejde, som bør afsluttes ved næste lejlighed frem for at køre til ende. At udjævne rul og zoom ved at cache det, der allerede blev rasteriseret, er en separat bekymring med sit eget design, dækket i ledsageartiklen linket i slutningen. Her er det eneste spørgsmål, hvordan man får én progressiv gengivelse til at besvare en annulleringsanmodning hurtigt og rent

Den progressive gengivelses-API, PDFium allerede leverer

PDFium forudså den frysende halvdel af problemet. Ved siden af den et-skuds FPDF_RenderPageBitmap eksponerer den en progressiv variant, der opdeler en side i klumper af arbejde. Du kalder FPDF_RenderPageBitmap_Start én gang for at opsætte gengivelsen mod et destinationsbitmap, og kalder derefter FPDF_RenderPage_Continue gentagne gange. Hver Continue rasteriserer en afgrænset skive og returnerer en status. FPDF_RENDER_TOBECONTINUED betyder, at der er mere at gøre, FPDF_RENDER_DONE betyder, at siden er færdig, og FPDF_RENDER_FAILED betyder, at den stoppede på en fejl. Når sløjfen slutter, kalder du FPDF_RenderPage_Close for at frigive den per-side progressive tilstand. Fordi kontrollen vender tilbage til din kode mellem skiver, kan du pumpe meddelelser, opdatere en fremdriftsindikator eller tjekke, om arbejdet stadig er ønsket

Den mekanisme, PDFium leverer til at beslutte, hvornår der skal overgives tid, er en callback-struct ved navn IFSDK_PAUSE. Du overdrager den til Start og til hvert Continue. Efter hver klump kalder PDFium dens NeedToPauseNow-funktionspointer, og hvis den returnerer en værdi forskellig fra nul, stopper det aktuelle Continue tidligt og overgiver kontrollen tilbage med FPDF_RENDER_TOBECONTINUED. Struct'en bærer også et version-felt, som skal sættes til 1, og en fri-formet user-pointer, som PDFium aldrig rører og sender videre urørt. Den urørte pointer er hele hængslet i det design, der følger

Omformål af pause som annuller

Den oprindelige hensigt med NeedToPauseNow er tidsopdeling. Returner forskellig fra nul, når dit frame-budget er brugt, returner nul for at fortsætte med at gengive, og PDFium pauser, så du kan gøre noget andet, før du genoptager den samme gengivelse. PDFium-komponenten genbruger det samme signal til et andet udsagnsord. I stedet for at besvare "skal jeg pause og lade dig genoptage," besvarer callback'et "er dette arbejde blevet annulleret." De to mapper rent over på hinanden på grund af, hvad sløjfen gør, når den ser flaget. En ægte pause forventer et senere Continue; en annullering gør ikke. Når den kaldende sløjfe observerer, at tokenet er annulleret, lukker den gengivelseskonteksten og kalder aldrig Continue igen, så den samme forskellig-fra-nul returværdi, som PDFium læser som "stop denne klump," bliver i virkeligheden til "stop for altid."

Annullering udtrykkes gennem en grænseflade, IPdfCancellationToken, hvis IsCancelled-egenskab skifter fra falsk til sand, når en anden del af programmet beder om, at gengivelsen skal stoppe. Broen mellem den Pascal-grænseflade og PDFiums C-callback er en enkelt pointer. Tokenets grænsefladereference skrives ind i IFSDK_PAUSE.user, og et statisk cdecl-callback læser den ud igen og forespørger den. Dette er det klassiske problem med at lade et C-bibliotek kalde tilbage ind i Pascal: callback'et er nødt til at være en almindelig funktion med C-kaldskonvention, ikke en metode, fordi PDFium gemmer og påkalder en bar funktionspointer, der intet ved om Pascal-objekter eller Self

Callback'et gendanner tokenet ved at kaste pThis^.user tilbage til grænsefladetypen og læser IsCancelled. Intet i det allokerer, låser eller blokerer, hvilket betyder noget, fordi PDFium kalder det på gengivelsestråden efter hver klump, og ethvert arbejde udført her lægges til omkostningerne for selve gengivelsen. Sikringen mod en nil-struct eller et nil user-felt betyder, at den samme funktion er sikker at installere, selv på en gengivelse, der aldrig fik et rigtigt token

At holde tokenet i live hen over sløjfen

At kaste en grænsefladepointer gennem en rå Pointer og tilbage er der, hvor levetidsfejl fødes. En IInterface i Delphi er referenceoptalt, og tælleren bevæger sig kun, når compileren kan se en grænsefladetypet variabel blive tildelt. At gemme tokenet udelukkende som en bar pointer inde i IFSDK_PAUSE.user ville skjule det fuldstændigt for reference-tælleren. Hvis den eneste anden reference til det token gik ud af scope, mens Continue-sløjfen stadig kørte, ville objektet blive frigivet under callback'et, og den næste klump ville dereferere en dinglende pointer

Det er grunden til, at deskriptoren er en record, der indeholder to ting, ikke én. Pause-feltet er den struct, PDFium læser. Token-feltet er en ægte grænsefladetypet reference, som compileren tæller, og den eksisterer af ingen anden grund end at fastholde tokenet i hukommelsen, så længe recorden lever. Recorden er en lokal variabel på stakken af gengivelsesrutinen, så den forbliver gyldig i hele varigheden af sløjfen og nedrives først, når rutinen afslutter. Den bare pointer i user og den talte reference i Token navngiver det samme objekt; den ene er, hvad PDFium kan læse, den anden er, hvad der forhindrer, at det objekt bliver indsamlet

Lukning af gengivelseskonteksten uanset hvordan sløjfen ender

Hvert kald til FPDF_RenderPageBitmap_Start allokerer progressiv tilstand, som PDFium knytter til siden, og den tilstand frigives kun af FPDF_RenderPage_Close. Der er tre veje ud af driv-sløjfen. Siden fuldføres, og den sidste status er FPDF_RENDER_DONE. Tokenet udløses, og sløjfen afsluttes tidligt med rapportering om annullering. Noget mislykkes, og statussen er FPDF_RENDER_FAILED. Alle tre skal kalde Close, og annulleringsstien er den nemmeste at få forkert, fordi den naturlige form af "se annuller, bryd ud" har tendens til springe oprydning over på vej mod udgangen. At efterlade Close uopnået lækker den per-side tilstand, og en fremviser, der lader brugeren annullere gengivelse efter gengivelse, ville akkumulere den lækage på hver afbrudt side

Den robuste form lægger sløjfen og resultatklassificeringen ind i en try og FPDF_RenderPage_Close i den matchende finally. Destinationsbitmappet ødelægges i den samme blok. Annullering kan forlade sløjfen via en tidlig Exit, og finally'en kører stadig, så der er præcis ét sted, der frigiver den progressive tilstand, og det kan ikke omgås

Sløjfen tjekker tokenet før hvert Continue såvel som at stole på callback'et inde i det. Callback'et forkorter den aktuelle klump; sløjfens tjek forhindrer den næste i at starte. Tilsammen begrænser de, hvor lang tid en annullering tager om at træde i kraft, til groft sagt varigheden af én klump

Tre udfald, og hvad bitmappet rummer efter en annullering

Det offentlige indgangspunkt er TPdf.RenderPageProgressive, og det returnerer en TPdfProgressiveStatus, som er en af prsDone, prsCancelled eller prsFailed. Værdierne spejler PDFiums FPDF_RENDER_*-konstanter i Pascal-idiom, men folder annulleringstilfældet ind som et førsteklasses resultat frem for en fejl

Det punkt, der fanger folk, er, hvad destinationsbitmappet indeholder efter prsCancelled. Det er ikke tomt. PDFium gengiver progressivt ind i det samme bitmap klump efter klump, så når en annullering stopper sløjfen, rummer bitmappet, hvad der end blev malet op til det øjeblik, hvilket er et partielt billede: nogle bånd er færdige, resten viser stadig fyldfarven. Om det partielle resultat er nyttigt, afhænger af kalderen. En fremviser, der er ved at smide bitmappet væk, fordi brugeren navigerede et andet sted hen, kan simpelthen ignorere det. En fremviser, der ønsker at vise et lav-omkostnings preview, kan beholde det. Hvad du ikke må gøre, er at antage, at prsCancelled indebærer et tomt eller udefineret bitmap; det indebærer et sandfærdigt øjebliksbillede af en uafsluttet gengivelse

Nil-tokenet og et forgreningfrit callback-spor

Annullering er opt-in. En kalder, der bare vil have progressiv gengivelse for meddelelses-pumpe-fordelen, uden nogen intention om at afbryde, bør kunne overdrage nil for tokenet. Den naive måde at understøtte det på er at strø "hvis et token blev leveret"-tjek ud over callback'et og sløjfen, hvilket betyder en forgrening på hver klump og et callback, der skal håndtere både et ægte token og dets fravær

Implementeringen undgår det ved at substituere en singleton, når kalderen intet overdrager. Et nil-token byttes ud med PdfNoCancellationToken, en grænseflade, hvis IsCancelled altid er falsk. Fra det punkt har callback'et og sløjfen et token at forespørge i hvert eneste tilfælde, så ingen af dem har brug for et nil-tjek, og ingen af dem har brug for et særligt spor. Det altid-falske token svarer simpelthen altid falsk, callback'et returnerer altid nul, og gengivelsen kører til ende nøjagtigt som en ikke-annullerbar én ville. Valgfri adfærd er modelleret som et token, der aldrig udløses, frem for som fraværet af et token, hvilket holder det varme spor ensartet

Formen, der træder frem, er lille og værd at gentage, fordi det er den genbrugelige del. Et C-bibliotek, der understøtter et callback, giver dig præcis én kanal til at videregive tilstand ind i det callback, den opakke user-pointer. Læg en talt Pascal-grænsefladereference bag den pointer, hold en anden ægte reference i live ved siden af struct'en, så objektet ikke kan indsamles midt i et kald, og læs grænsefladen tilbage ud inde i en statisk cdecl-funktion. Indpak hele driv-sløjfen i en try og frigiv den native kontekst i finally. Den samme skabelon overføres til enhver progressiv eller callback-drevet PDFium-operation, hvor Pascal-kode skal forblive i kontrol over levetiden, mens C holder en pointer

Annullering er kun den ene halvdel af en responsiv fremviser. Den anden halvdel er at undgå at gen-gengive sider, du allerede har tegnet, og holde zoom og rul jævn ved at servere covede bitmaps, hvilket er dækket i vores artikel om gengivelsescaching og zoomydeevne. For hvordan den annullerbare gengivelse passer ind i en komplet fremviser ved siden af navigation, selektion og søgning, se opbygning af en funktionsrig PDF-fremviser med PDFium VCL-komponenten. Den progressive gengivelse beskrevet her leveres som del af PDFium Component til Delphi og Lazarus ved siden af de indlæsnings-, gengivelses- og formular-API'er, der er dækket andetsteds på denne blog