Computing: Free Pascal Programming

Creating PDF documents using fcl-pdf.

There are several PDF generators available for Free Pascal. This tutorial is about the fcl-pdf package, that is included by default in Lazarus, thus may be used without any download or installation to be done. As a difference with other packages, it has no visual components, what means that instead of drag-and-drop of controls, and filling in their properties and methods, you'll have to write all code yourself. The advantages of the package are that you can use it in applications and command line programs, and that it includes features that lack in visual packages like PowerPDF, in particular the possibility to embed fonts (what allows to create platform independent PDF files), and the support of Unicode fonts (what allows to create PDF files in any language); also there is no dependency on external libraries or operating system calls. To add, that the fcl-pdf routines (full object oriented) are really easy to use.

The main unit of the fcl-pdf package is fppdf, that you'll have to add to your uses statement. Another unit is fpttf, that you need in order to call some fonts related methods.

Creating a PDF document consists in creating a TPDFDocument object. Here are 4 important properties of such objects:

You can also set a certain number of options for the current document. This is done using the Options property, an array with predefined values, such as poPageOriginAtTop (this flips the coordinate system by setting the origin (0, 0) to the top-left corner; default = bottom-left corner), and poNoEmbeddedFonts (this does not embed the fonts into the document; default = embedding of all fonts).

To create a PDF document, the following steps are required:

  1. Creating a document (TPDFDocument object).
  2. Setting the document properties and options.
  3. Initializing the document creation process, calling the TPDFDocument.StartDocument method.
  4. Creating a section (TPDFSection object) and adding it to the document using the TPDFDocument.Sections.AddSection method. The fcl-pdf component is page oriented. Pages are grouped in sections. A document must have at least on section (with at least one page). All pages of the document may be part of one single section.
  5. Creating a page (TPDFPage object) and add it to the document using the TPDFDocument.Pages.AddPage method.
  6. Adding the page to a section using the TPDFSection.AddPage method.
  7. Eventually setting page properties (such as overwriting the default paper type (using the TPDFPage.PaperType property), or overwriting the default unit of measure (using the TPDFPage.UnitOfMeasure property).

Here is the corresponding Free Pascal code:

    uses fppdf;
    var
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;

    begin
        Document := TPDFDocument.Create(nil);
        Document.Options := Document.Options + [poPageOriginAtTop];
        Document.StartDocument;
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Section.AddPage(Page);
    end;

The fcl-pdf coordinate system.

The coordinate system uses floating point values for coordinates. The origin (0, 0) is by default located at the bottom-left corner of the page. The coordinate values to be used with the different methods have to be expressed in the measure unit that has been set for the actual page, or the document default measure unit, if no specific measure unit has been specified for the page.

The dimensions of the page are determined by the values of its PaperType and Orientation properties, or the document properties DefaultPaperType and DefaultOrientation, if there aren't specific settings for this page. Setting the page type and orientation will set the page dimensions in the TPDFPage.Paper property, and may be retrieved by reading the values of Page.Paper.W (width) and Page.Paper.H (height) ("Page" being a TPDFPage object). The TPDFPage.Paper property is also used when creating a page with custom dimensions (cf. program sample 2). To note that these values are not expressed in the page's measure unit, but (always) in PDF points, which actually corresponds to pixels. Example: With default settings (portrait A4), the values will be 595 for the width, and 842 for the height; the number of pixels per inch being (normally) calculated for a DPI (dots per inch) of 72.

We can convert measures between two units by calling one of the available functions MMtoPDF, PDFtoMM, etc. For example to get the page dimension in cm, use PDFtoCM("Page".Paper.W) and PDFtoCM("Page".Paper.H). In the case of portrait A4 this returns 2.099027824E+01 resp. 2.970388794E+01 (ca 21×30).

Drawing lines.

Drawing commands use a certain color, which can be set using the method
    procedure SetColor(AColor: TARGBColor; AStroke: Boolean = True);

The colors used in fcl-pdf are specified using RGB notation. The simplest way to specify them is to use hexadecimal numbers. Examples: $FF0000 = red, $0000FF = blue, $FF00FF = magenta. Some popular colors are available as predefined constants:

Color nameRGB value
clBlack$000000
clWhite$FFFFFF
clBlue$0000FF
clGreen$008000
clRed$FF0000
clAqua$00FFFF
clMagenta$FF00FF
clYellow$FFFF00
clLtGray$C0C0C0
Color nameRGB value
clMaroon$800000
clOlive$808000
clDkGray$808080
clTeal$008080
clNavy$000080
clPurple$800080
clLime$00FF00
clWaterMark$F0F0F0

The Boolean "AStroke" is used to indicate if the color set is the stroking (line drawing) color (AStroke = True; default), or the fill color (AStroke = False).

Drawing commands also use a certain line style and pen width, which can be set using the method
    procedure SetPenStyle(AStyle: TPDFPenStyle; ALineWidth: TPDFFloat = 1.0);

The pen style determines how lines are drawn and can be one of the following: ppsSolid (default), ppsDash, ppsDot, ppsDashDot, and ppsDashDotDot.

To draw a line, use one of the following:
    procedure DrawLine(X1, Y1, X2, Y2, ALineWidth: TPDFFloat; const AStroke: Boolean = True);
    procedure DrawLine(APos1, APos2: TPDFCoord; ALineWidth: TPDFFloat; const AStroke: Boolean = True);

In the first declaration of the procedure, the starting and ending coordinates are indicated by a pair of x-y coordinates (floats); in the second declaration, they are indicated as a TPDFCoord value (cf. example programs).

The Boolean "AStroke" (set by default to "True" in both declarations), is used to indicate if the line should or should not actually be drawn. This allows to draw several lines "in one stroke" (cf. example program 2).

Note that color, style and width rest in effect until you change them!

Note: It is also possible to define color, style and line width as items of the TPDFDocument.LineStyles property. This allows to draw lines with given color, style and width using a single command (procedure DrawLineStyle). The color, style and width used in this command, will also be applied to subsequent DrawLine commands.

Program sample 1: Drawing a page diagonal.

This command line program draws a left-right diagonal on a A4 page, letting a margin of 1.5 cm (we suppose that A4 is 21×30 cm).

Before viewing the code, two remarks: First, as I said further up in the text, fcl-pdf is page oriented. Thus, color, line characteristics, and drawing procedures are methods of a TPDFPage object. Second (as you probably guessed) to save a PDF document as a PDF file, we use the TPDFDocument.SaveToFile method.

Here is the source of the program fppdf1:

    program fclpdf1;
    {$mode objfpc}{$H+}
    uses
        Classes, fppdf;

    var
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;
        LStart, LEnd: TPDFCoord;

    begin
        Document := TPDFDocument.Create(Nil);
        Document.Options := Document.Options + [poPageOriginAtTop];
        Document.StartDocument;
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Section.AddPage(Page);
        Page.SetColor(clMagenta);
        LStart.X := 15;  LStart.Y := 15;
        LEnd.X   := 195; LEnd.Y := 285;
        Page.DrawLine(LStart, LEnd, 2);
        Document.SaveToFile('fclpdf1.pdf');
    end.

And here is the program output (PDF file content):

Free Pascal programming: fppdf - Simple line drawing

Program sample 2: Drawing a stairway on a custom sized page.

Our stairway should have 5 stairs, each 1.5 cm long and 1 cm high. The stairway should be drawn on a page of 150×100 cm.

General guideline to create a custom sized page:

  1. Define the orientation first, then set the type to ptCustom (either at document, or at page level).
  2. Assign the custom dimensions (in pixels!) to the TPDFPage.Paper property.

Here is the source of the sample program fppdf2:

    program fclpdf2;
    {$mode objfpc}{$H+}
    uses
        Classes, fppdf;

    const
        Width = 150; Height = 105;
    var
        I: Integer;
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;
        LStart, LEnd: TPDFCoord;
        Paper: TPDFPaper;

    begin
        Document := TPDFDocument.Create(nil);
        Document.DefaultOrientation := ppoLandscape;
        Document.DefaultPaperType := ptCustom;
        Document.StartDocument;
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Paper.W := Round(MMtoPDF(Width));
        Paper.H := Round(MMtoPDF(Height));
        Page.Paper := Paper;
        Page.SetColor(clBlack);
        Section.AddPage(Page);
        for I := 0 to 4 do begin
            LStart.X := 37.5 + 15 * I; LStart.Y := 27.5 + 10 * I;
            LEnd.X := LStart.X; LEnd.Y := 27.5 + 10 * (I + 1);
            if I = 0 then
                Page.MoveTo(LStart);
            Page.DrawLine(LStart, LEnd, 2, False);
            LStart.X := 37.5 + 15 * I; LStart.Y := 27.5 + 10 * (I + 1);
            LEnd.X := 37.5 + 15 * (I + 1); LEnd.Y := 27.5 + 10 * (I + 1);
            Page.DrawLine(LStart, LEnd, 2, False);
        end;
        Page.StrokePath;
        Document.SaveToFile('fclpdf2.pdf');
    end.

The stairway is drawn from the bottom to the top, thus the origin at the bottom-left corner is here ok. The individual stairs are drawn the vertical part first. Note, that the DrawLine instructions (used with the last argument set to "False") don't actually draw anything at all. The entire stairway is drawn in one stroke; this is done calling the method TPDFPage.StrokePath.

You may wonder what the instruction Page.MoveTo(LStart), executed when drawing the first stair, is about. When the Stroke parameter of the DrawLine procedure is set to "False", the first coordinate will be ignored. This would be drawing from the current (undefined) position. Therefore, we call the MoveTo procedure in order to set the initial position (the starting point of the stairway).

Another remark concerns the usage of the variable "Paper". Its usage is in fact mandatory, because it is not allowed to individually assign the width and height to the attributes TPage.Paper.W resp. TPage.Paper.H. That's why we assign the page dimensions to the record items of our "Paper" variable, and then assign this variable to the page property TPage.Paper.

Here is the program output (PDF file content):

Free Pascal programming: fppdf - Line drawing on a custom sized page

If you want to create a multiple lines drawing, where each point is joined to the previous point, instead of using individual calls to DrawLine, you can use the method
    procedure DrawPolyLine(const APoints: array of TPDFCoord; const ALineWidth: TPDFFloat);

Important to note, that this method does not actually do the drawing, thus, you'll have to call StrokePath after having "drawn" the lines between the points in the array.

Drawing shapes.

DrawPolyLine can be used to draw shapes like squares, rectangles, triangles, trapezoids, etc. Note, however, that to create a closed shape, you'll have to specify 1 point (array element) more than the shape has corners (ex: 4 points for a triangle), the last point having the same coordinates than the first one. The fppdf unit includes a procedure that automatically draws a line from the last point to the first point, thus allows to draw closed shapes, indicating the shape corner points without the need of a supplementary point as is the case for DrawPolyLine. Here is the declaration of the procedure DrawPolygon:
    procedure DrawPolygon(const APoints: array of TPDFCoord; const ALineWidth: TPDFFloat);

As with DrawPolyLine, you'll have to call the StrokePath method to actually draw the shape.

It is possible to fill a polygon with the actual fill-color. The fppdf unit includes two procedures to this: FillStrokePath; and FillEvenOddStrokePath. The procedures use different algorithms, and may or may not produce different results. Use the one that works best for your shape...

The fppdf unit includes procedures to draw rectangles, rounded rectangles and ellipses. Here are the declarations:
    procedure DrawRect(const X, Y, W, H, ALineWidth: TPDFFloat; const AFill, AStroke : Boolean; const ADegrees: Single = 0.0);
    procedure DrawRoundedRect(const X, Y, W, H, ARadius, ALineWidth: TPDFFloat; const AFill, AStroke : Boolean; const ADegrees: Single = 0.0);
    procedure DrawEllipse(const APosX, APosY, AWidth, AHeight, ALineWidth: TPDFFloat; const AFill: Boolean = True; AStroke: Boolean = True; const ADegrees: Single = 0.0);

The rectangles are defined by the xy-coordinates of the bottom-left (!) corner, and their width and height. For the rounded rectangle, the radius of the "rounding circle" has also to be specified. In both cases, you also have to specify a line width, and two Boolean. Setting the first of these ("AFill") to "True" will fill the rectangle with the actual fill-color; the second one ("AStroke") may be set to "False" to not actually do the drawing (doing it later by calling StrokePath). Finally, you can specify a rotation angle (default = 0). Note, that the rotation will not be done around the rectangle's center, but around its origin point (its bottom-left corner). The unit used for the angle is degrees (not radians).

To draw an ellipse, you have to define its bounding rectangle (as described above); the ellipse will be drawn inside this rectangle.

Program sample 3: Drawing shapes.

The program sample fppdf3 shows how to draw polygons (as example, a triangle), simple and rounded rectangles, ellipses and circles. It also shows how to use the SetColor method to either set the stroke or the fill color. The shape dimensions are chosen in a way that on an A4 page, with top, left and right margins of 1.5 cm, we also get horizontal and vertical shape separations of ca 1.5 cm. Here is the code:

    program fclpdf3;
    {$mode objfpc}{$H+}
    uses
        Classes, fppdf;

    const
        H = 15; V = 15;
        Triangle: array[1..3] of TPDFCoord = (
            (X: H + 50 / 2; Y: V;),
            (X: H + 50; Y: V + 75;),
            (X: H; Y: V + 75;)
        );
    var
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;

    begin
        Document := TPDFDocument.Create(Nil);
        Document.Options := Document.Options + [poPageOriginAtTop];
        Document.StartDocument;
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Section.AddPage(Page);
        Page.DrawPolygon(Triangle, 1);
        Page.SetColor(clLime, False);
        Page.FillStrokePath;
        Page.SetColor(clBlue, True);
        Page.DrawRect(2 * H + 50, V + 75, 50, 75, 5, False, True);
        Page.SetColor(clAqua, False);
        Page.DrawRoundedRect(3 * H + 100, V + 75, 50, 75, 5, 2, True, True);
        Page.DrawRect(2 * H + 50, 2 * V + 75 + Sin(Pi / 4) * 75, 50, 75, 5, True, True, -45);
        Page.SetColor(clBlack, True);
        Page.SetColor($FFA500, False);
        Page.DrawEllipse(H, 3 * V + 150 + Sin(Pi / 4) * 75 + Sin(Pi / 4) * 50, 50, 75, 1, True, True);
        Page.SetColor(clRed, True);
        Page.DrawEllipse(3 * H + 100, 3 * V + 75 + 50 + Sin(Pi / 4) * 75 + Sin(Pi / 4) * 50, 50, 50, 5, False, True);
        Page.SetColor($FFA500, True);
        Page.DrawEllipse(3 * H + 100, 3 * V + 75 + 75 + Sin(Pi / 4) * 75 + Sin(Pi / 4) * 50, 50, 20, 5, False, True);
        Document.SaveToFile('fclpdf3.pdf');
    end.

Here is the program output (PDF file content):

Free Pascal programming: fppdf - Polygons and other shapes drawing

Concerning the triangle, note that FillStrokePath does the drawing (that is not done by DrawPolygon alone) and fills it with the fill color (lime). This one has been set using SetColor with the "Stroke" argument set to "False"; the stroke color (border of the triangle) has not been set by the program, so defaults to black.

If rectangles are filled or not depends on the "Fill" argument of the drawing command. Thus, the first rectangle is not filled, the second and third ones are. Note the blue border of the rounded rectangle. It is due to the fact that color settings are kept until they are changed, and we actually set the stroke color to "blue" when drawing the not filled shape.

Concerning the rotated rectangle, note the negative value of the angle. In mathematics, angles are evaluated counter-clockwise. Thus if we want rotate the rectangle 45° to the right, we have to specify an angle of -45°.

The drawing of an ellipse consists in specifying the characteristics of the bounding rectangle. Circles are ellipses with the two axes being equal (and corresponding to the diameter of the circle), thus if we set the width and height arguments of the DrawEllipse procedure to the same value, we get a circle. Coloring the border and filling or not the shape with a color works the same way as for rectangles.

Drawing the shapes is really easy, determining the horizontal and vertical position, where to start drawing may be more complicated. To understand the following explanations, note that I changed the default page origin, setting the coordinates (0, 0) to the top-left corner of the page. Also remember that when drawing a rectangle or an ellipse, the starting point (first argument of the procedure) must be the bottom-left corner of the shape. If you are not comfortable with math, just skip the following paragraphs, and go directly to the section Writing text.

I said above that the page should have a left, right and top margin of 1.5 cm. I also said that I wanted the distance separating two shapes being the same as the margins. In the program the constant "H" is used as horizontal margin (left and right), "V" as vertical margin (top, letting to the bottom margin what remains below the shape drawings). This led me to choose shape dimensions of 50×75 mm. Not considering the rotated rectangle, this gives for the width: 4*15 + 3*50 = 210 mm (A4 width), and for the height: 3*15 + 3*75 = 270, letting some 40 mm bottom margin (with an A4 height of ca 310 mm).

The bottom-left corner of the shapes in the first "column" have thus an x-coordinate of H. For those of the second "column", x = H + width of the shape + distance from the previous shape = H + 50 + H = 2*H + 50; similarly, for those in the last "column", we get x = 3*H + 100.

If all shapes were without rotation, we would (by the same reasoning as above) getting the following for y-coordinates: first "row": y = V + 75 (remember that we have to consider the bottom corners!); second "row": y = 2*V + 150; third "row": y = 3*V + 225.

Before analyzing how the rotated shape requires to recalculate the vertical positions (y-coordinates) of the shapes in the second and third "row", lets have a look at the triangle. I started the drawing at the top corner, drawing a line to the bottom-right corner and finally drawing a line to the bottom-left corner. The coordinates of the second and third points are thus (H+50, V+75) resp. (H, V+75), following the same reasoning as for the other shapes. Remains the first point (top corner). We know from geometry, that in an isoscele triangle, the point where the two equal lines intersect has an x-coordinate that is half the length of the third side; thus x = H + 50/2. Concerning y, it corresponds to the top margin: y = V.

Now, lets take a look at the rotated rectangle. When we rotate the rectangle to the right, its upper-most corner will be shifted towards the bottom of the page. This means that if we drew the rectangle at y = 2*V + 150, the distance between the first and second row would be greater than the 15 mm that we want it to be. Thus, we have to start drawing "somewhat nearer" to the top of the page. The question is, what is the value of this "somewhat"? If you look at the rotated rectangle on the screenshot above, you notice that the difference between the y-coordinate of the left corner and the one of the top corner corresponds to the length of a segment drawn vertically from the top corner to the horizontal line passing through the left corner. We actually have the case of a right angled triangle, where we know the angles, and the length of one side (the segment from the left to the top corner being the rectangle's height = 75). This allows to calculate the two other sides of the triangle, the one adjacent to the considered angle being equal to the length of the known side multiplied by the cosine of the angle, and the one opposed to the considered angle being equal to the length of the known side multiplied by the sine of the angle.

This means that the starting point of the rotated rectangle has not to be situated V + 75 mm, but V + 75 * sin(45°) mm below the first row, and we have to use the coordinate y = 2*V + 75 + 75*sin(Pi/4).

If we have a look at the rotated rectangle on the screenshot, we notice that for the third row, things become even more complicated. In fact, it's not the bottom-left, but the bottom-right corner of the rotated rectangle that has the highest y-coordinate. That means, that if we took the bottom-left corner's y-coordinate and added V + 75, the ellipses in the third "row" would be drawn to much towards the top (if there was a shape in the second "column" of the third "row", it would overlap the rotated rectangle in the second "row"). Thus, we have to start drawing "somewhat nearer" to the bottom of the page. The value of this "somewhat" may be determined following the same reasoning than above. And the supplementary value to be added to the y-coordinate has to be equal to the width of the rectangle multiplied by sin(45°).

The value of the y-coordinate of the starting point of our ellipses will thus be [2*V + 75 + 75*sin(Pi/4)] + [V + 75] + [50*sin(Pi/4)] = 3*V + 150 + 75*sin(Pi/4) + 50*sin(Pi/4).

An example, how a basic knowledge of mathematics can help to solve problems of the practical life. I agree that this tutorial is not really the place to write about this, but you can't blame me. Because there was a link to just skip my "math fantasies".

Writing text.

To write text to a PDF document, we'll have to indicate the font that we want to use. A few fonts may be emulated by the PDF standard. This means that the document can be properly viewed on a computer where this font isn't installed. Beside the built-in forms, we can use true-type fonts (TTF). Using these fonts, the document can only be viewed properly on another computer, if this font is installed there. Unless we include the font into the document.

Note: There are actually 14 fonts defined in the PDF standard: Times-Roman, Times-Bold, Time-italic, Time-Bolditalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, Symbol, and ZapfDingbats.

The fppdf unit includes the following "two-forms" function to associate a font with a document:
    function AddFont(AName: string): Integer;
    function AddFont(AFontFile: string; AName: string): Integer;

The first form is used for standard fonts; the only argument needed by the function is the name of the font. The second form requires a further parameter, that indicates the name of the font file on the local machine. In order not to have to specify the full path, we can set the path to the directory containing the fonts, using the TPDFDocument.FontDirectory property. On Windows, this path is C:\Windows\Fonts.

The function above doesn't actually set a font, but just adds it to the list of fonts available to write text to this document. To use the font in order to write to a given page, we must set it for this page. To do so, we'll need the font index; this is a numerical ID, that is returned when calling the TPDFDocument.AddFont method.

Writing to a PDF document thus requires three steps:

  1. Adding the font to the list of available fonts for the document, using the method TPDFDocument.AddFont.
  2. Setting the font (for a given page) using the method TPDFPage.SetFont.
  3. Writing the text to the page using the method TPDFText.WriteText.

The method TPDFPage.SetFont is declared as follows:
    procedure SetFont(AFontIndex: Integer; AFontSize: Integer);

The procedure has two arguments: the font index (as returned by the TPDFDocument.AddFont method, and the font width (in points) to be used when writing the text. Note that the font color that will be used is the actually set fill color (not the stroke color, as you may perhaps have expected).

Here are the declarations of the methods TPDFPage.WriteText:
    procedure WriteText(X, Y: TPDFFloat; AText: UTF8String; const ADegrees: Single = 0.0; AUnderline: Boolean = false; AStrikethrough: Boolean = false);
    procedure WriteText(APos: TPDFCoord; AText: UTF8String; const ADegrees: Single = 0.0; AUnderline: Boolean = false; AStrikethrough: Boolean = false);

The procedure has either two or three mandatory arguments: the xy-coordinates, where to write the text on the page (either as two floats, or as TPDFCoord) and the text string itself (encoding will be done in UTF-8). There are also three optional arguments: the rotation angle (default = 0), and two Boolean that may be set to true do apply the font decorations "underline", resp. "strike-through".

To note that the y-coordinate specified corresponds to the baseline of the text! This means that the text can extend above and below the vertical position specified as y.

Program sample 4: Writing text.

The program sample fppdf4 writes some text to a page. Here is the code:

    program fclpdf4;
    {$mode objfpc}{$H+}
    uses
        Classes, fppdf;

    const
        Fonts: array[0..4] of string = (
            'Arial', 'Arial Bold', 'Arial italic', 'Old English Text MT', 'Helvetica'
        );
        Files: array of string = (
            'arial.ttf', 'arialbd.ttf', 'ariali.ttf', 'OLDENGL.TTF', ''
        );
    var
        I: Integer;
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;
        FontIDs: array[0..4] of Integer;

    begin
        Document := TPDFDocument.Create(Nil);
        Document.DefaultPaperType := ptA5;
        Document.DefaultOrientation := ppoLandscape;
        Document.Options := Document.Options + [poPageOriginAtTop];
        Document.FontDirectory := 'C:\Windows\Fonts';
        Document.StartDocument;
        for I := 0 to 4 do begin
            if Files[I] = '' then
                FontIDs[I] := Document.AddFont(Fonts[I])
            else
                FontIDs[I] := Document.AddFont(Files[I], Fonts[I]);
        end;
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Section.AddPage(Page);
        Page.SetFont(FontIDs[0], 12);
        Page.WriteText(20, 20, 'This is Arial, black, 12 points.');
        Page.WriteText(20, 30, 'This is the same but underlined.', 0, True);
        Page.SetFont(FontIDs[1], 11);
        Page.SetColor(clBlue, False);
        Page.WriteText(20, 40, 'This is Arial, blue, 11 points, bold.');
        Page.SetFont(FontIDs[2], 11);
        Page.WriteText(20, 50, 'This is the same but italic instead of bold.');
        Page.SetFont(FontIDs[3], 16);
        Page.SetColor(clRed, False);
        Page.WriteText(20, 60, 'This is Old English Text MT, red, 16 points');
        Page.SetFont(FontIDs[4], 15);
        Page.SetColor(clBlack, False);
        Page.WriteText(40, 70, 'Vertical printing', -90);
        Document.SaveToFile('fclpdf4.pdf');
    end.

Here is the program output (PDF file content):

Free Pascal programming: fppdf - Writing text

The font styles (bold, italic) correspond in fact to different fonts (different font files); this is logical, as the symbols used are different. On the other hand, the font decorations (underline, strike-through) are not dependent on the font, so may be freely applied to all fonts. Why my text, that should have been underlined, actually isn't underlined, no idea. I think that my call of the method is correct. So, maybe there is a problem with my PDF viewer, or a bug in the fppdf unit (?).

There isn't any font file specified for the font called "Helvetica". The reason is that this is one of the fonts that are part of the PDF standard.

Note that "vertical printing" is not really vertical printing (with the base of the letters turned toward the bottom), but just a text rotation (with the base of the letters turned toward the left or the right).

Program sample 5: Listing the fonts available.

The program sample fppdf5 is a command line version of the Lazarus code published by forum member paweld in the fcl-pdf article of the Free Pascal Wiki. It lists all fonts available on the local computer, with font family, font name, and file name. I suggest to run the file in Command Prompt, by entering
    fppdf5 >fonts.txt
This redirects the program output to the text file fonts.txt.

Here is the code of the program:

    program fclpdf5;
    {$mode objfpc}{$H+}
    uses
        Classes, SysUtils,
        fppdf, fpttf;

    var
        Fonts: TFPFontCacheList;
        I: Integer;

    procedure WriteFont(Family0, Name0, Filename0: string);
    var
        I: Integer;
        Family, Name, Filename: string;
    begin
        Family := Family0; Name := Name0; Filename := Filename0;
        for I := Length(Family0) to 35 do
            Family += ' ';
        for I := Length(Name0) to 40 do
            Name += ' ';
        Writeln(Family, Name, ExtractFilename(Filename));
    end;

    begin
        Fonts := TFPFontCacheList.Create;
        Fonts.SearchPath.Add('C:\Windows\Fonts');
        Fonts.BuildFontCache;
        for I := 0 to Fonts.Count - 1 do
            WriteFont(Fonts.Items[I].FamilyName, Fonts.Items[I].HumanFriendlyName, Fonts.Items[I].FileName);
        Fonts.Free;
    end.

Note: To use TFPFontCacheList class, you'll have to include the unit fpttf.

Width and height of a text.

In a simple case like the text writing example above, we don't need to care about the dimensions of the text that we want to write to the PDF page. If we create a "real" PDF document, however, this is not true anymore. The most obvious problem that occurs when writing a text of several lines is the question where on the page a given line has to be written, i.e. what the y-coordinate to be used in the WriteText procedure has to be. This can only properly done if we know the exact value of the text height. Examples where the knowledge of the text width has to be known, are writing out a text that must not exceed a certain horizontal position (when writing into several columns, for example), text alignment, drawing of a border around a given text.

To be able to calculate a text's dimensions, the fppdf unit includes a font manager. We can use this font manager to read a font file and look up the sizes of the various characters in the font (these are called the metrics of the font). Using the metrics, the font manager will be able to calculate the height and width of a given text.

Note: The font manager and the font metrics are also needed when embedding a font into a PDF document.

Determining the width and height of a given text requires the following steps:

  1. Initializing the font manager using the TFPFontCacheList.BuildFontCache method. Before this can be done, we have to create a TFPFontCacheList object, and letting it know the path to the directory, containing the font files; this is done by calling the method TFPFontCacheList.SearchPath.Add.
  2. Finding the font that we want to use in the font cache. This is done by calling the method TFPFontCacheList.Find, that returns a TFPFontCacheItem object.
  3. Retrieving the width and height of a given text with a given size (in points) by calling the TFPFontCacheItem.TextWidth and TFPFontCacheItem.TextHeight methods. These return the dimensions of the text in pixels, according to the DPI in the cache.
  4. Using the value of the DPI in the cache (that we can get by reading the TFPFontCacheList.DPI property) in order to convert the pixels to inches, or mm.

I agree, this is rather complex... Lets review the different steps, one by one.

Suppose, that we want to use the 'arial' font with a size of 12 points. Lets use the following declarations:
    var
        Fonts: TFPFontCacheList;
        Font: TFPFontCacheItem;

The code for the initialization of the font manager is nothing more than the first three lines of the previous program sample...

One of the declarations to find a given font in the cache is as follows:
    function Find(const AFamilyName: string; ABold: boolean; AItalic: boolean): TFPFontCacheItem;

In our example, the instruction to set the TFPFontCacheItem to "arial" would be:
    Font := Fonts.Find('arial', False, False);
The two Boolean are set to "False", as we don't want neither the bold, nor the italic form of the font.

The declarations of the functions to find the width and height of a text (a given string) are as follows:
    function TextWidth(const AStr: UTF8String; const APointSize: Single): Single;
    function TextHeight(const AText: UTF8String; const APointSize: Single; out ADescender: Single): Single;

In our example (with S being the text that we want to display, and W, H this text's dimensions, that have to determine), the instructions to get the width and height of our text are:
    W := Font.TextWidth(S, 12);
    H := Font.TextHeight(S, 12, D);

What's about the third argument of the TextHeight function (the variable D in our instruction)? It is declared as out, what means that it is a new value generated within the function. In fact, this is a rather bizarre implementation of a function with two return values (the function result and "ADescender"). But, what does this value stand for? The text height returned by the function is actually the height of the text above the baseline, and the descender value is the amount of pixels that the text extends below the baseline. This means that
    total text height = text height + descender.
In our example, this is: H + D.

Important to note that the width and height values returned are the number of pixels for the DPI of the cache.

Remains to transform the pixels in inches, or mm. As the DPI setting is nothing else than the number of dots (pixels) per inch, the values in inch may be calculated as follows:
    value in inch = value in pixels / DPI.
To get the value in mm, we have to multiply by 25.4; thus:
    value in mm = (value in pixels * 25.4) / DPI.
In our example, for the text width:
    W := (W * 25.4) / Fonts.DPI.

Program sample 6: Text width and height.

The program fppdf6 is an application of what we learned in the paragraphs above. A text with a border (a rectangle drawn around) should be placed at the center of a landscape A5 page. Here is the code:

    program fclpdf6;
    {$mode objfpc}{$H+}
    uses
        Classes, fppdf, fpttf;

    const
        MyText   = 'HELLO WORLD FROM FREE PASCAL!';
        MyFDir   = 'C:\Windows\Fonts';
        MyFont   = 'Castellar';
        MyFFile  = 'CASTELAR.TTF';
        MyFSize  = 20;
        MyFColor = clBlue;
    var
        Document: TPDFDocument;
        Section: TPDFSection;
        Page: TPDFPage;
        Fonts: TFPFontCacheList;
        Font: TFPFontCacheItem;
        FontID: Integer;
        CX, CY, W, H, DH, X, Y: TPDFFloat;
        RX, RY, RW, RH: TPDFFloat;

    begin
        // Build the font cache
        Fonts := TFPFontCacheList.Create;
        Fonts.SearchPath.Add(MyFDir);
        Fonts.BuildFontCache;
        // Create the document
        Document := TPDFDocument.Create(Nil);
        Document.DefaultPaperType := ptA5;
        Document.DefaultOrientation := ppoLandscape;
        Document.Options := Document.Options + [poPageOriginAtTop];
        Document.StartDocument;
        // Register the font to be used
        Document.FontDirectory := MyFDir;
        FontID := Document.AddFont(MyFFile, MyFont);
        // Create the page
        Section := Document.Sections.AddSection;
        Page := Document.Pages.AddPage;
        Section.AddPage(Page);
        // Calculate the text hight and width
        Font := Fonts.Find(MyFont, False, False);
        W  := Font.TextWidth(MyText, MyFSize);
        H  := Font.TextHeight(MyText, MyFSize, DH);
        W  := (W * 25.4) / Fonts.DPI;
        H  := (H * 25.4) / Fonts.DPI;
        DH := (DH * 25.4) / Fonts.DPI;
        // Calculate text position
        CX := PDFToMM(Page.Paper.W) / 2;
        CY := PDFToMM(Page.Paper.H) / 2;
        X  := CX - W / 2;
        Y  := CY + (H + DH) / 2;
        // Write the text to the center of the page
        Page.SetFont(FontID, MyFSize);
        Page.SetColor(MyFColor, False);
        Page.WriteText(X, Y, MyText);
        // Calculate border position and dimensions
        RX := X - 4;
        RY := Y + 4;
        RW := 2 * 4 + W;
        RH := 2 * 4 + (H + DH);
        // Draw the border (as rectangle)
        Page.SetColor(MyFColor, True);
        Page.DrawRect(RX, RY, RW, RH, 3, False, True);
        // Save document to file
        Document.SaveToFile('fclpdf6.pdf');
    end.

Here is the program output (PDF file content):

Free Pascal programming: fppdf - Text width and height calculation example

Drawing images.

The fppdf unit includes three functions to make an image available to a PDF document:
    function AddJPEGStream(Const AStream: TStream; Width, Height: Integer): Integer;
    function AddFromStream(Const AStream: TStream; Handler: TFPCustomImageReaderClass; KeepImage: Boolean = False): Integer;
    function AddFromFile(Const AFileName: String; KeepImage: Boolean = False): Integer;

The three functions do the same: They add an image to the image list associated with a document, i.e. they add the image to the document's Images property (what means that these functions are methods of TPDFDocument.Images). The functions return an image index, that we'll need in order to refer to a given image. A picture of any image format supported by Free Pascal can be added to the document, all that is necessary to convert it, is done by the fppdf unit.

So, to insert an image into a PDF document, two steps are required:

  1. Adding the image to the document's image list (to its Images property), using, for example, the AddFromFile function. This function returns a numerical ID, identifying this image.
  2. Drawing the image on a page, using one of the procedures DrawImageRawSize and DrawImage. The ID of the image to actually be drawn is one of the arguments passed to these procedures.

Here are the declarations of the two image drawing procedures:
    procedure DrawImageRawSize(const X, Y: TPDFFloat; const APixelWidth, APixelHeight, AImageIndex: Integer; const ADegrees: Single = 0.0);
    procedure DrawImage(const X, Y: TPDFFloat; const AWidth, AHeight: TPDFFloat; const AImageIndex: Integer; const ADegrees: Single = 0.0);

The difference between the two procedures is that the first one will draw the image using their dimensions in pixels, the second one using the dimensions in the measure unit used on the current page. As with all commands, the position (xy-coordinates) to specify are those of the picture's bottom-left corner. An optional argument may be used to rotate the image.

The properties of a given image can be accessed by accessing the ith element of the Images array (where i is the numerical image ID). For example, to read the image's width and height:
    W := Document.Images[ID].Width;
    H := Document.Images[ID].Height;
where "Document" is a TPDFDocument object and "ID" is the image index of this image; the picture's dimensions read this way are always in pixels!

Program sample 7: Drawing an image.

The sample program FclPDF7 is a Lazarus GUI application that draws the image loaded from a file browsed to by the user onto an A4 page. The user can choose the page orientation, or letting the application choose it (depending on the original picture orientation). They can also choose the output size of the image, by selecting an item from a combobox. "Original" draws the image with its original dimensions, "Resize" draws it, resizing both width and height by a resize factor (in %) entered by the user, "Custom" draws the image with a width and height entered by the user. In all three cases, the image is only drawn if it fits into the drawing area (page as actually oriented minus a 1 cm margin). Output size = "Page" will draw the image with a maximum width (for landscape page orientation), resp. maximum height (for portrait page orientation), the image being automatically resized in order to fit into the drawing area. Output size = "Half page" is similar, but only half of the page is used for output (the other half being left blank). If the page is landscape oriented, the image will be drawn on the left half of the page; if the page is portrait oriented, the image is drawn on the upper half of the page. For all 5 output formats, the picture will be horizontally and vertically centered (vertically centered on the upper half page with the "Half page" format on a portrait oriented page).

The screenshot shows the form layout in Lazarus:

Free Pascal programming: fppdf - Image drawing GUI example - form layout

The code of the main unit is some 280 lines; that's too much to publish the entire source code here. Below, just the "image drawing" part (method called when the "Create" button is pushed) (that's still a lot...). If you want more, click the following link to download the sources of all program samples of the tutorial (in fact the download ZIP includes the 7 programs as Lazarus projects, so you can easily build and change them as you like).

    procedure TfPDF7.btCreateClick(Sender: TObject);
    var
        ImgID, ImgWidth, ImgHeight, TotalWidth, TotalHeight, MaxWidth, MaxHeight: Integer;
        DWidth, DHeight, W1, W2, H1, H2: Integer;
        X, Y: TPDFFloat;
    begin
        sMess := '';
        // Check user input
        if edImage.Text = '' then
            sMess := 'Image file name is missing!'
        else if edPDF.Text = '' then
            sMess := 'PDF file name is missing!'
        else if (cobSize.ItemIndex = 4) and ((edWidth.Text = '') or (edHeight.Text = '')) then
            sMess := 'Image dimensions are incomplete/missing!'
        else if (cobSize.ItemIndex = 4) and ((StrToInt(edWidth.Text) <= 0) or (StrToInt(edHeight.Text) <= 0)) then
            sMess := 'Image dimensions are invalid!'
        else if not FileExists(edImage.Text) then
            sMess := 'Image file not found!'
        // User input is ok: Determine parameters for image creation
        else begin
            // Create the document
            Document := TPDFDocument.Create(Nil);
            if rbLandscape.Checked then
                Document.DefaultOrientation := ppoLandscape
            else
                Document.DefaultOrientation := ppoPortrait;
            Document.DefaultUnitofMeasure := uomPixels;
            Document.StartDocument;
            // Add image ot document's image list
            ImgID := Document.Images.AddFromFile(sImage, False);
            // Create the page
            Section := Document.Sections.AddSection;
            Page := Document.Pages.AddPage;
            Section.AddPage(Page);
            // Get original image width and height
            ImgWidth := Document.Images[ImgID].Width;
            ImgHeight := Document.Images[ImgID].Height;
            // Automatic page orientation: Adapt orientation if necessary
            if ImgWidth > ImgHeight then begin
                if rbAutomatic.Checked then
                    Page.Orientation := ppoLandscape;
            end;
            // Total width and height
            TotalWidth := Page.Paper.W;
            TotalHeight := Page.Paper.H;
            if cobSize.ItemIndex = 1 then begin
                // Only one half of the page will be used
                if Page.Orientation = ppoPortrait then
                    TotalHeight := TotalHeight div 2
                else
                    TotalWidth := TotalWidth div 2;
            end;
            // Calculate maximum width and height (-> drawing area = page area - margins)
            MaxWidth := Round(TotalWidth - 2 * MMtoPDF(Margin));
            MaxHeight := Round(TotalHeight - 2 * MMtoPDF(Margin));
            // Calculate drawn image width and height
            if cobSize.ItemIndex in [2..4] then begin
                // Size options "Original", "Resize", "Custom"
                if cobSize.ItemIndex = 2 then begin
                    // Size option "Original": Drawn image dimensions = original image dimensions
                    edWidth.Text := IntToStr(ImgWidth);
                    edHeight.Text := IntToStr(ImgHeight);
                end
                else if cobSize.ItemIndex = 3 then begin
                    // Size option "Resize": Drawn image dimensions = original image dimensions x user specified resize factor
                    edWidth.Text := IntToStr(Round(ImgWidth * rResize / 100));
                    edHeight.Text := IntToStr(Round(ImgHeight * rResize / 100));
                end;
                // Drawn image width and height read from form input fields
                DWidth := StrToInt(edWidth.Text);
                DHeight := StrToInt(edHeight.Text);
                // Draw image only if, with specified dimensions, it fits into the page drawing area
                if (DWidth > MaxWidth) or (DHeight > MaxHeight) then begin
                    if cobSize.ItemIndex = 2 then
                        sMess := 'Original image does not fit on page!'
                    else
                        sMess := 'Custom size image does not fit on page!';
                end;
            end
            else begin
                // Size options "Page", "Half page"
                W1 := MaxWidth; H1 := Round(ImgHeight * (MaxWidth / ImgWidth));
                H2 := MaxHeight; W2 := Round(ImgWidth * (MaxHeight / ImgHeight));
                if (H1 <= MaxHeight) and (W2 <= MaxWidth) then begin
                    // If maximum width and maximum height formats fit both into the drawing area,
                    // choose the one that fills it the most (greatest surface); if the surfaces
                    // are equal, too, make choice depending on page orientation
                    if W1 * H1 > W2 * H2 then begin
                        DWidth := W1; DHeight := H1;
                    end
                    else if W1 * H1 < W2 * H2 then begin
                        DWidth := W2; DHeight := H2;
                    end
                    else begin
                        if Page.Orientation = ppoPortrait then begin
                            DWidth := W2; DHeight := H2;
                        end
                        else begin
                            DWidth := W1; DHeight := H1;
                        end;
                    end;
                end
                else if H1 <= MaxHeight then begin
                    // If only the maximum width format fits into the drawing area, choose this format
                    DWidth := W1; DHeight := H1;
                end
                else begin
                    // If only the maximum height format fits into the drawing area, choose this format
                    DWidth := W2; DHeight := H2;
                end;
                // Fill width and height values into form text fields
                edWidth.Text := IntToStr(DWidth);
                edHeight.Text := IntToStr(DHeight);
            end;
            if sMess = '' then begin
                // Calculate image position (= bottom-left corner position) on the page
                X := TotalWidth div 2 - DWidth div 2;
                if (cobSize.ItemIndex = 1) and (Page.Orientation = ppoPortrait) then
                    // Y-coordinate special case if outout size = half page
                    Y := 3 * TotalHeight div 2 - DHeight div 2
                else
                    // Y-coordinate normal case
                    Y := TotalHeight div 2 - DHeight div 2;
                // Draw the image
                Page.DrawImage(X, Y, DWidth, DHeight, ImgID);
                // Save the image to PDF file
                Document.SaveToFile(sDir + '\' + sPDF);
            end;
            // Free the document
            Document.Free;
        end;
        // If there was an error, display a message
        if sMess <> '' then
            MessageDlg('FclPDF7', sMess, mtError, [mbOK], 0);
    end;

The screenshots below show the PDF created for a landscape oriented image drawn with automatic paper orientation and output size = full page (screenshot on the left) and for a portrait oriented image drawn with paper orientation = landscape and output size = full page (screenshot on the right).

Free Pascal programming: fppdf - Image drawing GUI example - PDF output [1]
Free Pascal programming: fppdf - Image drawing GUI example - PDF output [2]

The next two screenshots show the PDF created for a landscape oriented image drawn with automatic paper orientation and output size = 20% of the original image (screenshot on the left) and for the same image drawn with automatic paper orientation and output size = 700×350 pixels (screenshot on the right).

Free Pascal programming: fppdf - Image drawing GUI example - PDF output [3]
Free Pascal programming: fppdf - Image drawing GUI example - PDF output [4]

The last two screenshots show the PDF created for a portrait oriented image drawn with automatic paper orientation and output size = half page (screenshot on the left) and for a landscape oriented image drawn with portrait paper orientation and output size = half page (screenshot on the right).

Free Pascal programming: fppdf - Image drawing GUI example - PDF output [5]
Free Pascal programming: fppdf - Image drawing GUI example - PDF output [6]

Maybe, you wonder what this kind of castle is and why I used photos of it. That (my home is my) castle was ... my squat for some three years!


If you find this text helpful, please, support me and this website by signing my guestbook.