A C++Builder form holds a TRichEdit with pasted pictures, a TMetafile chart several screens tall and a TImage logo, and all three have to end up in one PDF. With the HotPDF component, THotPDF::ImportRichEdit paginates the rich text, THPDFPage::ShowMetafile spreads a tall metafile across pages, and AddImage plus ShowImage place the picture at a DPI you choose
Does HotPDF export a TRichEdit to PDF directly?
Yes. THotPDF::ImportRichEdit, available since HotPDF 2.642.0, takes a VCL TRichEdit, paginates its content and adds as many PDF pages as the text needs, and THotPDF::ImportRTF does the same for RTF held in a TStream or a TBytes buffer. Under the hood it is the classic Windows technique: HotPDF sends EM_FORMATRANGE to the control once per page, with an enhanced metafile canvas as the drawing surface and the screen as the target device, and hands each page metafile to its own EMF interpreter through ShowMetafileEx. That interpreter replays GDI text and paths as PDF text and path operators, so the result stays vector: body text is selectable and searchable, and it scales without the blur of a screenshot. The record-by-record details are in how HotPDF turns EMF records into vector PDF operators. Line breaks come from the page width minus the margins you set, so the PDF reflows to the page rather than copying how the control happens to wrap on screen
THPDFRTFImportOptions::Default() returns A4 portrait (595 × 842 points) with 54-point margins, a 16 MiB input cap, 4,000,000 characters, 1000 pages and AllowOLE switched off; override only the fields you need. Call it after BeginDoc, from the main VCL thread. The import drives a window control, so an export started inside a TThread gets risWrongThread and has to go through Synchronize. UseCurrentPage, true by default, renders the first page of text onto the page BeginDoc created, which suits a document that is only the rich text; set it to false when you have already drawn a cover page and the text should start on a new one. When the call returns false, THPDFRTFImportStatistics::Status names the condition or limit that stopped it, and PagesRendered and LastCharacter show how far it got
#include <vcl.h>
#include <richedit.h>
#include <memory>
#include "HPDFDoc.hpp"
// Export RichEdit1 to FileName; returns false when HotPDF stops the import
bool __fastcall TMainForm::ExportRichText(const String &FileName)
{
std::unique_ptr<THotPDF> Pdf(new THotPDF(NULL));
Pdf->AutoLaunch = false;
Pdf->FileName = FileName;
Pdf->BeginDoc();
THPDFRTFImportOptions Opt = THPDFRTFImportOptions::Default();
Opt.PageWidth = 612; // US Letter in points; default is A4
Opt.PageHeight = 792;
Opt.MarginLeft = Opt.MarginRight = 72;
Opt.MarginTop = Opt.MarginBottom = 54;
THPDFRTFImportStatistics Stats;
bool Ok = Pdf->ImportRichEdit(RichEdit1, Opt, Stats);
if (!Ok && Stats.ContainsOLE)
{
// Pasted pictures are OLE objects, and this text comes from our own form
Opt.AllowOLE = true;
Ok = Pdf->ImportRichEdit(RichEdit1, Opt, Stats);
}
if (!Ok)
{
StatusBar1->SimpleText = "RTF import stopped, status " +
IntToStr(static_cast<int>(Stats.Status)) + " after " +
IntToStr(Stats.PagesRendered) + " page(s)";
return false; // Pdf is freed, no file is written
}
Pdf->EndDoc(); // writes FileName
return true;
}
// Any control built on the Windows RichEdit window class: stream its RTF out,
// then pass the stream to Pdf->ImportRTF(Rtf, Opt, Stats)
static DWORD CALLBACK RtfOut(DWORD_PTR Cookie, LPBYTE Buf, LONG Count, LONG *Done)
{
*Done = reinterpret_cast<TMemoryStream*>(Cookie)->Write(Buf, Count);
return 0;
}
void RichEditWindowToRtf(HWND Wnd, TMemoryStream *Rtf)
{
EDITSTREAM Es = {};
Es.dwCookie = reinterpret_cast<DWORD_PTR>(Rtf);
Es.pfnCallback = RtfOut;
::SendMessage(Wnd, EM_STREAMOUT, SF_RTF, reinterpret_cast<LPARAM>(&Es));
Rtf->Position = 0; // ImportRTF reads from here: {\rtf...
}
Why are pictures missing from a RichEdit or TLMDRichEdit export?
Rich text stores a picture in one of two ways. A \pict group holds the image data directly and is drawn with the rest of the page. An \object group wraps an embedded OLE object plus a presentation picture for display, and that is often what an editor produces when a user pastes or drags an image in from another application. ImportRichEdit and ImportRTF scan the RTF for the \object control word and, with AllowOLE at its default of false, decline the whole document before drawing anything: the call returns false, Status is risInvalidInput and ContainsOLE is true. With AllowOLE set, HotPDF renders the presentation picture of supported OLE objects as part of the page, which is why the code above retries once. Keep the switch off for RTF files that arrive from outside, such as attachments and uploads. Turn it on for text your own users typed and pasted into your own form
Third-party rich edit controls such as TLMDRichEdit come in two shapes as far as HotPDF is concerned. If the class derives from Vcl::Comctrls::TRichEdit, the compiler accepts it as the first argument of ImportRichEdit and nothing else changes. If it does not, get the content out as RTF and pass the stream to ImportRTF: either through the control's own save-to-stream method, after checking that it writes RTF and not plain text (input that does not begin with {\rtf is rejected), or, when the control wraps a Windows RichEdit window, with EM_STREAMOUT and SF_RTF as in RichEditWindowToRtf above. ImportRTF loads the bytes into a TRichEdit of its own and runs the same pagination. The OLE rule applies unchanged: when a control's RTF wraps pasted pictures in \object, they are declined on both routes for the same reason. Check the fonts too: the RTF names its fonts, and a face installed on the editing machine but absent on the machine that runs the export is substituted by GDI while the page is laid out, before HotPDF sees a single glyph
Spreading one tall TMetafile over several PDF pages
A metafile taller than a page needs three decisions: how wide it should be on paper, where the page breaks fall, and how each page shows only its own band. HotPDF reads metafile coordinates as GDI recorded them and turns device pixels into points as pixels × 72 / screen DPI, so an 800 × 4000 pixel metafile recorded on a 96 DPI screen is 600 × 3000 points at natural size. On A4 with 36-point margins the printable box is 523 × 770 points, so ExportTallMetafile below scales by 523 / 600 = 0.872, which leaves 2615 points of height. Four pages. Coordinates follow HotPDF drawing units and the top-left page origin: (0, 0) is the top-left corner and Y grows downward, so moving to the next band means shifting the drawing up by one box height. THPDFPage::ShowMetafile(MetaFile, X, Y, HorScale, VertScale), the overload C++Builder sees, puts the metafile's top-left corner at (X, Y) and scales around that point, while Rectangle, Clip and NewPath inside a GStateSave / GStateRestore pair limit each page to its own band
One option matters here more than anywhere else. THotPDF::EmfOptions->Redraw is true by default, and with it a metafile wider or taller than the screen (the HORZRES × VERTRES of the reference device) is first replayed at reduced size so it fits that screen, and the interpreter reads the smaller copy. For a single diagram on a single page that is a sensible guard. For a 4000-pixel strip it would tie the scale of the printout to the monitor of whichever machine runs the export, which a paginated report should never depend on. Turn it off. With Redraw set to false before the loop, the metafile is read at its recorded size, and the arithmetic above holds on a laptop and on a 4K workstation alike
#include <algorithm>
#include <cmath>
#include "HPDFDoc.hpp"
// HotPDF converts metafile pixels with the resolution of the screen DC
int ScreenDpi()
{
HDC Dc = ::GetDC(0);
const int Dpi = ::GetDeviceCaps(Dc, LOGPIXELSY);
::ReleaseDC(0, Dc);
return Dpi;
}
// Spread a metafile taller than one page over as many A4 pages as it needs.
// Call after BeginDoc, with the current page still empty.
void ExportTallMetafile(THotPDF *Pdf, Vcl::Graphics::TMetafile *Source)
{
const float PageW = 595, PageH = 842, Margin = 36; // points
const float BoxW = PageW - 2 * Margin; // 523
const float BoxH = PageH - 2 * Margin; // 770
const float NatW = Source->Width * 72.0f / ScreenDpi();
const float NatH = Source->Height * 72.0f / ScreenDpi();
const float Scale = NatW > BoxW ? BoxW / NatW : 1.0f; // fit the width, never enlarge
const int Pages = std::max(1, static_cast<int>(std::ceil(NatH * Scale / BoxH)));
Pdf->EmfOptions->Redraw = false; // document-wide: read the recorded size as is
for (int I = 0; I < Pages; ++I)
{
if (I > 0)
Pdf->AddPage();
THPDFPage *Page = Pdf->CurrentPage;
Page->Width = PageW;
Page->Height = PageH;
Page->GStateSave();
Page->Rectangle(Margin, Margin, BoxW, BoxH); // top-left origin, Y grows down
Page->Clip();
Page->NewPath();
// Band I starts I * BoxH below the top of the scaled drawing
Page->ShowMetafile(Source, Margin, Margin - I * BoxH, Scale, Scale);
Page->GStateRestore();
}
}
Clipping keeps the code short, yet every page still carries the complete metafile in its content stream, because a clip hides drawing without removing it; a 40-page strip writes its vector operators 40 times, and the file grows with pages × metafile size. Band boundaries fall wherever the arithmetic puts them. A text line that straddles one is cut in half, top at the foot of one page and bottom at the head of the next. If the metafile is a table drawn at a fixed row pitch, say 18 points after scaling, make the band a whole number of rows: floor(770 / 18) × 18 = 756 points. No row is split. For long documents, have the producer draw page-sized metafiles in the first place, the way ImportRichEdit asks the RichEdit for one page at a time, so the breaks follow the content and each page carries only its own drawing
Placing a TImage: pixels, DPI and points
A TImage holds a TPicture, and what HotPDF does with it depends on the TGraphic class behind Picture->Graphic. THotPDF::AddImage accepts a TBitmap or a TJPEGImage and raises "Unsupported image format." for anything else, so a TPngImage, TGIFImage or TWICImage has to be drawn onto a bitmap first; drawing it onto white also decides what its transparent areas become. When real transparency matters, AddImageWithTransparency takes separate RGB and alpha byte arrays instead. A TJPEGImage goes in with icJpeg and is stored as DCTDecode; a TBitmap goes in with icFlate, which is lossless; and a TMetafile shown in a TImage is a vector drawing that belongs with ShowMetafile. AddImage copies the pixel data when you call it, so the temporary bitmap can be freed at once, and the returned index can be shown on as many pages as you like
#include <memory>
#include <Vcl.Imaging.jpeg.hpp>
#include "HPDFDoc.hpp"
// Put the picture of a TImage on the current page, top-left corner at (X, Y).
// Dpi is what the pixels were made for: 96 for a screen capture, 300 for a scan.
// Returns the height used, so the caller can continue below it.
float PlaceImage(THotPDF *Pdf, TImage *Img, float X, float Y, int Dpi)
{
TGraphic *G = Img->Picture->Graphic;
if (G == NULL || G->Empty)
return 0;
if (Vcl::Graphics::TMetafile *Meta = dynamic_cast<Vcl::Graphics::TMetafile*>(G))
{
Pdf->CurrentPage->ShowMetafile(Meta, X, Y, 1, 1); // stays vector
return Meta->Height * 72.0f / ScreenDpi();
}
int Index;
if (dynamic_cast<TJPEGImage*>(G))
Index = Pdf->AddImage(G, icJpeg); // stays DCT-compressed
else if (dynamic_cast<Vcl::Graphics::TBitmap*>(G))
Index = Pdf->AddImage(G, icFlate); // lossless
else
{
// PNG, GIF, WIC: flatten onto white, which also settles transparent areas
std::unique_ptr<Vcl::Graphics::TBitmap> Bmp(new Vcl::Graphics::TBitmap());
Bmp->PixelFormat = pf24bit;
Bmp->SetSize(G->Width, G->Height);
Bmp->Canvas->Brush->Color = clWhite;
Bmp->Canvas->FillRect(Rect(0, 0, G->Width, G->Height));
Bmp->Canvas->Draw(0, 0, G);
Index = Pdf->AddImage(Bmp.get(), icFlate); // pixels are copied here
}
const float W = G->Width * 72.0f / Dpi; // same factor on both axes
const float H = G->Height * 72.0f / Dpi;
Pdf->CurrentPage->ShowImage(Index, X, Y, W, H, 0);
return H;
}
ShowImage takes X, Y, width and height in page units, points here, and the pixel count of the picture plays no part until you decide how pixels become points: points = pixels × 72 / DPI. A 1200 × 900 photo meant for 300 DPI print lands at 288 × 216 points, four by three inches. The same photo at 96 DPI, the size it has on a standard-DPI screen, becomes 900 × 675 points and runs off a 595-point-wide A4 page. That is the familiar picture three times too large. Screen captures and form snapshots want the screen DPI; scans and camera images want the DPI they were made for. Compute both sizes from the same factor, as PlaceImage does: THotPDF::KeepImageAspectRatio is true by default, and when the box you pass has different proportions from the picture, HotPDF changes one of the two sizes to restore them, so a box worked out separately on each axis may not come out at the size you typed
Why does EndDoc fail when the PDF is open in a viewer?
The export worked a minute ago, and now it fails inside EndDoc. THotPDF::EndDoc writes the file at the very end: it serializes the document and opens FileName with a TFileStream in fmCreate mode. If a viewer holds that file open without write sharing, Windows refuses with a sharing violation, and EndDoc raises EFCreateError carrying the system text "The process cannot access the file because it is being used by another process". THotPDF::AutoLaunch is on by default so a desktop export opens in the default viewer right after the save, through ShellExecute. Behind an Export button that users click again and again, the viewer it opened is the program holding the file
Viewers differ here. Acrobat and Acrobat Reader keep the open document locked, while some lighter viewers read the file into memory and release it, so a test that passes on a developer machine can fail on a customer's. Do not try to force the write. Closing another process's handle or killing the viewer can throw away work the user has open there, such as unsaved comments or a half-filled form. Detect the lock and let the user decide:
- Set
AutoLaunchto false where the export can run repeatedly, and open the file yourself after a write that succeeded - Build the PDF in memory by assigning a
TMemoryStreamtoTHotPDF::OutputStreambeforeBeginDoc;EndDocthen writes into the stream instead ofFileName, which is how the MemStream sample in the HotPDF C++Builder demos works - Probe the target with
CreateFileW,GENERIC_WRITEand a share mode of 0, whereERROR_SHARING_VIOLATIONmeans another process has the file open - On a lock, ask the user to close the document and retry, or offer another name such as
report (2).pdf - To name the program that holds the file, the Windows Restart Manager (
RmStartSession,RmRegisterResourcesandRmGetList, declared inRestartManager.hand linked fromRstrtmgr.lib) returns the application name, which turns a vague "file in use" into a message that says which window to close
#include <windows.h>
#include <memory>
#include "HPDFDoc.hpp"
enum class TargetState { Writable, Locked, Failed };
// Open the existing file for writing with no sharing allowed, then let it go
TargetState ProbeTarget(const String &FileName)
{
HANDLE H = ::CreateFileW(FileName.c_str(), GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (H != INVALID_HANDLE_VALUE)
{
::CloseHandle(H);
return TargetState::Writable;
}
const DWORD Err = ::GetLastError();
if (Err == ERROR_FILE_NOT_FOUND)
return TargetState::Writable; // nothing there yet
if (Err == ERROR_SHARING_VIOLATION || Err == ERROR_LOCK_VIOLATION)
return TargetState::Locked; // open in a viewer or another tool
return TargetState::Failed; // read-only, no access, bad path
}
TargetState WritePdfBytes(TMemoryStream *Bytes, const String &FileName)
{
const TargetState State = ProbeTarget(FileName);
if (State != TargetState::Writable)
return State;
try
{
std::unique_ptr<TFileStream> Out(new TFileStream(FileName, fmCreate));
Bytes->Position = 0;
Out->CopyFrom(Bytes, Bytes->Size);
return TargetState::Writable;
}
catch (const EFCreateError &)
{
return TargetState::Locked; // opened between probe and write
}
}
void __fastcall TMainForm::ExportClick(TObject *Sender)
{
std::unique_ptr<TMemoryStream> Bytes(new TMemoryStream());
{
std::unique_ptr<THotPDF> Pdf(new THotPDF(NULL));
Pdf->AutoLaunch = false;
Pdf->OutputStream = Bytes.get(); // set before BeginDoc
Pdf->BeginDoc();
// ... ImportRichEdit, ExportTallMetafile, PlaceImage ...
Pdf->EndDoc(); // writes into Bytes
Pdf->OutputStream = NULL;
}
const String Target = SaveDialog1->FileName;
TargetState State;
while ((State = WritePdfBytes(Bytes.get(), Target)) == TargetState::Locked)
{
if (MessageDlg(ExtractFileName(Target) + " is open in another program. "
"Close it there, then click Retry.", mtWarning,
TMsgDlgButtons() << mbRetry << mbCancel, 0) != mrRetry)
return; // the PDF stays in memory until here
}
if (State == TargetState::Failed)
ShowMessage("Cannot write " + Target);
else
::ShellExecuteW(Handle, L"open", Target.c_str(), NULL, NULL, SW_SHOWNORMAL);
}
The probe and the write are two separate steps, so a viewer can still grab the file in between; that is why the write sits in a try block that treats EFCreateError as one more locked answer. Building in memory also makes a failed write cheap, since the finished PDF is still in the TMemoryStream and a retry only copies bytes. Create the THotPDF object per export and free it afterwards, as in creating and freeing THotPDF at runtime in C++Builder, so a failure in one export never leaves a half-built document behind for the next. Start from the MemStream sample in the HotPDF C++Builder demos (Demo\CBuilder\Miscellaneous\MemStream): it already builds into a TMemoryStream, so adding the ImportRichEdit call from ExportRichText and the WritePdfBytes loop gives you an Export button you can test with the previous PDF still open in Acrobat Reader. Supported Delphi and C++Builder versions are listed on the HotPDF Delphi and C++Builder PDF component page