Teknisk artikel

Kombiner scannede billeder til én PDF med PDFium Component i Delphi

Et skadebehandlingsteam havde tredive års papirfiler, der gik gennem en arkfødet scanner. Scanneren spyttede én JPEG ud pr. side i en mappe, navngivet 0001.jpg, 0002.jpg og så videre. Hvad arkivet faktisk havde brug for var én PDF pr. sagsmappe, med siderne i rækkefølge, så en anmelder kunne åbne et enkelt dokument i stedet for at klikke sig igennem hundrede billedminiaturer. Dette sidste trin, at forvandle en nummereret bunke scanninger til en enkelt ordnet PDF, er opgaven her

PDFium Component håndterer det direkte. Ud over rendering og tekstudtrækning kan komponenten bygge en PDF fra bunden: opret et tomt dokument, tilføj en blank side af en hvilken som helst ønsket størrelse, slip et billede på den side i brugerområdekoordinater (user-space), og gem derefter. Hele pipelinen lever på TPdf-komponenten, så en batchkonverter er en løkke over filnavne plus en håndfuld kald

Konverteringens form

Tre ting skal ske for hver scanning. Du bestemmer sidestørrelsen, du placerer billedet inde på siden og efterlader en margen, og du går videre til næste side. PDFium Component giver dig én metode til hver: AddPage opretter en blank side med en given størrelse, AddImage (eller AddPicture hvis du allerede har en TPicture) tegner bitmappet ind på den aktuelle side, og PageNumber fortæller komponenten, hvilken side efterfølgende tegnekald retter sig mod

Den ene detalje, der snyder folk, er koordinatsystemet. PDF's brugerområde sætter origo (nulpunktet) i nederste venstre hjørne af siden, med Y stigende opad, det modsatte af de skærmkoordinater, Delphi-udviklere rækker ud efter af refleks. De X, Y du overfører til AddImage er nederste venstre hjørne af billedrektanglet, og Width, Height er placeringsstørrelsen i punkter, ikke pixestørrelsen på kildefilen. Gør du det omvendt, lander dine scanninger uden for siden eller på hovedet i forhold til, hvor du forventede dem

Oprettelse af dokumentet og en side pr. scanning

Start med et tomt dokument. CreateDocument allokerer en frisk PDF og efterlader komponenten aktiv, så der er intet separat åbningstrin. Derfra gennemgår du listen over scannede filer, og for hver fil tilføjer du en side, gør den aktuel og placerer billedet. Sidedimensionerne her er A4 i punkter (595 × 842 portræt), standardarkstørrelsen for arkiveret korrespondance

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4 width in points
  PageH = 842.0;   // A4 height in points
  Margin = 36.0;   // half-inch border around each scan
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // new, empty, already active
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-based page index
      Pdf.PageNumber := I + 1;                // make the new page current
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Hver iteration opretter en side og sætter straks PageNumber til den. Denne anden linje er vigtig: AddPage indsætter siden, men tegnemetoderne virker på den side, der er aktuel, så det at sætte PageNumber er det, der retter AddImage mod den side, du lige har lavet. Springer du dette over, stables dine billeder oven på den side, der tilfældigvis var indlæst før

Én antagelse gemmer sig i den løkke: rækkefølgen af Files. En scanner navngiver sider 0001.jpg til og med 0100.jpg, men en mappeopregning returnerer dem ikke altid sorteret, og i det øjeblik du støder på page9.jpg ved siden af page10.jpg, vil en simpel strengsortering placere side 10 før side 9. Sorter listen eksplicit før løkken, og foretræk nulfyldte navne på scanningstidspunktet, så den leksikalske rækkefølge matcher siderækkefølgen. Siderækkefølge er det ene, en anmelder lægger mærke til med det samme, og det er den billigste fejl at forhindre

Placering af en scanning og bevarelse af dens billedformat

En scanning har sjældent samme form som siden. Hvis du strækker den for at fylde arket, forvrænger du teksten; hvis du placerer den i fuld pixelstørrelse, flyder den over. Løsningen er at skalere efter det mindste af de to forhold, bredde-tilpasning eller højde-tilpasning, og centrere det, der er tilovers. Fordi origo sidder nederst til venstre, betyder centrering at opdele det resterende rum ligeligt og tilføje det til både X og Y

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, etc. via the VCL graphics units

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

    // Fit inside the margins without distorting the scan.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Center: leftover space split evenly. Y measured from the page bottom.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

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

Dette indlæser filen én gang for at læse dens pixeldimensioner, beregner en enkelt ensartet skala og sender placeringsrektanglet til AddImage. AddImage accepterer en filsti direkte og sender den gennem den samme billedpipeline som AddPicture, så ethvert format, VCL-grafikenhederne genkender, fungerer uden særbehandling. Hvis du allerede har billedet afkodet i en TPicture fra et forhåndsvisningspanel, så kald AddPicture(Pic, X, Y, DrawW, DrawH) med det samme rektangel og spring den anden filindlæsning over

At springe afkodningen over for JPEG-scanninger

Scannere udsender næsten altid JPEG. At indlæse en JPEG i en TPicture afkoder den til en bitmap, hvorefter PDFium genkoder den, når der gemmes; to tabsgivende ture frem og tilbage, du ikke har brug for. AddJpegImage indlejrer de originale komprimerede bytes direkte på siden fra en stream, hvilket er både hurtigere og visuelt renere for en stor batch-kørsel

var
  Stream: TFileStream;
begin
  // ... after AddPage + PageNumber for the current page ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Embeds the JPEG bytes as-is; no decode/re-encode cycle.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Du beregner stadig X, Y, DrawW og DrawH på samme måde, da du skal bruge pixeldimensionerne til at skalere. Læs dem fra filen eller en hurtig header-parsning, og giv derefter den rå stream til AddJpegImage. For PNG- eller TIFF-scanninger er AddImage-stien den rigtige; reserver JPEG-genvejen til det format, den rent faktisk gælder for

Mærkning af hver side

Arkiverede scanninger er nemmere at revidere, når hver side bærer sit kildefilnavn. AddText tegner en streng ved et brugerområdekoordinat, så en billedtekst sidder lige under billedet. Husk den omvendte Y-akse: for at sætte en mærkat under scanningen, skal du trække fra billedets nederste kant i stedet for at lægge til den

// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Et sidste punkt omkring at gemme. SaveAs er en funktion, der returnerer en boolsk værdi, så i produktionskode bør du kontrollere dens resultat i stedet for at antage, at skrivningen lykkedes; en fuld disk eller en låst output-sti fejler ellers i stilhed. Når løkken er færdig og filen er skrevet, har du præcis, hvad arkivet havde brug for: én ordnet PDF pr. sagsmappe, sider skaleret til at passe, klar til at blive læst i enhver fremviser

De samme byggeklodser dækker relaterede opgaver. Byt om på reglen for sidestørrelse pr. side, og du får en fotobog med ét billede pr. ark; behold løkken, men læs fra en flersidet TIFF-kilde, og du har en fax-arkivkonverter. Hvis du vil have det bredere billede af at bygge PDF'er programmatisk, se oprettelse af PDF-dokumenter fra bunden med PDFium Component; for at rendere resultatet tilbage til skærmen senere, se konvertering af PDF-sider til JPEG-billeder med PDFium Component

PDFium Component fra loslab.com samler de dokumentoprettelses-, renderings- og tekst-API'er, der bruges i hele denne serie