Il contesto

Il sito ha già una manciata di tool PDF separati — unisci, dividi, comprimi, compila moduli con campi AcroForm, firma disegnata. Ognuno è una pagina indipendente, utile per intercettare la propria query di ricerca specifica ("unisci pdf gratis", "comprimi pdf online"). Quello che mancava era un editor vero e proprio: uno spazio di lavoro unico dove intervenire su un PDF con più strumenti nella stessa sessione, senza dover passare da un tool all'altro e riscaricare il file a ogni passaggio.

La decisione architetturale è stata di non toccare le pagine esistenti — restano lì, continuano a fare il loro lavoro SEO — e costruire editor-pdf.html come nuovo tool a parte, che le assorbe funzionalmente in un'unica interfaccia più ricca: sidebar di miniature, riordino e rotazione pagine via drag&drop, annotazioni (testo libero, evidenziazione, forme), redazione, firma disegnata, filigrana. Stesso vincolo di sempre: tutto client-side, nessun upload, il file non lascia mai il browser dell'utente.

Architettura: canvas di rendering più overlay coordinato

Il pattern di base non è nuovo sul sito: era già stato validato in compila-pdf.html, che renderizza le pagine con PDF.js su un <canvas> e sovrappone un livello HTML per l'interazione (i campi compilabili, in quel caso). L'editor riusa lo stesso schema, generalizzato: ogni pagina è un canvas di rendering PDF.js più un overlay assoluto delle stesse dimensioni, dove vivono tutte le annotazioni come elementi posizionati in percentuale rispetto al viewport della pagina, non in pixel assoluti.

Questo dettaglio conta perché lo zoom e il resize della finestra cambiano continuamente la scala di rendering: se le annotazioni fossero ancorate in pixel, ogni ridimensionamento le farebbe scivolare fuori posizione rispetto al testo sottostante. Ancorandole in percentuale del viewport corrente di PDF.js, restano coerenti a qualunque zoom, e la conversione in coordinate assolute del PDF avviene solo una volta, al momento dell'export finale con pdf-lib.

Il rendering è protetto da un render token per pagina: ogni volta che scale o contenuto cambiano, si genera un nuovo token e il rendering in corso, se non è più quello più recente, si auto-annulla al termine invece di disegnare un frame ormai obsoleto sopra quello nuovo. Senza questa guardia, ridimensionare rapidamente la finestra durante un rendering pesante produce sovrapposizioni tra frame vecchi e nuovi — un problema classico di race condition su canvas asincroni.

Il pezzo interessante: la redazione non è un rettangolo nero

Annotazioni, firma e filigrana sono, tutto sommato, "solo" gestione di overlay e composizione finale — impegnativo ma diretto. La redazione è diversa, perché ha un requisito che le altre feature non hanno: deve essere irreversibile. Uno strumento che si chiama "redazione" e lascia il contenuto originale ancora presente nel file, anche solo coperto visivamente, non sta facendo quello che promette — e in un contesto legale o professionale, la differenza tra "sembra coperto" e "è stato rimosso" non è un dettaglio.

Redazione ingenua — cosa NON fare
// ❌ Disegna solo un rettangolo nero sopra il contenuto esistente
const page = pdfDoc.getPage(pageIndex);
page.drawRectangle({
  x, y, width, height,
  color: rgb(0, 0, 0)
});
// Il testo originale è ancora nel content stream della pagina:
// selezionabile, copiabile, estraibile con qualunque parser PDF

Questo è esattamente il comportamento di molti tool "redigi PDF" trovati in giro: disegnano una forma sopra il testo e la chiamano redazione. Aprendo il PDF risultante in un lettore qualsiasi e selezionando "tutto il testo della pagina" (o, più semplicemente, copiando l'area nera stessa), il contenuto "nascosto" riappare negli appunti. Il PDF, a livello di dati, non ha perso nulla — ha solo guadagnato un rettangolo colorato in cima.

🔴

Coprire visivamente un testo in un PDF non lo elimina: il content stream della pagina resta invariato sotto la forma disegnata. Chi si affida a un blackout come questo per proteggere dati sensibili sta distribuendo comunque quei dati, solo un po' più nascosti.

La soluzione: rasterizzare, ma solo le pagine che serve

L'unico modo per rimuovere davvero un contenuto da una pagina, restando completamente client-side senza un motore di riscrittura del content stream PDF, è convertire l'intera pagina in un'immagine dopo aver applicato il blackout — un bitmap non ha "testo sotto" da estrarre, perché non c'è più testo: ci sono solo pixel. Il compromesso ovvio è che rasterizzare gonfia il peso del file e perde la selezionabilità del testo. La soluzione adottata è applicare questo trattamento solo alle pagine che contengono almeno una redazione, lasciando tutte le altre esattamente come sono nell'originale.

export finale — semplificato
// Per ogni pagina del documento originale...
for (let i = 0; i < totalPages; i++) {
  const hasRedaction = redactionsByPage[i]?.length > 0;

  if (!hasRedaction) {
    // ✅ Nessuna redazione: pagina copiata intatta, resta vettoriale
    const [copied] = await outputDoc.copyPages(sourceDoc, [i]);
    outputDoc.addPage(copied);
    continue;
  }

  // 🔥 Pagina con redazioni: rasterizzata ad alta risoluzione
  const bitmap = await renderPageToCanvas(pdfjsDoc, i, { scale: 3 });
  burnRedactionsIntoCanvas(bitmap, redactionsByPage[i]);
  burnAnnotationsIntoCanvas(bitmap, annotationsByPage[i]);

  const jpg = await outputDoc.embedJpg(bitmap.toJPEG(0.92));
  const newPage = outputDoc.addPage([bitmap.width, bitmap.height]);
  newPage.drawImage(jpg, { x: 0, y: 0, width: bitmap.width, height: bitmap.height });
}

Due dettagli fanno la differenza in questo passaggio. Primo: la rasterizzazione avviene a una scala più alta (3x) di quella usata per la visualizzazione a schermo, altrimenti il testo sulle pagine "bruciate" risulterebbe visibilmente sgranato rispetto a quelle vettoriali rimaste nel documento. Secondo: sulle pagine rasterizzate, anche le annotazioni non-redazione (testo, evidenziazioni, forme, firma, filigrana) vengono bruciate nello stesso bitmap invece che aggiunte come overlay vettoriale separato — altrimenti si avrebbe una mescolanza di rendering diversi sulla stessa pagina, con incongruenze quando il PDF viene poi riaperto in lettori diversi.

Il risultato pratico: un documento di 20 pagine con una sola redazione a pagina 12 produce un PDF dove 19 pagine restano leggere, vettoriali e con testo selezionabile, e solo pagina 12 è un'immagine — pesante quel tanto che basta, ma senza nulla di estraibile sotto il blackout.

Riordino, rotazione ed eliminazione pagine

La sidebar di miniature supporta drag&drop per riordinare le pagine, rotazione di 90° per pagina e eliminazione — tutte operazioni che si applicano a un array di stato in memoria (l'ordine, gli angoli di rotazione, le pagine escluse) senza toccare il documento sorgente finché non si arriva all'export. Questo tiene l'interfaccia reattiva anche su PDF di decine di pagine, perché ogni interazione aggiorna solo lo stato e ridisegna la miniatura interessata, non l'intero documento.

La rotazione è l'unico punto dove va prestata attenzione extra sulle pagine che finiscono rasterizzate: il viewport passato a PDF.js in fase di rendering deve già includere la rotazione applicata dall'utente, così il bitmap esce con l'orientamento corretto senza bisogno di ruotare l'immagine dopo — ruotare un bitmap già renderizzato introdurrebbe un ricampionamento aggiuntivo (e quindi ulteriore perdita di nitidezza) del tutto evitabile.

Firma disegnata e filigrana: riuso, non reinvenzione

La firma disegnata (touch e mouse, con smoothing sui punti di tracciamento) era già stata scritta e validata in compila-pdf.html: qui è stata riadattata da "un campo del form" a "un'annotazione riposizionabile" — stesso motore di cattura del tratto, ma inserita nel sistema generico di overlay dell'editor, con lo stesso handle di ridimensionamento condiviso da tutte le altre annotazioni. La filigrana è più semplice: testo con opacità e rotazione configurabili, applicabile a una singola pagina o all'intero documento, disegnata anch'essa come overlay vettoriale e "bruciata" solo se la pagina finisce rasterizzata per via di una redazione nelle vicinanze.

Cosa ho imparato

  1. "Redazione" ha un requisito implicito di irreversibilità che un semplice overlay grafico non soddisfa. Se il nome della feature promette rimozione, va verificato che il dato sparisca davvero, non solo che sparisca alla vista.
  2. Rasterizzare selettivamente, non l'intero documento. Applicare il trattamento pesante solo alle pagine che lo richiedono mantiene leggero e ricercabile tutto il resto — un compromesso netto migliore che trattare l'intero PDF come "meno fidato" per via di una singola pagina sensibile.
  3. Ancorare le annotazioni al viewport, non ai pixel dello schermo. Coordinate percentuali rispetto al viewport corrente di PDF.js restano coerenti a ogni zoom o resize; coordinate pixel assolute si disallineano non appena la scala di rendering cambia.
  4. Un render token per pagina evita race condition su canvas asincroni. Con rendering ripetuti in rapida successione (resize, zoom), senza un modo per invalidare un rendering ormai obsoleto si rischiano frame sovrapposti visivamente scorretti.
  5. La rasterizzazione va fatta a scala più alta di quella di visualizzazione. Renderizzare al 3x invece che alla scala schermo evita che le pagine "bruciate" risultino visibilmente più sgranate di quelle rimaste vettoriali nello stesso documento.
  6. Codice già scritto per un tool si riusa per il successivo. Motore di rendering PDF.js/overlay e firma disegnata, validati in compila-pdf.html, hanno accelerato di molto la costruzione dell'editor invece di essere riscritti da zero.

Domande frequenti

Perché un rettangolo nero sopra un testo in un PDF non è vera redazione?

Perché in un PDF il testo è un livello di dati indipendente dalla grafica: se si disegna una forma nera sopra una riga, quella riga resta nel documento sotto la forma, selezionabile e copiabile, ed è estraibile con un semplice "seleziona tutto" o con qualunque parser PDF, anche se visivamente non si vede più nulla. La vera redazione deve rimuovere il contenuto testuale dal documento, non solo coprirlo.

Come si rimuove davvero il testo sotto un'area redatta senza un server che processa il file?

Rasterizzando la pagina interessata: la si disegna ad alta risoluzione su un canvas tramite PDF.js, si applica il blackout direttamente sul bitmap, e si sostituisce l'intera pagina vettoriale con quell'immagine nel PDF finale. A quel punto il testo originale non esiste più come dato: è stato convertito in pixel insieme al resto della pagina.

Perché rasterizzare tutte le pagine di un PDF, anche quelle senza redazioni, è una cattiva idea?

Perché un'immagine occupa molto più spazio del testo vettoriale che rappresenta, quindi rasterizzare l'intero documento gonfia il peso del file e rende il testo non più selezionabile né ricercabile ovunque, anche dove non serviva nascondere nulla. Meglio rasterizzare solo le pagine con almeno una redazione, copiando le altre vettorialmente.

È possibile copiare pagine vettoriali da un PDF e mescolarle a pagine rasterizzate nello stesso documento?

Sì. Una libreria come pdf-lib permette di copiare pagine originali intatte da un PDF sorgente a uno nuovo (mantenendole vettoriali), e allo stesso tempo di creare pagine da un'immagine, inserendole nella stessa posizione dell'indice pagine — senza differenze visibili nell'ordine o nella numerazione.

Un editor PDF che gira solo nel browser è davvero sicuro per documenti sensibili?

L'assenza di upload elimina il rischio che il file transiti o resti salvato su un server terzo: rendering, editing ed export avvengono in memoria nel browser tramite PDF.js e pdf-lib, e il file lascia il dispositivo solo quando l'utente lo scarica o lo condivide.