Teknisk artikkel

Kombinere skannede bilder til én PDF med PDFium-komponent i Delphi

Et team for skadebehandling hadde tretti år med papirfiler som gikk gjennom en arkmater-skanner. Skanneren spyttet ut ett JPEG-bilde per side i en mappe, navngitt 0001.jpg, 0002.jpg, og så videre. Det arkivet egentlig trengte var én PDF per saksmappe, med sidene i rekkefølge, slik at en anmelder kunne åpne ett enkelt dokument i stedet for å klikke gjennom hundre miniatyrbilder. Det siste trinnet, å gjøre en nummerert bunke skanninger om til en enkelt ordnet PDF, er jobben her

PDFium-komponenten håndterer det direkte. Utover gjengivelse og tekstutvinning, kan komponenten bygge en PDF fra bunnen av: opprette et tomt dokument, legge til en tom side i valgfri størrelse, slippe et bilde på den siden i bruker-romskoordinater, og deretter lagre. Hele rørledningen (pipeline) lever på TPdf-komponenten, så en batch-konverterer er en løkke over filnavn pluss en håndfull kall

En Delphi bunke-rørledning bruker PDFium Component AddPage-, PageNumber-, AddImage- og SaveAs-kallene til å gjøre en mappe av nummererte skann om til én ordnet PDF
Hver skann blir én side opprettet av AddPage og sikret av PageNumber før AddImage tegner den; SaveAs skriver det ferdige dokumentet én gang

Formen på konverteringen

Tre ting må skje for hver skanning. Du bestemmer sidestørrelsen, du plasserer bildet inni siden med en marg, og du går videre til neste side. PDFium-komponenten gir deg én metode for hver: AddPage oppretter en tom side ved en gitt størrelse, AddImage (eller AddPicture hvis du allerede har en TPicture) tegner bitkartet inn i den gjeldende siden, og PageNumber forteller komponenten hvilken side etterfølgende tegnekall retter seg mot

Den ene detaljen som feller folk er koordinatsystemet. PDF-brukerrom plasserer opprinnelsen nederst i venstre hjørne av siden, med Y som øker oppover, det motsatte av skjermkoordinatene Delphi-utviklere strekker seg etter på refleks. X, Y-en du sender til AddImage er nederste venstre hjørne av bilderektangelet, og Width, Height er plasseringsstørrelsen i punkter, ikke pikselstørrelsen til kildefilen. Gjør det baklengs, og skanningene dine lander utenfor siden eller opp-ned i forhold til der du forventet dem

Opprette dokumentet og en side per skanning

Start med et tomt dokument. CreateDocument tildeler en fersk PDF og lar komponenten være aktiv, så det er ikke noe eget åpningstrinn. Derfra går du gjennom listen over skannede filer, og for hver enkelt legger du til en side, gjør den gjeldende og plasserer bildet. Sidedimensjonene her er A4 i punkter (595 × 842 portrett), standard arksstørrelse for arkivert korrespondanse

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4-bredde i punkter
  PageH = 842.0;   // A4-høyde i punkter
  Margin = 36.0;   // halvtommers kant rundt hvert skann
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // ny, tom, allerede aktiv
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-basert sideindeks
      Pdf.PageNumber := I + 1;                // gjør den nye siden gjeldende
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Hver iterasjon oppretter en side og setter umiddelbart PageNumber til den. Den andre linjen betyr noe: AddPage setter inn siden, men tegnemetodene virker på den siden som er gjeldende, så å sette PageNumber er det som retter AddImage mot siden du nettopp lagde. Hopper du over det, vil bildene dine stables på uansett hvilken side som tilfeldigvis ble lastet inn før

En antagelse skjuler seg i den løkken: rekkefølgen til Files. En skanner navngir sider 0001.jpg gjennom 0100.jpg, men en katalogopplisting (directory enumeration) returnerer dem ikke alltid sortert, og i det øyeblikket du treffer page9.jpg ved siden av page10.jpg, setter en ren strengsortering side 10 før side 9. Sorter listen eksplisitt før løkken, og foretrekk null-utfylte navn ved skanning slik at leksikal rekkefølge samsvarer med siderekkefølge. Sidesekvens er det ene en anmelder legger merke til med en gang, og det er den billigste feilen å forhindre

Plassering av en skanning og å beholde sideforholdet

En skanning har sjelden samme form som siden. Hvis du strekker den til å fylle arket, forvrenger du teksten; hvis du plasserer den i full pikselstørrelse, flyter den over. Løsningen er å skalere etter det minste av de to forholdene, breddetilpasning eller høydetilpasning, og sentrere det som er til overs. Fordi opprinnelsen sitter nederst til venstre, betyr sentrering å dele restplassen jevnt og legge den til både X og Y

Et A4-sidesdiagram viser hvordan PDFium Component AddImage plasserer en skalert skann innenfor margene ved bruk av Delphi-kode og et origo nede til venstre
AddImage tar nedre venstre hjørne av plasseringsrektanglet, så tilpassing og sentrering beregnes i sidepunkter fra origo
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG osv. via VCL-grafikkenhetene

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Pass innenfor margene uten å forvrenge skanningen.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Sentrer: gjenværende plass deles likt. Y måles fra sidenek bunnen.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Dette laster filen én gang for å lese pikseldimensjonene dens, beregner en enkelt uniform skala, og sender plasseringsrektangelet til AddImage. AddImage aksepterer en filsti direkte og ruter den gjennom den samme bildepipelinen som AddPicture, så ethvert format VCL-grafikkenhetene gjenkjenner, fungerer uten spesialbehandling. Hvis du allerede har bildet dekodet i en TPicture fra en forhåndsvisningsrute, kaller du AddPicture(Pic, X, Y, DrawW, DrawH) med det samme rektangelet og hopper over den andre fillesingen

Hoppe over dekodingen for JPEG-skanninger

Skannere sender nesten alltid ut JPEG. Å laste inn en JPEG i en TPicture dekoder den til et bitkart, og deretter re-koder PDFium den ved lagring, to tapstunge rundturer du ikke trenger. AddJpegImage bygger de originale komprimerte bytene rett inn i siden fra en strøm, som er både raskere og visuelt renere for et stort antall

AddJpegImage bygger inn de originale JPEG-bytene i en PDFium Component-side, mens AddImage dekoder, og SaveAs re-koder pikslene i Delphi
AddJpegImage bygger inn skannerens komprimerte byte som de er, og unngår dekodings- og rekodingspassene som AddImage og SaveAs utfører
var
  Stream: TFileStream;
begin
  // ... etter AddPage + PageNumber for den gjeldende siden ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Bygger inn JPEG-byttene som de er; ingen dekode/re-kode-syklus.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Du beregner fortsatt X, Y, DrawW og DrawH på samme måte, siden du trenger pikseldimensjonene for å skalere. Les de fra filen eller en rask parsing av headeren, og lever den rå strømmen til AddJpegImage. For PNG- eller TIFF-skanninger er AddImage-banen den rette; reserver JPEG-snarveien for formatet den faktisk gjelder for

Merke hver side

Arkiverte skanninger er enklere å revidere når hver side bærer sitt kildefilnavn. AddText tegner en streng ved en brukerrom-koordinat, så en bildetekst sitter rett under bildet. Husk den inverterte Y-aksen: for å sette en etikett under skanningen, subtraherer du fra bildets bunnkant i stedet for å legge til det

// Bildetekst under skanningen: Y avtar mot sidenek bunnen.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Ett siste poeng om lagring. SaveAs er en funksjon som returnerer en boolsk verdi, så i produksjonskode sjekker du resultatet i stedet for å anta at skrivingen lyktes; en full disk eller en låst utdata-sti feiler stille ellers. Når løkken er ferdig og filen er skrevet, har du akkurat det arkivet trengte: én ordnet PDF per saksmappe, sider skalert for å passe, klar til å leses i et hvilket som helst visningsprogram

De samme byggeklossene dekker relaterte jobber. Bytt regelen for dimensjonering per side og du får en fotobok med ett bilde per ark; behold løkken men les fra en TIFF-flersidig kilde og du har en faksarkivkonverterer. Hvis du vil ha et bredere bilde av det å bygge PDF-er programmatisk, se opprette PDF-dokumenter fra bunnen av med PDFium-komponent; for å gjengi resultatet tilbake til skjermen senere, se konvertering av PDF-sider til JPEG-bilder med PDFium-komponent

PDFium-komponenten fra loslab.com samler funksjonene for dokumentoppretting, gjengivelse og tekst som brukes gjennom hele denne serien