Tekninen artikkeli

RTF-tiedostojen muuntaminen (Transforming RTF files into) PDF:ksi (PDF): Kehittäjän (developers) opas (guide)

RTF on ollut olemassa riittävän kauan, jotta se ilmaantuu paikkoihin, joita kukaan ei suunnitellut: vanhoihin raporttigeneraattoreihin, sähköpostien yhdistämisputkiin, laillisten asiakirjojen arkistoihin, jotka ovat ajalta ennen nykyaikaisia tekstinkäsittelyohjelmia. Sen muuntaminen PDF:ksi lennosta on toistuva vaatimus, ja menetelmä, joka oikeasti toimii Windowsissa, ei ole omistettu RTF-jäsennin, vaan renderöintireitti, jonka Windows itse jo tarjoaa TRichEdit- ja EM_FORMATRANGE-komentojen kautta. losLab PDF Libraryn DLL-versio paljastaa virtuaalisen laitekontekstin, joka liittyy suoraan kyseiseen putkeen

Mekanismi: virtuaalinen DC ja EM_FORMATRANGE

Rich Edit -kontrollit voivat sivuttaa sisältönsä millä tahansa laitekontekstilla, ei vain fyysisellä tulostimella. EM_FORMATRANGE-viesti kertoo kontrollille, että sen on aseteltava merkkialue tiettyyn DC:hen, ja palauttaa sen viimeisen merkin sijainnin, jonka se sai sovitettua. Kutsu sitä toistuvasti, siirtäen cpMin-arvoa joka kerta, ja saat sivukohtaisen tulosteen. losLab PDF Libraryn GetCanvasDC tarjoaa muistissa olevan DC:n, joka on mitoitettu määrittämiesi sivumittojen kokoiseksi; sen jälkeen kun sivu on renderöity siihen, LoadFromCanvasDc nappaa tuloksen PDF-sivuna. Se on koko putki

Yksi asia on syytä tehdä oikein alusta alkaen: TRichEdit-kontrolli on mitoitettava kohdesivua vastaavaksi. Jos kontrolli on pienempi tai suurempi kuin DC:n mitat, sivutus ei vastaa sitä, mitä PDF:ään päätyy. A4-tulosteelle vakiotapa on asettaa kontrollin pikselimitat vastaamaan 210 x 297 mm:ää 96 DPI:llä ennen RTF-tiedoston lataamista, käyttäen samoja skaalausapureita, joita käytät DC:n mitoittamiseen

PDF: RTF:stä PDF:hen -putki: mitoitettu Rich Edit -kontrolli sivuttaa tekstinsä virtuaalisen kankaan DC:hen EM_FORMATRANGEn kautta ja kukin kierros kaapataan yhdeksi PDF-sivuksi
EM_FORMATRANGE asettaa RTF-tekstin alueen virtuaalikankaan DC:hen ja LoadFromCanvasDc sieppaa tuloksen toistaen, kunnes viimeinen merkki saavutetaan

Delphi-toteutus

Seuraavassa käytetään PDFlibAX_TLB-tuontiyksikköä, joka käärii kirjaston DLL-version. Lomake isännöi TRichEdit-komponenttia ja painiketta; lomakkeen OnCreate-käsittelijä mitoittaa kontrollin ja lataa RTF:n, ja painikkeen napsautus ohjaa muunnossilmukkaa

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Mitoita kontrolli A4-kokoiseksi näytön DPI:llä, jotta sivutus vastaa DC:tä
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // Hanki A4-kokoiseksi mitoitettu virtuaalinen DC
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Renderöi RTF-sisällön seuraava sivu DC:hen
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Nappaa DC:n sisältö PDF-asiakirjana
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // merkitsee viimeistä sivua
end;

end.

Mitä silmukka tekee

PrintRtfBox täyttää TFormatRange-rakenteen ja välittää sen Rich Edit -kontrollille SendMessage-kutsun kautta. Kontrolli renderöi merkkejä alkaen kohdasta cpMin, pysähtyen, kun DC täyttyy, ja palauttaa ensimmäisen merkin sijainnin, joka ei mahtunut. Kun paluuarvo on yhtä suuri tai suurempi kuin koko tekstin pituus, jokainen merkki on renderöity, ja funktio palauttaa nollan, mikä lopettaa repeat...until-silmukan

Jokainen iteraatio tuottaa yhden PDF-tiedoston, jonka nimi on Output1.pdf, Output2.pdf, ja niin edelleen. Jos haluat sen sijaan yhden monisivuisen asiakirjan, kirjaston sivunliittämis-API:n avulla voit koota ne jälkikäteen, tai voit jäsentää silmukan uudelleen kutsuaksesi AddPage-metodia yhden asiakirjaistunnon sisällä. Yllä oleva iteraatiokohtainen SaveToFile-malli, jota seuraa RemovePdfDocument, pitää huippumuistin rajattuna yhden sivun sisällön kokoiseksi, millä on merkitystä erittäin pitkien RTF-tiedostojen kohdalla

Mitoituksen yksityiskohtia, jotka saavat ihmiset kompastumaan

96 DPI:n argumentti LoadFromCanvasDc-kutsulle kertoo kirjastolle, millä näyttöresoluutiolla DC renderöitiin, jotta se voi laskea PDF-sivulle oikean pistestä pikseliksi -kartoituksen. Jos tämä menee väärin, teksti näkyy tulosteessa väärän kokoisena, vaikka kuva näyttäisi oikealta näytöllä

Kohteisiin RcPage.Right ja RcPage.Bottom lisätty +100 on pieni marginaali kontrollin näkyvän reunan ulkopuolella. Rich Edit käyttää rcPage-suorakulmiota päättääkseen, mistä sivut jaetaan; ilman marginaalia rajalle tarkalleen osuva rivi voi monistua kahdelle sivulle. Se ei ole taikavakio: haluat sen olevan riittävän suuri, jotta sivun raja osuu puhtaasti kontrollin asettelualueen sisälle eikä viimeiselle pikselille

PDF: sisäkkäiset rcPage- ja rc-suorakulmiot EM_FORMATRANGElle ylimääräisellä sadan pikselin marginaalilla, joka estää rajaviivojen kahdentumisen sivujen yli
rcPage ulottuu kontrollin piirtosuorakulmion ohi, joten sivunvaihdolle osuvan rivin jakaminen tai monistaminen ei ole mahdollista

Lopuksi kontrollin on jo oltava liitettynä näkyvään lomakeikkunaan, kun FormCreate suoritetaan, jotta sen ikkunakahva on kelvollinen ennen ensimmäistä SendMessage-kutsua. Ajonaikana dynaamisesti luotu TRichEdit vaatii eksplisiittisen HandleNeeded-kutsun ennen renderöintisilmukan alkamista, jos lomaketta ei ole vielä näytetty

Fonttien ja RTF-ominaisuuksien käsittely

Koska renderöinnin suorittaa Windowsin Rich Edit -moottori, fonttien korvaaminen noudattaa samoja sääntöjä, joita se käyttää näyttämisessä ja tulostamisessa. Koneelle asennetut fontit, joihin RTF-tiedostossa viitataan, renderöityvät uskollisesti; puuttuvat fontit korvataan hiljaisesti, mikä voi siirtää rivipituuksia ja sivutusta. Tuotantoerämuunnoksissa tämä kannattaa testata nimenomaisesti: lataa asiakirja kullakin RTF-lähteissäsi käytetyllä kirjasintyypillä ja varmista, että tulosteen sivumäärä vastaa manuaalisesta tulostuksen esikatselusta odottamaasi

Taulukot, upotetut kuvat ja useimmat Rich Text -muotoiluominaisuudet toimivat ilman minkäänlaista lisäkäsittelyä, koska Rich Edit renderöi ne natiivisti. Yksi alue, joka voi olla yllättävä, on teksti, joka käyttää twipseinä ilmaistua mukautettua kappaleväliä tai ensimmäisen rivin sisennystä: Rich Editin sisäinen koordinaattijärjestelmä on twipseinä (1/1440 tuumaa), kun taas TFormatRange-kohteeseen asettamasi DC-koordinaatit ovat pikseleinä nykyisellä DPI:llä. Kontrolli muuntaa nämä sisäisesti, mutta jos rakennat RTF:n ohjelmallisesti, sinun tulee varmistaa, että marginaaliarvosi ovat oikeassa yksikössä

DPI-tietoisuus ja korkean DPI:n näytöt

Näytöllä, joka toimii 150 %:n skaalauksella (144 DPI), ScaleX(210, mmPixel) palauttaa suuremman pikselimäärän kuin 100 %:n näytöllä. PDF Library tallentaa mitkä tahansa pikselimitat, jotka välität GetCanvasDC-kutsulle, ja käyttää LoadFromCanvasDc-kutsun DPI-argumenttia laskeakseen PDF:n fyysisen sivukoon takaperin. Niin kauan kuin välittämäsi DPI-arvo vastaa sovelluksesi käyttämää DPI:tä, tulostesivun koko on oikea näytön skaalauksesta riippumatta

Jos sovelluksesi ei ole DPI-tietoinen (vanha oletus), Windows skaalaa näytön DC:n, ja pikselilaskelmasi menevät pieleen korkean DPI:n koneilla. Yksinkertaisin korjaus on ilmoittaa DPI-tietoisuus sovelluksen manifestissa; sovellus saa tällöin todellisia laitepikseleitä, ja LoadFromCanvasDc-kutsulle välitettävä 96 tulisi korvata todellisella näytön DPI:llä, joka saadaan kutsulla GetDeviceCaps(GetDC(0), LOGPIXELSX). Yllä oleva koodiesimerkki kovakoodaa 96:n, koska se sopii 100 %:n skaalausympäristöön ja pitää esimerkin lyhyenä

Tulosteen rakenne: yksi tiedosto per sivu versus yhdistetty asiakirja

Yllä oleva silmukka kirjoittaa jokaisen sivun erilliseen PDF-tiedostoon. Se, onko tämä sitä mitä haluat, riippuu myöhemmästä käytöstä. Raporttien luomisjärjestelmät tarvitsevat usein yksittäisiä sivuja, koska ne kokoavat lopullisen asiakirjan myöhemmin yhdistämällä tai järjestämällä sivuja uudelleen. Jos haluat yhden PDF:n heti alusta alkaen, kirjasto antaa sinun luoda yhdessä istunnossa asiakirjan, jossa on useita sivuja: luo asiakirja kerran silmukan ulkopuolella, kutsu sivun lisäysmetodia SaveToFile-kutsun sijaan silmukan sisällä, ja tallenna koko asiakirja silmukan poistumisen jälkeen. Tämä välttää välitiedostot ja on oikea rakenne useimmille yhden asiakirjan muunnosskenaarioille

Suurille RTF-tiedostoille kannattaa lisätä silmukkaan jonkinlaista edistymispalautetta, koska muunnosnopeus on karkeasti verrannollinen sivumäärään, ja 200-sivuinen asiakirja voi kestää muutaman sekunnin. repeat...until-rakennetta on helppo laajentaa: seuraa merkkisiirtymää edistymispalkin päivityksessä jokaisen iteraation jälkeen käyttämällä LastChar-muuttujaa jaettuna RichEdit1.GetTextLen-kutsusta saatavalla kokonaismerkkimäärällä

Tässä esitetyt GetCanvasDC- ja LoadFromCanvasDc-metodit ovat osa losLab PDF Library -kirjastoa Delphille ja C++Builderille

PDF: valinta yhden PDF-tiedoston kirjoittamisen välillä kierrosta kohden myöhempää kokoamista varten ja yhden monisivuisen PDF:n rakentamisen välillä liittämällä sivuja silmukan sisällä
Sama renderöintisilmukka ruokkii joko erillisiä Output-tiedostoja tai yhtä kasvavaa dokumenttia vaihtaen myöhemmän joustavuuden väliaikaisiin tiedostoihin