Техническа статия

Обединяване на сканирани изображения в един PDF с PDFium Component в Delphi

Екип за обработка на искове имаше хартиени досиета от тридесет години, преминаващи през листов скенер. Скенерът изплюваше по един JPEG на страница в папка, наименувани 0001.jpg, 0002.jpg и така нататък. Това, от което архивът всъщност се нуждаеше, беше един PDF на досие, със страниците в ред, така че проверяващият да може да отвори един единствен документ, вместо да кликва през сто миниатюри на изображения. Тази последна стъпка, превръщането на номерирана купчина сканирания в един подреден PDF, е задачата тук

PDFium Component се справя с това директно. Отвъд рендирането и извличането на текст, компонентът може да изгради PDF от нулата: да създаде празен документ, да добави празна страница с какъвто размер пожелаете, да пусне изображение върху тази страница в координати на потребителското пространство, след което да запази. Целият конвейер живее в компонента TPdf, така че пакетният конвертор е цикъл върху имена на файлове плюс шепа извиквания

Формата на конвертирането

За всяко сканиране трябва да се случат три неща. Решавате размера на страницата, поставяте изображението вътре в страницата, оставяйки поле (margin), и преминавате към следващата страница. PDFium Component ви дава по един метод за всяко: AddPage създава празна страница със зададен размер, AddImage (или AddPicture, ако вече държите TPicture) рисува растерната графика в текущата страница, а PageNumber казва на компонента коя страница е цел на последващите извиквания за рисуване

Единствената подробност, която препъва хората, е координатната система. Потребителското пространство на PDF поставя началото в долния ляв ъгъл на страницата, като Y нараства нагоре, обратното на екранните координати, към които разработчиците на Delphi посягат рефлекторно. X, Y, които подавате на AddImage, е долният ляв ъгъл на правоъгълника на изображението, а Width, Height са размерът на разполагане в точки, а не размерът в пиксели на изходния файл. Объркайте това и вашите сканирания ще се приземят извън страницата или обърнати с главата надолу спрямо това, където сте ги очаквали

Създаване на документа и по страница за сканиране

Започнете с празен документ. CreateDocument заделя нов PDF и оставя компонента активен, така че няма отделна стъпка за отваряне. Оттам обхождате списъка със сканирани файлове и за всеки от тях добавяте страница, правите я текуща и поставяте изображението. Размерите на страницата тук са A4 в точки (595 × 842 портрет), стандартният размер на лист за архивирана кореспонденция

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;

Всяка итерация създава страница и незабавно задава PageNumber към нея. Този втори ред има значение: AddPage вмъква страницата, но методите за рисуване действат върху страницата, която е текуща, така че задаването на PageNumber е това, което насочва AddImage към страницата, която току-що сте направили. Пропуснете го и вашите изображения ще се подредят върху страницата, която случайно е била заредена преди това

Едно допускане се крие в този цикъл: редът на Files. Скенерът наименува страниците от 0001.jpg до 0100.jpg, но изброяването на директория не винаги ги връща сортирани, и в момента, в който попаднете на page9.jpg до page10.jpg, обикновеното сортиране на низове поставя страница 10 преди страница 9. Сортирайте списъка изрично преди цикъла и предпочитайте имена, допълнени с нули по време на сканиране, така че лексикалният ред да съвпада с реда на страниците. Последователността на страниците е нещото, което проверяващият забелязва веднага, и това е най-евтината грешка за предотвратяване

Поставяне на сканиране и запазване на пропорциите му

Сканирането рядко има същата форма като страницата. Ако го разтегнете, за да запълни листа, изкривявате текста; ако го поставите в пълен размер в пиксели, то прелива. Решението е да се мащабира по по-малкото от двете съотношения, побиране в ширина (width-fit) или побиране във височина (height-fit), и да се центрира това, което остава. Тъй като началото седи долу вляво, центрирането означава разделяне на оставащото пространство поравно и добавянето му към X и 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;

Това зарежда файла веднъж, за да прочете размерите му в пиксели, изчислява единен равномерен мащаб и предава правоъгълника за разполагане на AddImage. AddImage приема път към файл директно и го насочва през същия конвейер за изображения като AddPicture, така че всеки формат, който графичните единици на VCL разпознават, работи без специални случаи. Ако вече имате изображението декодирано в TPicture от панел за преглед, извикайте AddPicture(Pic, X, Y, DrawW, DrawH) със същия правоъгълник и пропуснете второто четене на файл

Пропускане на декодирането за JPEG сканирания

Скенерите почти винаги излъчват JPEG. Зареждането на JPEG в TPicture го декодира в растерна графика (bitmap), а след това PDFium го прекодира при запазване, две двупосочни пътувания със загуби, от които не се нуждаете. AddJpegImage вгражда оригиналните компресирани байтове направо в страницата от поток, което е както по-бързо, така и визуално по-чисто за партида с голям обем

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;

Все още изчислявате X, Y, DrawW и DrawH по същия начин, тъй като имате нужда от размерите в пиксели за мащабиране. Прочетете ги от файла или чрез бърз анализ на заглавката, след което предайте суровия поток на AddJpegImage. За PNG или TIFF сканирания пътят през AddImage е правилният; запазете прекия път за JPEG за формата, за който той действително се отнася

Етикетиране на всяка страница

Архивираните сканирания са по-лесни за проверка, когато всяка страница носи името на изходния си файл. AddText рисува низ на координата в потребителското пространство, така че надписът седи точно под изображението. Не забравяйте обърнатата ос Y: за да поставите етикет под сканирането, изваждате от долния ръб на изображението, вместо да добавяте към него

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

Една последна точка относно запазването. SaveAs е функция, която връща Boolean, така че в производствен код проверявайте резултата от нея, вместо да приемате, че записът е бил успешен; пълен диск или заключен изходен път се провалят мълчаливо в противен случай. След като цикълът приключи и файлът е записан, имате точно това, от което архивът се е нуждаел: по един подреден PDF на досие, страници, мащабирани за побиране, готови за четене във всяка програма за преглед

Същите градивни елементи покриват свързани задачи. Разменете правилото за оразмеряване на всяка страница и получавате фотокнига с по едно изображение на лист; запазете цикъла, но четете от многостраничен TIFF източник и имате конвертор за факс-архиви. Ако искате по-широката картина на програмното изграждане на PDF файлове, вижте създаване на PDF документи от нулата с PDFium Component; за да рендирате резултата обратно на екрана по-късно, вижте конвертиране на PDF страници в JPEG изображения с PDFium Component

PDFium Component от loslab.com обединява API-тата за създаване на документи, рендиране и текст, използвани в тази поредица