Graphics System in PDF

From
Revision as of 00:50, 28 June 2018 by Adminko (talk | contribs)
Jump to: navigation, search

Overview

Graphics system in PDF is built around a few concepts:

  • Coordinate space, which defines the canvas on which all painting occurs. It determines the position, orientation, and scale of the text, graphics, and images that appear on a page. Paths and positions shall be defined in terms of pairs of coordinates on the Cartesian plane. A coordinate pair is a pair of real numbers x and y that locate a point horizontally and vertically within a two-dimensional coordinate space. A coordinate space is determined by the following properties with respect to the current page: the location of the origin, the orientation of the x and y axes, the lengths of the units along each axis.
  • Graphics state, which defines the current state of the drawing system: current stroking and non-stroking colors, masks, transformations, clipping, alpha etc. See section 8.4 ”Graphics State” of the PDF specification.
  • Drawing commands, which use current coordinate space and graphics state and may affect them if designed to do so. They often use graphics paths as an input, which in turn consist of atomic operations for combining lines and curves into a single figure.
  • ColorSpace, which defines the current color representation and transformation

Implementation in Fixed layout API

Coordinate space

ClippedContent class instances are being used as command containers and can be nested into each other providing a way to create combined clippings and other visual effects. Page class is inherited from ClippedContent thus providing the same functionality. Page maps directly to the document's page it represents and its coordinate space corresponds by default to the doc's page coordinate space.

Drawing commands

One may draw complex and simple figures by using the Path class that represents the PDF path object. It's possible to draw lines, curves, standard primitives, clipped shapes and more using this object. Please check the samples below demonstrating its functionality. The approach for using it is as follows: build the path, set container properties, add path to the container invoking desired operation(stroke, fill, stroke and fill).

Drawing a line

We create a Path object and add line to it by calling the self-explaining method Path.AppendLine. After that we set line width and color and stroke this path object by calling ClippedContent.StrokePath method.

// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // create new document
    using(FixedDocument document = new FixedDocument())
    {
        // create new page
        Page page = new Page(Boundaries.A4);

        // create new path representing a line
        Path path = new Path(10, 820);
        path.AppendLine(300,750);

        // set current non-stroking color             
        page.Content.SetDeviceStrokingColor(new double[] { 1, 0, 1 });                
        // set line width                
        page.Content.SetLineWidth(2.0);
        // stroke path
        page.Content.StrokePath(path);

        // add page to the document
        document.Pages.Add(page);

        // save to output stream
        document.Save(outputStream);
    }
}
 

The results produced by this code are below:

Drawing a line


Drawing a Bézier curve

Drawing a curve has lots in common with drawing a line, except we have to define control points for it as described in section 8.5.2.2 “Cubic Bézier Curves” of the PDF specification. The code is almost the same as for drawing a line except that AppendCubicBezier method is being used instead of AppendLine to define a Bézier curve.

// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // create new document and add empty page
    using(FixedDocument document = new FixedDocument())
    {
        Page page = new Page(Boundaries.A4);

        // create new path representing a line
        Path path = new Path(10, 750);
        path.AppendCubicBezier(100,700,200,720,300,840);
        // set current non-stroking color             
        page.Content. SetDeviceStrokingColor (new double[] {0.3, 0.5, 1 });

        // set line width                
        page.Content.SetLineWidth(2.0);
        // stroke path
        page.Content.StrokePath(path);

        document.Pages.Add(page);

        // save to output stream
        document.Save(outputStream);
    }
}
 

Here are the results:

Drawing bézier curve


Drawing a circle using Bézier curves

Now we'll show how to draw a complex but very common figure, a circle. In order to draw it we will combine four Bézier curves to one path and stroke it. The code for drawing a circle is as follows:

// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // radius of the circle
    double radius = 100;
    // circle constant
    double r_c = 0.5522847498 * radius;

    // create new document
    using(FixedDocument document = new FixedDocument())
    {
        // create new page
        Page page = new Page(Boundaries.A4);

        // create new path representing a circle
        Path path = new Path(0, radius);                
        path.AppendCubicBezier(r_c, radius, radius, r_c, radius, 0);
        path.AppendCubicBezier(radius, -r_c, r_c, -radius, 0, -radius);
        path.AppendCubicBezier(-r_c, -radius, -radius, -r_c, -radius, 0);
        path.AppendCubicBezier(-radius, r_c, -r_c, radius, 0, radius);

        // set current non-stroking color             
        page.Content.SetDeviceStrokingColor(new double[] { 0.3, 0.5, 1 });
        // set line width                
        page.Content.SetLineWidth(2.0);

        page.Content.ModifyCurrentTransformationMatrix(1,0,0,1,250,730);
        // stroke path
        page.Content.StrokePath(path);

        // add page to the document
        document.Pages.Add(page);

        // save to output stream
        document.Save(outputStream);
    }
}
 

The resulting circle it creates is shown below:

Drawing circle using bézier curves

This sample demonstrated how to create complex paths and combine several drawing commands together. Next sample will show how to draw filled shapes and apply clipping to achieve various effects.


Drawing a filled shape with clipping

In order to draw a clipped and filled shape on PDF page you have to set clipping path and draw using it. PDF specification defines several filling and clipping rules described in sections 8.5.3.3 "Filling" and 8.5.4 “Clipping Path Operators” respectively. These sections contain very detailed description of what areas of the path should be considered “fillable” or “clippable”, it’s important to understand what EvenOdd and NonZeroWinding rules are and reading of these sections is highly recommended.

// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // radius of the circle
    double radius = 100;
    // circle constant
    double r_c = 0.5522847498*radius;

    // create new document
    using(FixedDocument document = new FixedDocument())
    {
        // create new page
        Page page = new Page(Boundaries.A4);

        // create and construct new clipping path
        Path clippingPath = new Path(radius,-radius);   
                
        // outer rect
        clippingPath.AppendLine(radius, radius);
        clippingPath.AppendLine(-radius,radius);
        clippingPath.AppendLine(-radius, -radius); 
        clippingPath.AppendLine(radius, -radius);

        //inner small rect
        clippingPath.MoveTo(-20, 20);
        clippingPath.AppendLine(20, 20);
        clippingPath.AppendLine(20, -20);
        clippingPath.AppendLine(-20, -20);
        clippingPath.AppendLine(-20, 20);     
                                
        // create clipped content using non-zero winding rule and our clipping path,
        // it will clip everything that hits the inner small rect and we will get a "hole"
        // because rectangles in clipping path are drawn in opposite direction
        ClippedContent clippedContent = new ClippedContent(clippingPath, FillRule.Nonzero);                                  

        // create new path representing a circle
        Path path = new Path(0, radius);
        path.AppendCubicBezier(r_c, radius, radius, r_c, radius, 0);
        path.AppendCubicBezier(radius, -r_c, r_c, -radius, 0, -radius);
        path.AppendCubicBezier(-r_c, -radius, -radius, -r_c, -radius, 0);
        path.AppendCubicBezier(-radius, r_c, -r_c, radius, 0, radius);

        // set current non-stroking color             
        clippedContent.SetDeviceStrokingColor (new double[] { 0.3, 0.5, 1 });
        clippedContent.SetDeviceNonStrokingColor(new double[] { 0.3, 0.5, 1 });
        // set line width                
        clippedContent.SetLineWidth(2.0);
        // stroke path
        clippedContent.FillAndStrokePath(path);                

        // set current tranform
        page.Content.ModifyCurrentTransformationMatrix(1, 0, 0, 1, 250, 730);
        // append clipped content
        page.Content.AppendContent(clippedContent);             

        // add page to the document
        document.Pages.Add(page);

        // save to output stream
        document.Save(outputStream);
    }
}
 

The results are shown below:

Filled shape with clipping applied


Drawing standard primitives

A number of the static methods provided by Path class can be used to draw standard primitives like circle, ellipse, rect etc. See the code below:

using (Stream outputStream = File.Create("primitives.pdf"))
{
    // create document and add one page to it
    using(FixedDocument fixedDocument = new FixedDocument())
    {
        fixedDocument.Pages.Add(new Page());

        ClippedContent pageContent = fixedDocument.Pages[0].Content;
        pageContent.Translate(50,790);

        // create circle
        Path circle = Path.CreateCircle(0, 0, 40);    
        pageContent.SetDeviceNonStrokingColor(new double[]{1,0,0});                
        pageContent.FillAndStrokePath(circle);

        // create ellipse
        Path ellipse = Path.CreateEllipse(0, 0, 60, 40);
        pageContent.Translate(70,0);
        pageContent.SetDeviceNonStrokingColor(new double[] { 0, 1, 0 });
        pageContent.FillAndStrokePath(ellipse);

        // create roundrect
        Path roundRect = Path.CreateRoundRect(0, 0, 100, 60, 5);    
        pageContent.Translate(50, -30);
        pageContent.SetDeviceNonStrokingColor(new double[] { 0, 0, 1 });
        pageContent.FillAndStrokePath(roundRect);

        // save document
        fixedDocument.Save(outputStream);
    }
} 
 

Resulting PDF document looks as follows:

Drawing standard primitives


ColorSpaces

Colors in PDF are defined using colorspaces and color values mapped to these spaces. All available colorspaces are described in section 8.6 “Colour Spaces” of the PDF specification. The colorspace in short, is a way to represent a particular color using specific “coordinates” in this space. As an example, for well-known sRGB colorspace these values are R, G and B components.

In order to stroke or fill using a particular color you’ll have to select the corresponding colorspace and set the desired color as current stroking or non-stroking color.

Expand to see the sample code below:
// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // create new PDF document
    using(FixedDocument document = new FixedDocument())
    {
        Page page = new Page();
        // register RGB and CMYK color spaces. 
        // it's also possible to use Lab, Gray, Indexed, ICC based color spaces.
        document.ResourceManager.RegisterResource(new RgbColorSpace("CS_RGB"));
        document.ResourceManager.RegisterResource(new CmykColorSpace("CS_CMYK"));

        // use RGB color space and set non stroking color
        page.Content.SetNonStrokingColorSpace("CS_RGB");
        page.Content.SetNonStrokingColor(0.33, 0.66, 0.33);
        // use RGB color space and set stroking color
        page.Content.SetStrokingColorSpace("CS_RGB");
        page.Content.SetStrokingColor(0.77, 0.2, 0.33);

        // create path and fill it using the color created above
        Path path = new Path();
        path.AppendRectangle(10, 720, 300, 80);
        page.Content.FillAndStrokePath(path);

        // use CMYK color space, and set non-stroking color
        page.Content.SetNonStrokingColorSpace("CS_CMYK");
        page.Content.SetNonStrokingColor(0.1, 0.3, 0.2, 0);

        page.Content.ModifyCurrentTransformationMatrix(1, 0, 0, -1, 150, 850);
      
        // create path, fill it using CMYK color and stroke it using RGB color set above
        Path path2 = new Path(70, 80);
        path2.MoveTo(75, 40);
        path2.AppendCubicBezier(75, 37, 70, 25, 50, 25);
        path2.AppendCubicBezier(20, 25, 20, 62.5, 20, 62.5);
        path2.AppendCubicBezier(20, 80, 40, 102, 75, 120);
        path2.AppendCubicBezier(110, 102, 130, 80, 130, 62.5);
        path2.AppendCubicBezier(130, 62.5, 130, 25, 100, 25);
        path2.AppendCubicBezier(85, 25, 75, 37, 75, 40);
        path2.ClosePath();                    
        page.Content.FillAndStrokePath(path2);

        // add created page and save document
        document.Pages.Add(page);                                        
        document.Save(outputStream);     
    }           
}
 

This code shows how to set current colors using various colorspaces for filling and stroking of graphics paths. The image below demonstrates the result:

Colorspaces and colors

We used simple device colorspaces in this example, but it’s possible to load colorspaces described by ICC profiles provided with some reproduction devices or use complex colorspaces e.g. Lab or Indexed.

Device colorspaces

Every raster output device has a native colourspace, which often corresponds to one of the following process color models: gray (monochrome), RGB (red,green,blue) or CMYK (cyan,magenta,yellow,black).

The device colorspaces enable the author to specify color values that are directly related to their representation on an output device. Color values in these colorspaces map directly (or by means of a simple conversion formula) to device "colorants", such as quantities of ink or intensities of pixel components. This allows a PDF creating software to control colors with high precision for a particular device, but the results may differ from one device to another.

Output devices form final colors either by summing light sources together or by subtracting light from an illuminated source. Displays typically add colors (eInk is one of the exceptions), while printing with inks subtracts them. These two ways of forming colors are called additive and subtractive color models. The most widely used methods to specify the color using these models are known as RGB and CMYK, named so for names of the primary colors on which they are based. They map to the following device colorspaces:

  • DeviceGray controls the intensity of achromatic light, from black to white. Colors in this colorspace should be represented by a single number from the range [0.0,1.0] where 0.0 is black, 1.0 is white.
  • DeviceRGB controls the intensities of red, green, and blue light, the three additive primary colors used in modern displays. Colors in this colorspace should be represented by 3 numbers where each should be in the range [0.0,1.0] where 0.0 is the absence of the component, 1.0 means maximum intensity.
  • DeviceCMYK controls the amount of cyan, magenta, yellow, and black inks, the four subtractive process colors used in printing. Colors in this colorspace should be represented by 4 numbers where each should be in the range [0.0,1.0] where 0.0 is the absence of the colorant (no ink), 1.0 means maximum amount of colorant.

There is even a simple formula to convert the color from CMYK to RGB (DeviceCMYK to DeviceRGB):

red = 1.0 - min(1.0, cyan + black)
green = 1.0 - min(1.0, magenta + black) 
blue = 1.0 − min (1.0, yellow + black)
 

Graphics State

Drawing commands in PDF are being added in sequences and therefore the commands that change the state of the current drawing context may affect subsequent drawings. In order to avoid that, you can generate your drawings as isolated sequences and perform a quick rollback when you're done. It's here when the concept of the graphics state comes into play. The PDF specification defines it in section 8.4 “Graphics State”. In a few words, a graphics state is an internal object that is being used to maintain the state of the current graphics context: selected stroking and non-stroking colors, clipping, transformations etc. These states can be "enclosed" into each other thus making inner state saving possible in a form of a graphics state stack. In Fixed layout API this class is represented by the GraphicsState class.

Basic usage

Basic technique for saving and restoring of the current graphics state requires a command to be added to the drawing commands sequence. Below is the code that shows it in action:

// create clipped content object for drawing
ClippedContent page = new ClippedContent();

// save current state
page.SaveGraphicsState();

… apply transforms, set colors, draw paths

// restore initial state
page.RestoreGraphicsState();

…continue drawing using restored state
 

Using graphics state from resources

The basic usage sample shows how to work with the current graphics state using Save/Restore commands, but it’s also possible to use graphics state objects that you have created in advance and designed to be reusable. E.g. a graphics state describing some alpha blending operations that you’d like to share and use for many independent drawings. There is a special class called GraphicsState that can be used to achieve this goal, it’s located under the Apitron.PDF.Kit.FixedLayout.Resources.GraphicsStates namespace. These state objects can be set using their resource identifiers and should be registered as resources first in order to be used.

See the code sample below showing how to set a named graphics state for drawing:

// create output PDF file
using (FileStream outputStream = new FileStream("outfile.pdf", FileMode.Create, FileAccess.Write))
{
    // create new PDF document
    using(FixedDocument document = new FixedDocument())
    {
        document.Pages.Add(new Page());

        // register font resource
        Font font = new Font("F1", StandardFonts.TimesItalic);
        document.ResourceManager.RegisterResource(font);

        // register graphics state resource
        GraphicsState gs = new GraphicsState("gs01") {FontResourceID = "F1", FontSize = 14};

        // register graphics state resource
        document.ResourceManager.RegisterResource(gs);
       
        Content content = document.Pages[0].Content;

        // font will be set using the graphics state resource
        content.SetGraphicsState("gs01");

        // draw text spiral
        for (double angle = 0; angle <= 4*3.14; angle += 0.2)
        {
            content.SaveGraphicsState();
            content.ModifyCurrentTransformationMatrix(Math.Cos(angle), -Math.Sin(angle), Math.Sin(angle),
                Math.Cos(angle), 200, 660);
            TextObject text = new TextObject();
            text.SetTextMatrix(1, 0, 0, 1, 200/(angle + 1), 0);
            text.SetTextRenderingMode(RenderingMode.FillText);
            text.AppendText("Hello world by Apitron!");
            content.AppendText(text);
            content.RestoreGraphicsState();
        }

        // save document
        document.Save(outputStream);
    }
}
 

This sample draws the text along the spiral and uses named graphics state resource object to set the current font. It also uses SaveGraphicsState and RestoreGraphicsState commands for saving and restoring the transformation changed during the drawing of each text line. Here is the resulting image showing the spiral text generated by the sample code above. You may notice that text in this PDF file uses standard TimesItalic font set by applying a named graphics state object:

Using graphics state from resources

Optional content

This feature of PDF allows the user to selectively show or hide parts of document's content configured for this purpose. It may be useful for documents containing CAD drawings, layered illustrations, or multilingual documents. E.g. instead of producing a few separate docs containing different versions of the same document, one may produce a single document allowing the user to select which part should be viewable at the given moment.

The steps to create a PDF document containing several content layers on a page using Fixed layout API, look as follows:

  1. Create several OptionalContentGroup objects and register them as document's resources – these objects serve as layer identifiers in PDF
  2. Create the OptionalContentConfiguration object, set its properties controlling the behavior and visual layer structure shown in reader’s UI. This object combines layers together and you can use it to define initially visible layers, locked layers, layers that should work as radio buttons etc. You can also define the visual tree structure – parent layer nodes and child nodes.
  3. Create and initialize the OptionalContentProperties object required by the FixedDocument instance – this object is used to define the default configuration to be used by the PDF reader for showing layers, and to specify the list of layers (OptionalContentGroups resource ids) actually referenced in document’s content (because not all registered layers' ids may be in use)
  4. Use ClippedContent class to define the layers and assign their OptionalContentID property to one of the registered layers' ids (ids of OptionalContentGroup objects created earlier). Add these objects on PDF page using Page.Content.AppendContent(…) method

The code sample demonstrating how to define optional content groups can be found below:

class Program
{
    static void Main(string[] args)
    {
        using (Stream stream = File.Create("manual.pdf"))
        {
            // create our PDF document
            using (FixedDocument doc = new FixedDocument())
            {
                // turn on the layers panel when opened in conforming reader
                doc.PageMode = PageMode.UseOC;

                // register image resource 
                doc.ResourceManager.RegisterResource(
                    new Apitron.PDF.Kit.FixedLayout.Resources.XObjects.Image("chair","../../data/chair.jpg"));

                // FIRST STEP: create layer definitions,
                // they should be registered as documents' resources
                OptionalContentGroup group0 = new OptionalContentGroup("group0", "Page layers", IntentName.View);                   
                doc.ResourceManager.RegisterResource(group0);

                OptionalContentGroup group1 = new OptionalContentGroup("group1", "Chair image", IntentName.View);
                doc.ResourceManager.RegisterResource(group1);                   

                OptionalContentGroup group2 = new OptionalContentGroup("English", "English", IntentName.View);
                doc.ResourceManager.RegisterResource(group2);

                OptionalContentGroup group3 = new OptionalContentGroup("Dansk", "Dansk", IntentName.View);
                doc.ResourceManager.RegisterResource(group3);

                OptionalContentGroup group4 = new OptionalContentGroup("Deutch", "Deutch", IntentName.View);
                doc.ResourceManager.RegisterResource(group4);

                OptionalContentGroup group5 = new OptionalContentGroup("Русский", "Русский", IntentName.View);
                doc.ResourceManager.RegisterResource(group5);

                OptionalContentGroup group6 = new OptionalContentGroup("Nederlands", "Nederlands", IntentName.View);
                doc.ResourceManager.RegisterResource(group6);

                OptionalContentGroup group7 = new OptionalContentGroup("Français", "Français", IntentName.View);
                doc.ResourceManager.RegisterResource(group7);

                OptionalContentGroup group8 = new OptionalContentGroup("Italiano", "Italiano", IntentName.View);
                doc.ResourceManager.RegisterResource(group8);

                // SECOND STEP:
                // create the configuration, it allows to combine the layers together in any order     
                // Default configuration:            
                OptionalContentConfiguration config = new OptionalContentConfiguration("configuration");
                
                // add groups to lists which define the rules controlling their initial visibility                  
                // ON groups (visible)
                config.OnGroups.Add(group0);
                config.OnGroups.Add(group1);
                config.OnGroups.Add(group2);
                        
                // OFF groups (hidden)          
                config.OffGroups.Add(group3);
                config.OffGroups.Add(group4);
                config.OffGroups.Add(group5);
                config.OffGroups.Add(group6);
                config.OffGroups.Add(group7);
                config.OffGroups.Add(group8);

                // lock the layer containing the image
                config.LockedGroups.Add(group1);

                // make other layers working as radio buttons, so that only one translation will be visible at time
                config.RadioButtonGroups.Add(new[] { group2, group3, group4, group5, group6, group7, group8 });                    

                // show only groups referenced by visible pages
                config.ListMode = ListMode.VisiblePages;
                // initialize the states for all content groups, for the default configuration it should be On
                config.BaseState = OptionalContentGroupState.On;
                
                // set the name of the presentation tree
                config.Order.Name = "Default config";      
                // create a root node + sub elements using the registed groups           
                config.Order.Entries.Add(group0);
                config.Order.Entries.Add(new OptionalContentGroupTree(group1, group2, group3, group4, group5, group6, group7, group8));

                // FINAL step:
                // assign the configuration properties to the document, all configurations and groups should be specified
                doc.OCProperties = new OptionalContentProperties(config, new OptionalContentConfiguration[] {}, 
                    new[] { group0, group1, group2, group3, group4, group5, group6, group7, group8 });

                // create page and assign top layer id to its content, it allows to completely hide page's
                // content using the configuration we have created                   
                Page page = new Page();
                page.Content.OptionalContentID = "group0";

                // create the layer containing image
                ClippedContent imageBlock = new ClippedContent(0, 0, 245, 300);
                // set the layer id
                imageBlock.OptionalContentID = "group1";
                imageBlock.AppendImage("chair", 0, 0, 245, 300);

                // put the layer on page
                page.Content.SaveGraphicsState();
                page.Content.Translate(0, 530);
                page.Content.AppendContent(imageBlock);
                page.Content.RestoreGraphicsState();

                // append text layers
                AppendTextLayers(page);
                // add the page to the document and save it
                doc.Pages.Add(page);

                doc.Save(stream);
            }
        }

        Process.Start("manual.pdf");
    }

    // generate text layers using pre-created string resources
    static void AppendTextLayers(Page page)
    {
        page.Content.SaveGraphicsState();
        page.Content.Translate(250, 325);

        // evaluate each property of a resource dictionary and add text to the PDF page
        foreach (PropertyInfo info in typeof(strings).GetRuntimeProperties())
        {
            if (info.PropertyType == typeof(string))
            {
                ClippedContent textContent = new ClippedContent(0, 0, 300, 500);
                // assign layer id
                textContent.OptionalContentID = info.Name;
                textContent.Translate(0, 0);

                // preprocess parsed elements and set additional properties
                // for better visual appearance
                IEnumerable<ContentElement> elements = ContentElement.FromMarkup((string)info.GetValue(null));
                   
                foreach (Br lineBreak in elements.OfType<Br>())
                {
                    lineBreak.Height = 10;  
                }

                foreach (Section subSection in elements.OfType<Section>())
                {
                    subSection.Font = new Apitron.PDF.Kit.Styles.Text.Font("HelveticaBold", 14);
                }

                // draw text
                textContent.AppendContentElement(new Section(elements), 300, 500);
                // put the text layer on page
                page.Content.AppendContent(textContent);                
            }
        }

        page.Content.RestoreGraphicsState();
    }
}
 

Image below demonstrates the results:

Optional content demo

You can see the locked layer containing the product image(chair) and a set of language layers available for viewing. These language layers work as radio buttons group, so when one is turned on the corresponding content group is shown while others become hidden.

The original article can be found by this [link] and complete code sample can be found in our samples repository.

Implementation in Flow layout API

There are no direct mappings to the graphic objects defined by the PDF specification in Flow layout API because it uses a different approach for documents generation, nevertheless Form XObjects can be included into the final document using the ContentReference elements.