Technisch artikel

Grote PDF's samenvoegen en splitsen in Delphi met PDF Library for Delphi

Een PDF van twee gigabyte samenvoegen of splitsen op de voor de hand liggende manier kost u twee dingen tegelijk: verstreken tijd en adresruimte. De voor de hand liggende manier is elke invoer laden, het werk doen, de uitvoer schrijven. Het laden is waar het breekt. Een scanarchief dat van 300 naar 600 DPI gaat verdubbelt zijn lineaire resolutie en groeit op schijf ruwweg viermaal, dus dezelfde assemblagejob die het hele jaar 400 MB-bestanden afhandelde begint te thrashen zodra een invoer een gigabyte overschrijdt, vaak terwijl hij niet meer doet dan pagina's tellen. De taak werd nooit moeilijker. Openen, tellen, bereiken kiezen, samenvoegen is het hele verhaal. Volledige-boom-laden hield eenvoudig op een zinnige standaard te zijn op die grootte. PDF Library for Delphi, losLab's PDF-bibliotheek voor Delphi en C++Builder, beantwoordt dit met zijn Direct Access-laag: een familie van DA-geprefixte functies ondersteund door een streaming reader die de cross-reference-tabel in-place doorloopt in plaats van het hele document in het geheugen op te bouwen

Waar het geheugen heen gaat bij een volledige lading

Een PDF "normaal" laden betekent de xref parsen, elk indirect object herleiden tot een in-memory boom, objectstreams decoderen, en de paginaboom, lettertypen en annotaties verbinden tot objecten die u kunt manipuleren. Voor bewerkingsworkflows is dat de juiste afweging. Voor samenvoeg-, splits- en inspectiewerk is het grotendeels verspilling. Een scanarchief van 30.000 pagina's kan miljoenen indirecte objecten bevatten, en een splitsjob hoeft er slechts enkele honderden te lezen: de paginaknooppunten in het gevraagde bereik, plus wat die knooppunten refereren

De Direct Access-laag keert het model om. DAOpenFile en DAOpenFileReadOnly parsen de trailer en xref, een paar kilobytes aan het staarteinde van het bestand, en retourneren een bestands-handle. Objecten worden lui opgehaald wanneer een aanroep ze nodig heeft. Het praktische gevolg is dat het openen van een multigigabyte-bestand ongeveer even lang duurt als het openen van een klein bestand, en het geheugen volgt wat u aanraakt in plaats van wat het bestand bevat

Vergelijking van PDF Library for Delphi tussen het laden van een gigabyte-PDF in een volledige in-memory objectboom en het openen met directe toegang, waarbij het parseren stopt bij de trailer en xref en een handle luie per-object-lezingen verzorgt
Een volledige laadactie decodeert elk indirect object voordat het samenvoegen kan beginnen, dus RAM en opensnelheid schalen mee met het archief. Het direct-access-pad geeft na het lezen van kilobytes een werkende handle terug en laat elke aanroep alleen de objecten ophalen die het nodig heeft

Een enorm bestand peilen zonder het te laden

Het patroon hieronder komt uit de eigen large-file-benchmark van de bibliotheek: alleen-lezen openen, vragen stellen, sluiten. Er bestaat nooit een documentboom

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Alleen-lezen-modus is de voorkeur waard wanneer u kunt: het laat de intake-fase draaien terwijl andere processen het bestand vasthouden, en het documenteert intentie. Een probe-fase die per ongeluk een muterende functie aanroept faalt snel in plaats van het archief te corrumperen

PageRef is een object-handle, geen paginanummer

De meest voorkomende fout met de DA-API is een paginanummer doorgeven waar een functie een PageRef verwacht. Bijna elke per-pagina DA-aanroep neemt een referentie-handle naar het paginaobject in plaats van een paginanummer: DAExtractPageText, DARenderPageToFile, DARotatePage en DACapturePage verwachten allemaal een ref. U krijgt er een door het mensgerichte getal te vertalen via DAFindPage:

PDF Library for Delphi: Vertaalstroom van paginanummer naar PageRef die toont dat DAFindPage de per-pagina directe-toegangsaanroepen voedt, terwijl een rauwe integer-ref op een willekeurig object belandt en stilletjes verkeerde-paginatekst oplevert
Elke directe-toegangsaanroep per pagina verbruikt een PageRef die DAFindPage produceert, nooit het voor mensen leesbare nummer. Die vertaling overslaan maakt van het geheel getal een vermomd object-id, en tekst van de verkeerde pagina kan onzichtbaar worden geleverd
PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Het ruwe getal 250 doorgeven in plaats daarvan roept geen fout op. Het adresseert welk object dan ook dat toevallig achter die handle-waarde zit, wat op een goede dag zichtbaar faalt en op een slechte dag tekst uit de verkeerde pagina extraheert in een klantgericht document. Als u de DA-laag in uw eigen servicecode verpackt, maak de vertaling dan onmogelijk over te slaan: accepteer paginanummers aan de grens, roep onmiddellijk DAFindPage aan, en geef intern alleen refs door

Honderden bestanden samenvoegen met een benoemde lijst

Voor twee bestanden is MergeFiles(First, Second, Output) voldoende. Batch-assemblage schaalt beter via bestandslijsten: registreer invoeren onder een lijstnaam en voeg de lijst dan in één pas samen

PDF Library for Delphi: Werkstroom met benoemde bestandslijst waarin januari-, februari- en maartafschriften onder één lijstnaam registreren en in één passe fuseren, waarbij de Fast-, standaard- en strikte varianten behoud van de structuurboom afwegen tegen snelheid
Honderden geregistreerde invoer vallen uiteen in één MergeFileList-pass waarvan het resultaat in milliseconden verifieert via een andere alleen-lezen probe. De variant is een beslissing per pijplijn, want Fast laat de Tagged PDF-structuurboom vallen
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Het resultaat goedkoop verifiëren: opnieuw directe toegang
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

De samenvoeg-familie heeft drie varianten, en het verschil is niet alleen snelheid. MergeFileListFast slaat behoud van de structuurboom over; MergeFileListStrict dwingt strict-modus af; de ongevormde versie is het gebalanceerde midden. De operationele regel die daaruit voortkomt: als een invoer een getagde PDF is waarvan de toegankelijkheidsstructuur moet overleven, alles wat voor PDF/UA wordt geproduceerd het voor de hand liggende geval, grijp dan naar de standaard- of Strict-variant, want Fast laat de structuurboom stilletjes vallen. Voor gewone scanarchieven zonder tagging is Fast gratis prestatie. Beslis per pipeline, niet per ontwikkelaarsstemming, en leg de gebruikte variant vast in het job-log

Splitsen zonder laden: bereikextractie

Splitsen volgt dezelfde geen-lading-filosofie. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) trekt een paginabereik rechtstreeks van bestand naar bestand, met een bereiklijst zoals '1-500', '501-1000' of door komma's gescheiden selecties, en de bron wordt nooit een documentboom. Wanneer een document om andere redenen al is geladen, produceert ExtractPageRanges een nieuw in-memory document uit het huidige, en CopyPageRanges trekt bereiken over vanuit een ander geladen document op ID. Voor per-afschrift-splitsen van geconsolideerde printstreams is de bestand-naar-bestand-vorm degene die voorkomt dat een 4 GB-invoer ooit in RAM opzwelt

Bestanden die liegen over hun geometrie

Large-file-pipelines ontmoeten beschadigde bestanden in een tempo dat small-file-pipelines nooit zien, eenvoudigweg omdat de invoeren door meer systemen gaan. Twee faalvormen verdienen expliciete afhandeling

Ten eerste, verschoven headers. Mail-gateways en print-spoolers prependen soms bytes aan een PDF, zodat de %PDF-marker niet langer op offset 0 staat en elke xref-offset in het bestand met hetzelfde bedrag fout is. De streaming reader detecteert dit en toont het (DAShiftedHeader op het platte niveau, ShiftedHeader op TSmartPDFReader), en compenseert het vervolgens tijdens het lezen. Zelfgebouwde offset-rekenkunde doet dat meestal niet, vandaar dat "werkt op elk bestand dat we genereren, faalt op bestanden van klant X" het klassieke symptoom is

Ten tweede, kapotte cross-reference-tabellen. DACopyFile(InputFileName, OutputFileName, PageCount) streamt het hele bestand naar een nieuwe kopie terwijl de xref wordt herbouwd, en retourneert het paginatotaal als bijproduct. Het uitvoeren ervan als een normalisatiestap vóór een kieskeurige downstream-consument zet een klasse van intermitterende parse-fouten om in één voorspelbare reparatiestap. En wanneer uw eigen bewerkingen opslaan moeten, schrijft DAAppendFile ze als een incrementele update, waarmee een nieuwe revisie wordt toegevoegd in plaats van gigabytes te herschrijven, wat de opslagkosten proportioneel houdt aan de wijziging in plaats van aan het bestand

Leverdetails: linearisatie en compositie

Twee aangrenzende mogelijkheden ronden een large-file-pipeline af. Wanneer de geassembleerde uitvoer over HTTP wordt geserveerd voor weergave in de browser, reorganiseert LinearizeFile die voor byte-range-streaming zodat de eerste pagina toont voordat de rest van een 500 MB-pakket is gedownload. Voer het uit als de laatste fase, na al het samenvoegen, want elke latere wijziging de-lineariseert het bestand weer. En wanneer pakketten compositie nodig hebben in plaats van gewone concatenatie, zeg een omslag die achter elk afschrift wordt gestempeld of twee bronpagina's die op één uitvoerblad worden samengevoegd, maakt DACapturePage van elke pagina een herbruikbaar sjabloon dat DADrawCapturedPage op een bestemmingspagina in een willekeurige rechthoek plaatst, nog steeds zonder een volledige documentlading op de multigigabyte-bron

Limieten en wat alleen-lezen blijft

Het formaat zelf raakt de limiet lang voordat Direct Access dat doet. Offsets zijn Int64 de hele weg door de DA-laag, dus de echte plafonds zijn beschikbare schijfruimte en het 10-cijferige xref-offsetveld van klassieke (non-stream) cross-reference-tabellen. Multigigabyte-scanarchieven zijn in de praktijk onopvallend, en het geheugen blijft gebonden ongeacht de bestandsgrootte omdat objecten alleen worden gelezen wanneer een aanroep erom vraagt

Twee vragen komen vaak genoeg voor om ze direct te beantwoorden. Samenvoegen via het standaardpad draagt documentstructuur over, dus bladwijzers en links overleven; de Fast-variant is degene die de structuurboom voor snelheid inruilt, wat precies de reden is om hem voor ongetagde invoeren te reserveren. De veilige gewoonte is om de samengevoegde uitvoer te openen, de outline ervan te doorlopen en een paar interne links te steekproeven voordat u verzendt. Wat betreft bewerken: er is een nuttig middengebied tussen alleen-lezen-proben en een volledige lading. Paginaniveau-bewerkingen werken rechtstreeks op de handle, DARotatePage, DAMovePage en DAHidePage daaronder, samen met formulierveld-lezingen, en DAAppendFile persisteert die bewerkingen als een incrementele revisie. Inhoudsniveau-bewerken, alles wat de markerings-operatoren binnen een pagina herschrijft, behoort nog steeds tot de volledige documentlaag

Gerelateerde artikelen

Als uw samengevoegde uitvoer toegankelijk moet blijven, wordt de achtergrond van de structuurboom behandeld in het getagde PDF-toegankelijkheidsartikel, dat precies uitlegt wat de Fast-samenvoegvariant zou weggooien. Voor het eruit trekken van inhoud uit de bereiken die u splitst, zie de gids voor tekst-, afbeeldings- en lettertype-extractie

De volledige Direct Access-functielijst wordt meegeleverd met de bibliotheek; edities en trial-downloads staan op de PDF Library for Delphi-productpagina