Sostituire la pagina 3 di un contratto già firmato non dovrebbe spostare l'indice. Elimina la vecchia pagina, inserisci quella nuova, e ogni segnalibro che prima puntava lì ora atterra da un'altra parte. La libreria PDFlibPas Delphi PDF evita questo mantenendo lo stesso oggetto pagina di destinazione e trasferendo solo le voci che portano il contenuto visivo
Perché i segnalibri si rompono dopo la sostituzione di una pagina PDF?
I segnalibri si rompono perché una destinazione PDF nomina una pagina tramite riferimento indiretto a oggetto, non tramite numero di pagina. ISO 32000-1 §12.3.2.2 definisce una destinazione esplicita come un array il cui primo elemento è un riferimento indiretto all'oggetto pagina. Elimina quell'oggetto e accoda un sostituto, e il riferimento resta pendente: la maggior parte dei visualizzatori risponde facendo atterrare il lettore sulla pagina 1, che è esattamente il sintomo segnalato dopo una sostituzione con elimina-poi-inserisci. L'albero delle pagine sembra perfetto, il conteggio delle pagine è corretto, il rendering è corretto, e l'intero livello di navigazione è silenziosamente sbagliato
Nemmeno le destinazioni con nome ti salvano. §12.3.2.3 instrada un nome attraverso il name tree /Dests nel catalogo del documento, ma la foglia a cui quel nome si risolve è comunque un array di destinazione esplicita che contiene lo stesso riferimento di pagina. Il nome aggiunge un livello di indirezione sopra il riferimento di pagina, non attorno ad esso. Lo stesso ragionamento copre il resto del livello interattivo descritto in §12.5: un'annotazione link porta un /Dest o un'azione GoTo /A il cui /D è quell'array, ogni annotazione può portare una voce /P che è un riferimento indiretto alla sua pagina, e un widget di campo form è un'annotazione esattamente sullo stesso piano. Uno scambio di pagina ingenuo scollega quattro sottosistemi in una volta, e se vuoi vederli elencati su un file reale, è lo stesso grafo di oggetti che l'introspezione di outline e annotazioni percorre
Quali voci di pagina portano identità e quali portano aspetto
Un dizionario di pagina mescola due tipi di voci, e una sostituzione sul posto riesce esattamente quando le separi. Il lato aspetto è finito ed enumerabile: /Contents, /Resources, i cinque box di pagina /MediaBox, /CropBox, /BleedBox, /TrimBox e /ArtBox, più /Rotate, /Group, /UserUnit e /BoxColorInfo. Quelle undici voci decidono tutto ciò che un rasterizzatore produce per la pagina, e nient'altro nel file punta a esse per nome
Il lato identità è ciò a cui il resto del documento si è legato: il numero di oggetto e la generazione della pagina, il collegamento all'indietro /Parent verso l'albero delle pagine, e /Annots. PDFlibPas mantiene intatta ognuna di quelle voci. ReplacePageRanges elimina le undici voci visive dal dizionario della pagina di destinazione e le riaggiunge dalla pagina sorgente importata, così l'oggetto pagina di destinazione viene modificato sul posto anziché sostituito. Anche la struttura dell'albero delle pagine richiesta da §7.7.3 resta identica come forma: l'ordine di /Kids, /Count, e ogni /Parent sopravvissuto sono gli stessi prima e dopo, perché nessun nodo è mai stato scollegato
Come sostituisce PDFlibPas una pagina senza rinumerare gli oggetti?
La chiamata prende un documento sorgente, una pagina iniziale di destinazione basata su 1, un'espressione di intervallo sorgente, e un flag di opzioni. Entrambi i documenti devono essere aperti nella stessa istanza, e il documento di destinazione è quello selezionato. Poiché il conteggio delle pagine di destinazione non cambia mai, l'intervallo richiesto deve rientrare nel documento a partire da TargetStartPage, e questo viene verificato prima che venga creato qualunque cosa
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Internamente le pagine sorgente non possono semplicemente essere lette attraverso i confini dei documenti, perché ogni riferimento indiretto al loro interno appartiene alla numerazione degli oggetti sorgente. Quindi l'intervallo sorgente viene prima importato nel modo ordinario, come pagine temporanee accodate dopo l'ultima pagina reale, il che esegue il rimappaggio completo del grafo degli oggetti: content stream, font, XObject, shading e spazi colore vengono tutti rinumerati nel documento di destinazione. Solo allora le undici voci visive vengono copiate da ogni pagina temporanea sulla sua pagina di destinazione, e solo allora le pagine temporanee vengono scollegate dall'albero delle pagine. Il lavoro di rimappaggio avviene dove è economico e sicuro, e la modifica distruttiva si riduce a uno scambio a livello di dizionario su pagine che già esistono
Il percorso di eliminazione che distruggerebbe ciò che hai appena trasferito
Rimuovere quelle pagine temporanee è il passo che sembra banale e non lo è. Il percorso ordinario di cancellazione pagine nella libreria fa più che scollegare un nodo: combina i layer di ogni pagina in eliminazione, svuota il primo content stream, e recupera le risorse che nessun'altra pagina condivide. Questo è un comportamento corretto per una cancellazione reale, e catastrofico qui, perché nel momento in cui le pagine temporanee vengono rimosse le pagine di destinazione già referenziano esattamente quei content stream e quegli oggetti risorsa. Svuotarli renderebbe vuota la pagina appena sostituita, e lo sweep delle risorse raccoglierebbe font e immagini che ora hanno un proprietario attivo
La soluzione è una modalità di preservazione degli oggetti referenziati sul percorso di eliminazione interno. Quando è impostata, la cancellazione salta sia lo sweep delle risorse non condivise sia la pulizia del content stream, e non fa altro che scollegare le pagine dall'albero delle pagine e sistemare la contabilità dell'albero. Gli oggetti trasferiti sopravvivono con un nuovo proprietario, e la proprietà degli oggetti dopo l'operazione è esattamente ciò che disegneresti su una lavagna: un content stream, una pagina proprietaria, un numero di oggetto che non si è mai spostato. Le regole di ciclo di vita correlate per creare, eliminare e riordinare pagine sono trattate separatamente nelle note su operazioni sul ciclo di vita di documento e pagina
Ordinamento, duplicati e fallimento tutto-o-niente
Il flag delle opzioni seleziona come viene interpretato l'intervallo sorgente. 0 ordina i numeri di pagina analizzati e rimuove i duplicati, il che è il default sensato quando il chiamante passa qualcosa come '4-6,2' e significa semplicemente quelle quattro pagine. 1 preserva l'ordine così come scritto e permette a una pagina di ripetersi, quindi '2,1,2' significa realmente tre sostituzioni prese da due pagine sorgente. La validazione viene eseguita per prima e completamente: la sintassi dell'intervallo, ogni numero di pagina rispetto al conteggio delle pagine sorgente, il valore stesso dell'opzione, e la capacità di destinazione vengono tutti verificati prima che venga creato un solo oggetto. Una chiamata respinta imposta LastErrorCode a 412, ripristina la pagina precedentemente selezionata, e lascia il documento esattamente com'era
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
L'atomicità si estende oltre la validazione, fino al trasferimento stesso. Prima che venga importata la prima pagina sorgente, le undici voci visive di ogni pagina di destinazione nell'intervallo vengono fotografate come valori codificati. Se l'importazione fallisce, o il conteggio delle pagine importate non corrisponde a quanto richiesto, le istantanee vengono decodificate di nuovo sulle pagine di destinazione e le pagine temporanee vengono rimosse, così un fallimento a metà percorso lascia comunque gli aspetti visivi originali al loro posto sui rispettivi oggetti originali. Questo conta più di quanto sembri: un intervallo di pagine sostituito a metà in un contratto è peggio di una chiamata fallita, perché nulla nel file lo segnala come fatto a metà
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Cosa la sostituzione sul posto ancora non fa per te?
Le annotazioni sorgente, i campi form sorgente e gli outline sorgente non vengono deliberatamente importati. Portare un widget attraverso senza la sua voce di campo /AcroForm, o un'annotazione portatrice di contenuto marcato senza la sua appartenenza all'albero di struttura, produce un oggetto interattivo importato a metà che nessun visualizzatore può interpretare, quindi l'operazione trasferisce solo l'aspetto. La conseguenza pratica è che se la pagina sostitutiva deve portare nuovi campi form o nuovi link, li aggiungi in seguito alla pagina di destinazione, contro l'oggetto pagina di destinazione che è ancora lì ad aspettarli
Vale la pena verificare altri due limiti sui tuoi file. Primo, /Annots viene preservato ma la geometria della pagina no, quindi sostituire una pagina di 220 mm con una di 320 mm mantiene i rettangoli delle annotazioni alle loro vecchie coordinate dentro un /MediaBox di dimensione diversa; se la geometria cambia, riposiziona le annotazioni che hai mantenuto. Secondo, le voci al di fuori delle undici chiavi visive restano con la pagina di destinazione per progetto, il che è corretto per /Trans o /AA e obsoleto per /Thumb, quindi rigenera le miniature dopo una sostituzione. I documenti taggati richiedono un pensiero in più: gli elementi di struttura continuano a puntare all'oggetto pagina corretto tramite /Pg, ma i loro identificatori di contenuto marcato descrivono contenuto che non è più lì, quindi uno scambio di pagina dentro un workflow PDF/UA è una modifica dell'albero di struttura oltre che una modifica del contenuto. Se il tuo compito è in realtà compositing anziché scambio, sovrapporre grafica a pagine che mantieni, l'approccio di cucitura pagine e template è lo strumento più economico
Tutto ciò che è descritto qui, inclusa la sintassi dell'espressione di intervallo, i valori delle opzioni e l'API di manipolazione pagine circostante, viene fornito nella PDFlibPas Delphi PDF Library standard per Delphi e C++Builder, la cui documentazione di riferimento riporta la voce completa per la chiamata di sostituzione pagina e i suoi codici di errore