Articolo tecnico

Redazione PDF e N-up Stitch in Delphi con HotPDF

Arriva una richiesta sulla tua scrivania: prendi un lotto di estratti conto già renderizzati, oscura i numeri di conto e distribuisci due pagine per foglio per risparmiare carta. Entrambe le metà del lavoro sono chirurgia del content stream su un PDF che non hai creato, quindi non c'è un canvas di pagina amichevole su cui disegnare e nessun gestore di font su cui appoggiarti. Stai modificando direttamente il grafo degli oggetti di un documento caricato, aggiungendo operatori di disegno grezzi a una pagina impaginata da un altro strumento. HotPDF espone esattamente due punti di ingresso per questo, e il più pericoloso dei due è quello che sembra innocuo

HotPDF è un componente PDF VCL nativo per Delphi e C++Builder. La sua API per i documenti caricati, nella round-nine, ha aggiunto i primi metodi che creano contenuti nuovi su una pagina aperta da disco invece che su una costruita da zero. Due di questi sono al centro qui: RedactLoadedRect, che dipinge un rettangolo opaco sopra un'area, e StitchLoadedPage, che scala una pagina e la disegna su un'altra. Entrambi funzionano scrivendo operatori di content stream ISO 32000-1 §8.5 nello stream /Contents della pagina. Capire cosa fanno quegli operatori, e soprattutto cosa non fanno, è la differenza tra uno strumento funzionante e una fuga di dati

Aggiungere operatori a una pagina caricata

Quando costruisci una pagina con la normale API di HotPDF, il componente possiede il content stream e serializza per te le chiamate a TextOut e le chiamate vettoriali per te. Una pagina caricata è diversa: il suo /Contents è uno stream object esistente, forse condiviso, forse parte di un array di contenuti, e devi inserirti senza corrompere ciò che è già lì. La round-nine ha introdotto tre piccoli helper che rendono sicura questa operazione. NewIndirectStream alloca un nuovo THPDFStreamObject con un buffer vuoto e una /Length 0 voce; ResolveLoadedStream segue un riferimento indiretto fino allo stream sottostante; e AppendLoadedStream scrive byte grezzi alla fine dello stream e riscrive /Length così l'oggetto salvato resta ben formato

Il pattern che seguono entrambi i metodi pubblici è lo stesso. Trova il /Contents, risolvilo in uno stream e, se non c'è uno stream utilizzabile, creane uno e attaccalo. Poi aggiungi gli operatori. Poiché i nuovi byte vanno alla fine dello stream, il modello del pittore garantisce che vengano resi sopra tutto ciò che aveva disegnato il layout originale. Questo ordine è tutto il meccanismo dietro il rettangolo di redazione, ed è anche il motivo per cui quel rettangolo non è ciò che la maggior parte delle persone immagina

RedactLoadedRect: una copertura opaca, non una cancellazione

RedactLoadedRect prende un indice di pagina a base zero, quattro coordinate nello spazio utente e tre componenti di colore nell'intervallo da 0 a 1:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Sotto il cofano il metodo emette tre operatori nello stream di contenuto: un'impostazione del colore di riempimento in DeviceRGB (r g b rg), un percorso rettangolare (x y w h re), e un riempimento (f). La larghezza e l'altezza si ricavano come X2 - X1 e Y2 - Y1, quindi passi due angoli opposti e lasci che il metodo calcoli l'estensione. Passa 0, 0, 0 per il colore e ottieni una barra nera; passa 1, 1, 1 per una bianca che corrisponde a una pagina bianca. Le coordinate sono lo spazio utente della pagina caricata, il che significa che l'origine è l'angolo in basso a sinistra e che le unità sono punti, e significa anche che ti serve il /MediaBox della pagina per posizionare qualcosa con precisione; GetLoadedPageBox con pbMediaBox te lo dà

Leggilo due volte: un rettangolo riempito copre il contenuto solo visivamente, non lo rimuove. Il testo, l'immagine o la grafica vettoriale sotto il rettangolo restano presenti nel PDF, ancora nel grafo degli oggetti, ancora estraibili da chiunque copi la pagina, esegua un estrattore di testo o semplicemente elimini il tuo rettangolo dallo stream di contenuto. Questo è mascheramento visivo, non redazione nel senso legale o di sicurezza. Se stai nascondendo dati davvero sensibili, numeri di conto, cartelle cliniche, identità, qualunque cosa regolamentata, coprirli con un riquadro nero e spedire il file è una perdita di dati in attesa di essere scoperta. La vera redazione richiede l'eliminazione degli oggetti di contenuto sottostanti, non di dipingervi sopra

The method name says "Redact", and that is a useful warning about how the result will be misread, not a promise about what it deletes. The implementation is honest about this in its own comment: it calls itself the "visual redaction primitive" and notes that content-removing redaction needs a content-stream interpreter that walks and rewrites the existing operators. HotPDF's loaded-document path does not do that here. So the safe rule is narrow: use RedactLoadedRect for non-sensitive cosmetic masking — hiding a draft watermark, blanking a region before a screenshot, covering an obsolete logo on an internal proof. The moment the thing under the box would matter if it leaked, this method is the wrong tool, and the right answer is to regenerate the document without the data or to use a real content-removal pipeline

StitchLoadedPage: scala, trasla, disegna

N-up imposition is the friendlier problem because nothing is hidden, only rearranged. StitchLoadedPage takes a target page index, a source page index, an X/Y offset, and a scale factor, and it draws the source page onto the target at that position and size:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

The operator string it appends is a standard transform-and-paint sequence: q to save graphics state, a cm matrix carrying the scale on the diagonal and the offset in the translation slots, /StitchSrc Do to invoke an external object, and Q to restore state. The q/Q pair matters: it isolates the transform so the stitched page does not bleed its coordinate system into anything appended afterward. The method also guards the obvious mistakes — indices out of range, a target equal to the source, a non-positive scale (which it clamps to 1.0) — and exits quietly rather than raising, so check your inputs because a silent no-op looks identical to success

StitchLoadedPageSideBySide is a thin convenience over the general method. It reads the target's media-box width, halves it, and calls StitchLoadedPage with that half-width as the X offset and a fixed scale of 0.5, putting the source on the right half. That hard-coded 0.5 assumes the source and target share a width; if they do not, the source will not fill its half cleanly, and you will want the general StitchLoadedPage with a scale you compute yourself from both media boxes

La strategia semplificata degli XObject e il suo compromesso ISO

Here is where the implementation makes a deliberate shortcut you need to know about before you trust the output across viewers. A correct N-up imposition wraps the source page's content in a Form XObject — a self-contained drawable object that ISO 32000-1 §8.10.1 says must carry /Type /XObject, /Subtype /Form, and its own /BBox clipping box. HotPDF's round-nine stitch does not build that wrapper. Instead it registers the source page dictionary itself directly under the target's /Resources /XObject with the name StitchSrc, then draws it with Do. A page dict and a Form XObject share enough of their content model — both reference a content stream and a resource dictionary — that many readers will render the result

But it is not a conforming Form XObject. It lacks the /Subtype /Form marker and its own /BBox, which means a strict consumer is within its rights to ignore the Do or to clip it differently than you expect. The TechnicalNotes for this round say so plainly: the approach "renders under most readers" but is "not a strictly ISO-compliant Form XObject", and full compliance requires synthesizing a real Form XObject stream as a separate step. So treat the stitch output the way you would treat any non-conforming construct: verify it in the specific viewers your customers run, not just the one on your machine, and if you need archival or strict-validator-clean PDFs, do not rely on this path. The same discipline applies to anything you build on the loaded object graph, which is why a Controllo di preflight PDF in Delphi earns its place in the release pipeline whenever you mutate documents programmatically

Dove si collocano questi strumenti, e dove no

Both methods are content-stream tools, so the mental model is the same one you use for direct drawing. If you have built pages from scratch with the component, the vector and colour operators behind these calls will look familiar from Disegno su canvas HotPDF in Delphi; the difference is only that here you are appending to a stream someone else authored rather than one you own. Keep three boundaries in mind:

  • La redazione è solo cosmetica RedactLoadedRect paints over content and never deletes it. For anything sensitive, regenerate the source or use real content removal — a black box is not security
  • Lo stitch non è conforme per scelta La pagina sorgente viene referenziata come un pseudo-XObject senza il /Subtype /Form e /BBox, quindi conferma il rendering nei viewer di destinazione ed evitala dove serve una validazione rigorosa
  • Le coordinate sono lo spazio utente della pagina. Origine in basso a sinistra, punti, determinate dal media box della pagina stessa. Leggi il box con GetLoadedPageBox prima di posizionare qualsiasi cosa, perché la pagina che hai caricato potrebbe non avere la dimensione che pensavi

Usata entro questi limiti, la coppia copre un flusso di lavoro reale: riordina le pagine per la stampa, maschera le regioni non riservate e riscrive il risultato con SaveLoadedDocument, il tutto senza un re-render completo. L'API per documenti caricati che include queste primitive di stitch e mascheramento viene fornita con il HotPDF Component per Delphi e C++Builder, insieme ai metodi per campi modulo, annotazioni e FDF della stessa tornata