Technical Article

PDF dokumentų spausdinimas su PDFium VCL Delphi aplinkoje

PDF koordinatės matuojamos taškais (points), spausdintuvo koordinatės – įrenginio vienetais (device units), ir šios dvi sistemos neturi nieko bendro, kol jų apgalvotai nekonvertuojate. Šis nesutapimas yra daugelio prastų spausdinimo rezultatų Delphi programose priežastis: kodas siunčia teisingą failą, tačiau puslapis išeina apkarpytas, ištemptas arba tuščias. „PDFium VCL“ švariai tvarko atvaizdavimo dalį, o spausdintuvo pajungimas yra standartinė VCL dalis. Abi dalys dera tarpusavyje su nedideliu kodo kiekiu, kai suprantate, ko tikisi kiekviena pusė.

Kaip veikia atvaizdavimo ir spausdinimo konvejeris

„PDFium VCL“ tiesiogiai su spausdintuvais nebendrauja. Modelis yra toks: atvaizduokite puslapį į TBitmap pasirinkta rezoliucija, tada perduokite šį taškinį paveikslėliui į spausdintuvo drobę (canvas) naudodami StretchDIBits. Funkcija TPdf.RenderPage grąžina iškvietėjui priklausantį taškinį paveikslėlį, todėl jūs patys valdote jo pikselių matmenis. Perduokite parinktį [rePrinting] ir PDFium pakeis savo atvaizdavimo kelią į tokį, kuris praleidžia tik ekranui skirtus efektus (tokius kaip LCD subpikselių patarimai – hinting) ir teisingai apdoroja puslapio MediaBox spausdinimui. Nenaudokite rePrinting, ir tai, ką nusiųsite spausdintuvui, bus ekranui skirtas vaizdas, kuris gerai atrodo monitoriuje, tačiau didelės raiškos (high-DPI) spausdintuvuose gali atrodyti neryškus, nes sprendimai dėl šriftų patarimų, priimti 96 DPI ekranams, netinka 300 ar 600 DPI spausdinimui.

Savybė TPdf.Active yra vienintelė sąlyga, kurią reikia patikrinti prieš naudojant bet kurią puslapio savybę. Komponentas tyliai nutildo įkėlimo klaidas: nustačius Active := True sugadintam ar slaptažodžiu apsaugotam failui, išimtis nekyla; tiesiog Active lieka False. Visada patikrinkite šią savybę po priskyrimo. Nuskaitant PageCount arba PageWidth neaktyviame dokumente, grąžinamas nulis, o tai sukelia tylius veiksmus, kuriuos labai sunku diagnozuoti, kai jie pasiekia spausdinimo eilių valdyklę (spooler).

Minimalus spausdinimo ciklas

Paprasčiausias veikiantis atvejis įkelia failą, pradeda spausdinimo užduotį, pereina per puslapius ir užsidaro. Vienintelė svarbi detalė – Printer.NewPage neturi būti iškviečiamas prieš pirmąjį puslapį, tam ir naudojama vėliavėlė FirstPage. Perdavimas per StretchDIBits naudoja GetDIBSizes ir GetDIB, kad gautų nuo įrenginio nepriklausomus bitus iš taškinio paveikslėlio rankenos (handle), o tada nupiešia juos spausdintuvo drobėje pilnu puslapio dydžiu:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Perduodant Printer.PageWidth ir Printer.PageHeight kaip taškinio paveikslėlio matmenis, atvaizdavimas atliekamas spausdintuvo gimtąja taškų rezoliucija, kuri jau atsižvelgia į įrenginio DPI. Tame iškvietimas StretchDIBits atvaizduoja šiuos pikselius santykiu 1:1 puslapyje. Tai suteikia geriausią įmanomą tikslumą nenaudojant jokios aiškios DPI aritmetikos, tačiau tai veikia tik tada, kai PDF puslapis ir fizinis popierius sutampa savo dydžiu. Kai jie skiriasi, reikalingas aiškus mastelio keitimas.

Mastelio keitimas, kai puslapio ir popieriaus dydžiai skiriasi

Vertikalus A4 formato PDF puslapis automatiškai netelpa į US Letter formato spausdintuvą, o horizontalus puslapis, pateiktas vertikaliam spausdintuvui, bus apkarpytas. Standartinis metodas – apskaičiuoti vienodą mastelio koeficientą pagal spausdintuvo pikselių ir PDF taškų (points) santykį, tada pritaikyti jį abiem matmenims, išlaikant kraštinių santykį. Pdf.PageWidth ir Pdf.PageHeight pateikia esamus puslapio matmenis taškais, kur vienas taškas yra 1/72 colio. Padauginus iš tikslinio DPI ir padalijus iš 72, gaunami pikseliai toje rezoliucijoje. Paimkite mažiausią (Min) iš X ir Y santykių, kad gautumėte didžiausią mastelį, kuris vis dar telpa spausdinimo srityje:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Atvaizdavimas esant Dpi = 300 tinka daugumai biuro spausdintuvų. Esant 600 DPI, vieno A4 puslapio taškinis paveikslėlis siekia maždaug 34 megapikselius, o tai sudaro apie 100 MB kaip 32 bitų taškinis paveikslėlis; kokybės pagerėjimas įprastiems tekstiniams dokumentams yra minimalus, o atminties sąnaudos puslapiui yra didelės. Naudokite 600 DPI tik spaustuvėse arba sudėtingiems vektoriniams brėžiniams, kur tai tikrai svarbu.

Vėliavėlė reAnnotations antrame kodo bloke yra nepriklausoma nuo rePrinting. Įtraukite ją, kai vartotojas tikisi, kad ant popieriaus atsiras spaudai, paryškinimai ir komentarų laukeliai. Praleiskite ją, jei spausdinamas tik turinys. Abi vėliavėlės gali būti derinamos laisvai.

Puslapio pasukimas

PDFium saugo puslapio pasukimą PDF faile kaip įrašą /Rotate, pasiekiamą per Pdf.PageRotation, kuris grąžina TRotation reikšmę (ro0, ro90, ro180, ro270). Spausdintuvo koordinačių sistema apverčia 90 ir 270 laipsnių pasukimus ekrano atžvilgiu. Jei perduosite neapdorotą PageRotation reikšmę tiesiai į RenderPage bez jokių korekcijų, horizontalūs puslapiai vertikaliame dokumente daugelyje Windows spausdintuvų tvarkyklių bus atspausdinti atvirkščiai. Sprendimas yra paprastas sukeitimas prieš atvaizdavimo iškvietimą: susiekite ro90 su ro270, o ro270 – su ro90, palikdami ro0 ir ro180 nepakeistus.

Prieš platindami programą, patikrinkite šį elgesį su konkrečiu tiksliniu spausdintuvu. Tvarkyklių elgsena dėl pasukimo skiriasi priklausomai nuo gamintojo, o kai kurios tvarkyklės taiko savo pasukimo korekciją GDI lygmeniu. Jei matote dvigubą pasukimą, pašalinkite sukeitimą; jei korekcijos visiškai nėra, pridėkite ją. Dokumentas su įvairių orientacijų puslapiais (pakaitomis vertikaliais ir horizontaliais) yra greičiausias būdas pastebėti bet kurį iš šių gedimų testavimo metu.

Atminties valdymas ilgos spausdinimo užduoties metu

Kiekvienas RenderPage iškvietimas sukuria naują TBitmap, kurį valdo iškvietėjas ir privalo jį atlaisvinti. Aukščiau pateiktame cikle try/finally Bitmap.Free blokas teisingai atlieka šį darbą po vieną puslapį. Nekaupkite taškinių paveikslėlių per kelis puslapius: 300 DPI atvaizdavimas 200 puslapių dokumentui sunaudotų gigabaitus atminties dar prieš pirmajam puslapiui pasiekiant spausdinimo eilių valdyklę. Atlaisvinkite kiekvieną taškinį paveikslėlį prieš pereidami prie kito puslapio.

Pora AllocMem / FreeMem perdavimo bloko viduje vadovaujasi ta pačia taisykle. GetDIBSizes praneša, kiek atminties reikia DIB antraštei ir taškų duomenims; jūs išskiriate atmintį, užpildote, nupiešiate ir atlaisvinate – visa tai atliekama vieno puslapio rėmuose. Pamiršus atlaisvinti atmintį, spausdinimo užduotis išeikvos proceso krūvą (heap) apdorojant ilgesnius nei kelių dešimčių puslapių dokumentus.

Jei reikia vykdyti spausdinimo užduotis foninėje gijoje, laikykite TPdf ir visus VCL spausdintuvo iškvietimus toje pačioje gijoje. TPdf pats savaime nėra saugus gijoms (thread-safe) tarp egzempliorių, besidalijančių globalia PDFium DLL būsena; saugiausias modelis yra po vieną TPdf egzempliorių gijai, kiekvienam įkeliant savo failo kopiją.

Čia parodyta atvaizdavimo ir dokumentų API yra „PDFium VCL Component“, skirto Delphi ir C++Builder, dalis.