Articolo tecnico

Da PDF a Markdown e DOCX in Delphi con PDFlibPas

PDFlibPas converte il contenuto PDF in due formati modificabili senza automazione Office. ExportPageMarkdown ed ExportDocumentMarkdown restituiscono Markdown semantico con titoli inferiti, elenchi ordinati e non ordinati e tabelle pipe, mentre SaveDOCXToFile e SaveDOCXToStream scrivono un pacchetto WordprocessingML contenente paragrafi, titoli, numerazione di elenco nativa, tabelle rilevate, stile dei font, interruzioni di pagina e immagini PNG posizionate

Entrambi funzionano interamente in Pascal, su un server, senza Word installato e senza COM. Questo vincolo è il motivo per cui la funzionalità esiste in una libreria PDF anziché in uno strumento desktop

Perché «da PDF a Word» è davvero difficile?

Perché una pagina PDF non contiene paragrafi. Contiene operatori di visualizzazione del testo che posizionano run di glifi a determinate coordinate, nell'ordine qualunque in cui il producer li ha emessi, senza alcun obbligo di indicare che due run appartengono alla stessa frase, tanto meno allo stesso elemento di elenco. Il formato è stato progettato per descrivere esattamente una pagina stampata, e ci riesce scartando la struttura che ha prodotto la pagina

Quindi ogni convertitore deve ricostruire ciò che il generatore ha scartato. Il raggruppamento delle righe deriva dalla spaziatura verticale e dall'allineamento della baseline. I confini dei paragrafi derivano da cambi di spaziatura e indentazione. Un titolo è una riga il cui font è più grande o più pesante del corpo del testo e che si distingue da ciò che segue. Un elenco è una sequenza di paragrafi che inizia con un carattere puntato o uno schema numerico. Una tabella è una griglia di blocchi di testo i cui bordi si allineano lungo righe e colonne. Ognuna di queste è un'inferenza, e inferenza significa un buon risultato sui documenti che seguono le convenzioni tipografiche ordinarie e uno mediocre sui documenti che non le seguono

I PDF taggati sono l'eccezione, e una grande eccezione. Quando il documento porta un albero di struttura, i ruoli di paragrafo, titolo, elenco e tabella sono registrati anziché indovinati, ed è per questo che il lavoro di accessibilità descritto in struttura di accessibilità dei PDF taggati ripaga anche in qualità di conversione. Se controlli il producer, taggare il tuo output è la singola cosa a più alto impatto che puoi fare per chiunque dovrà poi convertirlo

Esportazione in Markdown, una pagina alla volta

Il percorso Markdown è quello da scegliere quando la destinazione è una pipeline testuale: un sito di documentazione, un indice di ricerca, un corpus di retrieval per un assistente. Le opzioni sono una bit mask: PDF_MARKDOWN_INCLUDE_PAGE_MARKERS, PDF_MARKDOWN_DETECT_HEADINGS, PDF_MARKDOWN_PRESERVE_STYLES, con PDF_MARKDOWN_DEFAULT che le combina tutte e tre

var
  Pdf: TPDFlib;
  Md: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('handbook.pdf', '');

    // Una pagina, come stringa
    Md := Pdf.ExportPageMarkdown(1, PDF_MARKDOWN_DEFAULT);

    // Un intervallo di pagine, in streaming su disco come UTF-8 senza BOM
    Pdf.SaveMarkdownToFile('1-40',
      PDF_MARKDOWN_DETECT_HEADINGS or PDF_MARKDOWN_PRESERVE_STYLES,
      'handbook.md');
  finally
    Pdf.Free;
  end;
end;

I marcatori di pagina si guadagnano il loro posto nel lavoro di retrieval. Un chunk di testo che porta con sé la pagina da cui proviene può essere citato con precisione, e un lettore che segue la citazione atterra dove si trova davvero l'affermazione. Disattivali quando il Markdown è destinato alla lettura umana, dove i confini di pagina del layout sorgente sono rumore

I punti di ingresso in streaming contano per i documenti di grandi dimensioni. SaveMarkdownToStream e SaveMarkdownToFile scrivono UTF-8 una pagina alla volta e non bufferizzano l'output completo, quindi un manuale di 900 pagine non diventa prima una stringa di 900 pagine in memoria. Anche l'assenza di un byte-order mark è deliberata: un BOM su un file Markdown confonde un numero sorprendente di generatori di siti statici e strumenti di diff

DOCX senza Office sulla macchina

Il writer DOCX produce da sé il pacchetto: voci ZIP scritte come raw Deflate con controlli CRC, le parti WordprocessingML, e le relazioni che le legano. Nulla richiama Word, il che significa che la conversione gira su un server headless, dentro un account di servizio, in un container, in tutti i posti in cui l'automazione Office è senza licenza, instabile o vietata

var
  Pdf: TPDFlib;
  Target: TFileStream;
begin
  Pdf := TPDFlib.Create;
  Target := TFileStream.Create('handbook.docx', fmCreate);
  try
    Pdf.LoadFromFile('handbook.pdf', '');
    Pdf.SaveDOCXToStream('1-40',
      PDF_DOCX_INCLUDE_IMAGES or PDF_DOCX_DETECT_HEADINGS or
      PDF_DOCX_PRESERVE_STYLES or PDF_DOCX_PRESERVE_PAGE_BREAKS,
      Target);
  finally
    Target.Free;
    Pdf.Free;
  end;
end;

I dati immagine vengono scritti man mano che ogni pagina viene elaborata invece di essere raccolti e aggiunti alla fine, così il picco di memoria segue una singola pagina anziché l'intero documento. L'ordine esplicito delle pagine viene preservato, e la pagina PDF selezionata viene ripristinata al termine, il che conta quando l'esportazione è un passo dentro un job più lungo che aveva una pagina selezionata per altri motivi

Cosa ti offre un packaging deterministico?

Riproducibilità byte per byte. Due conversioni dello stesso input con le stesse opzioni producono lo stesso pacchetto, il che significa che puoi calcolare l'hash dell'output per rilevare cambiamenti, confrontare due build di un documento generato, e mettere in cache in modo aggressivo senza preoccuparti che un input identico abbia prodotto un artefatto diverso

L'automazione Office non può promettere questo. Incorpora timestamp, identificatori di revisione e metadati dipendenti dalla macchina, così lo stesso documento convertito due volte differisce in modi che vanificano l'hashing. Lo stesso ragionamento guida gli identificatori di file deterministici trattati in ID PDF deterministici per build riproducibili: quando l'output è riproducibile, la verifica diventa un confronto invece di un'ispezione

Dove l'output è buono, e dove non lo è

Sii onesto con i tuoi utenti su questo, perché la qualità della conversione varia più con l'input che con il convertitore. I PDF taggati e i documenti aziendali generati in modo pulito, fatture, report, contratti, si convertono bene: i titoli diventano titoli, le tabelle sopravvivono, gli elenchi si rinumerano correttamente in Word. I layout accademici a due colonne si convertono in modo accettabile se la geometria delle colonne è regolare. Le tabelle che attraversano interruzioni di pagina vengono riassemblate per inferenza e talvolta divise. Il materiale marketing fortemente disegnato, dove il testo è posizionato per effetto visivo anziché in ordine di lettura, si converte male, e nessuna quantità di inferenza lo risolve

I documenti scansionati sono un caso completamente a parte. Una pagina che è una grande immagine unica non contiene oggetti di testo, quindi non c'è nulla da esportare finché non esiste un livello di testo; il percorso OCR che ne produce uno è un prerequisito, non un'opzione. Prima di eseguire un batch grande, campiona una dozzina di file rappresentativi e osserva l'output, e valuta di enumerare prima gli elementi di pagina, come descritto in ricerca testuale ed enumerazione degli elementi di pagina, per vedere cosa contengono davvero le pagine

Per pipeline di assistenti e retrieval il percorso Markdown è di solito l'obiettivo migliore: i titoli diventano confini di chunk, le tabelle restano leggibili come tabelle pipe, e i marcatori di pagina danno a ogni chunk una posizione citabile. Per l'editing umano, DOCX è la risposta, perché ciò che l'utente vuole non è il testo ma la possibilità di modificarlo

PDFlibPas è una libreria PDF per Delphi, C++Builder e Lazarus con interfacce DLL e ActiveX corrispondenti, quindi le stesse chiamate di esportazione sono disponibili da C#, C++ o host di scripting. La documentazione completa e una build di prova sono sulla pagina della libreria PDF Delphi PDFlibPas