Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Objective-C

Parse SVG Files to Bezier Curves in C++ and Save to PDF

4.64/5 (9 votes)
19 Jul 2018CPOL3 min read 23.5K   799  
This articles describes how to open SVG files in C++ application, work with Bezier curves and export to PDF.

Introduction

This code was written to demonstrate the process of parsing vector SVG files in C/C++ application, working with Bezier curves, and saving the vector image to PDF. For ease of understanding, all processing in app only works in grayscale mode. The code was written for Windows, but has no Windows-only dependencies and can be easily ported to Linux or any embedded platform.

Scalable Vector Graphics (SVG) is a 2D vector image format widely used in Web development. The SVG format is heavily based on XML standard. While the raster image (PNG, JPG, GIF, etc.) is composed of a fixed set of pixels, the vector SVG image is composed of a fixed set of shapes, like circles, lines, rectangles, Bezier curves, etc. The advantage of SVG format is that it can be easily scaled to any level without the loss of quality.

I have included a nice kitten here as a sample of SVG image:

Cat

Bézier curve is a parametric curve which is used to model smooth curves that can be scaled indefinitely. Bezier curve can be represented as a set of control points. Quadratic and cubic Bezier curves are most common. Higher degree curves are more computationally expensive to evaluate. When more complex shapes are needed, low order Bezier curves are patched together, producing a composite Bezier curve. A composite Bezier curve is commonly referred to as a path.

Quadratic Bézier curve

Cubic Bézier curve

Quadratic Bézier curve

Cubic Bézier curve

The Portable Document Format (PDF) is a file format to represent documents, including text formatting, raster images, vector graphics, etc. Vector graphics in PDF are constructed with paths too. Paths are usually composed of lines and cubic Bezier curves, but can also be constructed from the outlines of text. Paths in PDF can be stroked, filled, clipping. Strokes and fills can use any color set in the graphics state, including patterns.

Background

For the processing of SVG files, I used brilliant library called nanosvg. It is small and lightweight and can be used even in embedded development projects. To compose PDF files, I have used write-only libHaru library. Though not actively developed now (2018), it still can be used in C/C++ projects.

Using the Code

The SvgConverter class is a basic workhorse to convert SVG to PDF. The same object can be used multiple to convert other images. You just need to call a method that is responsible for loading the other file.

C++
//..

SvgConverter converter;
converter.loadFromFile(fileInput); // load svg image to inner buffer
converter.convertToPDF(fileOutput); // parse and write image from buffer to pdf file

//..

Class structure looks like this:

C++
class SvgConverter {

private:

    NSVGimage * g_image; 
    std::string fileName;

public:
    SvgConverter(std::string fileName);
    SvgConverter();
    bool isLoaded();
    bool loadFromFile(std::string fileName);
    bool convertToPDF(std::string fileName);
    
    ~SvgConverter();

private:
    float distPtSeg(float x, float y, float px, float py, float qx, float qy);
    void pdfcubicBez(HPDF_Page page, float x1, float y1, float x2, float y2,
        float x3, float y3, float x4, float y4,
        float tol, int level, Vector2f startPoint);
    void pdfPath(HPDF_Page page, float* pts, int npts, char closed, float tol, bool bFilled,
        Vector2f startPoint);
    static void error_handler(HPDF_STATUS   error_no,
        HPDF_STATUS   detail_no,
        void         *user_data);
};

To approximate Bezier curve and draw it, we will use one of the properties of such curves.
Any cubic Bezier curve B from beginning to end can be divided into two curves, which together will describe the same curve as B.

Converting and approximation method implementation, some code taken from nanosvg sample and adapted to PDF coordinate system:

C++
bool SvgConverter::convertToPDF(std::string fileName) {
    if (fileName.empty() || !isLoaded()) return false;

    HPDF_Doc pdf = HPDF_New(error_handler, NULL);
    if (!pdf) return false;

    HPDF_Page page = HPDF_AddPage(pdf);
    float width = this->g_image->width;
    float height = this->g_image->height;
    Vector2f startPoint = { 0, height};

    HPDF_Page_SetWidth(page, width);
    HPDF_Page_SetHeight(page, height);
    HPDF_Page_SetLineWidth(page, 0.1f); //initializing page

    for (NSVGshape * shape = g_image->shapes; shape != NULL; shape = shape->next) {
        if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; //pass invisible shapes

        float r = (float)((shape->fill.color >> 16) & 0xFF) / 255.0f;
        float g = (float)((shape->fill.color >> 8) & 0xFF) / 255.0f;
        float b = (float)((shape->fill.color) & 0xFF) / 255.0f;
        float gray = (r + g + b) / 3.0f; 
        gray = (gray < 0.5f) ? 0 : 1.0f;

        HPDF_Page_SetGrayFill(page, gray);    // sets the filling color

        for (NSVGpath * path = shape->paths; path != NULL; path = path->next ){
            // drawing each path in shape to pdf file
            pdfPath(page, path->pts, path->npts, path->closed, 
                    0.1f, shape->fill.type != NSVG_PAINT_NONE, startPoint);
        }
        if (shape->fill.type != NSVG_PAINT_NONE) {
            if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD)
                HPDF_Page_EofillStroke(page); // fills the curr path using the 
                                              // even-odd rule and paints
            else HPDF_Page_FillStroke(page);  // fills the curr path using the 
                                              // nonzero winding number rule and paints
        }
        else HPDF_Page_Stroke(page);          // paints the path
    }
    HPDF_SaveToFile(pdf, fileName.c_str());
    HPDF_Free(pdf);

    return true;
}

float SvgConverter::distPtSeg(float x, float y, float px, float py, float qx, float qy)
{
    float pqx, pqy, dx, dy, d, t;
    pqx = qx - px;
    pqy = qy - py;
    dx = x - px;
    dy = y - py;
    d = pqx*pqx + pqy*pqy;
    t = pqx*dx + pqy*dy;
    if (d > 0) t /= d;
    if (t < 0) t = 0;
    else if (t > 1) t = 1;
    dx = px + t*pqx - x;
    dy = py + t*pqy - y;
    return dx*dx + dy*dy;
}

void SvgConverter::pdfcubicBez(HPDF_Page page, float x1, float y1, float x2, float y2,
    float x3, float y3, float x4, float y4,
    float tol, int level, Vector2f startPoint)
{
    float x12, y12, x23, y23, x34, y34, x123, y123, x234, y234, x1234, y1234;
    float d;

    if (level > 12) return;
    //geting midpoints
    x12 = (x1 + x2)*0.5f;
    y12 = (y1 + y2)*0.5f;
    x23 = (x2 + x3)*0.5f;
    y23 = (y2 + y3)*0.5f;
    x34 = (x3 + x4)*0.5f;
    y34 = (y3 + y4)*0.5f;
    x123 = (x12 + x23)*0.5f;
    y123 = (y12 + y23)*0.5f;
    x234 = (x23 + x34)*0.5f;
    y234 = (y23 + y34)*0.5f;
    x1234 = (x123 + x234)*0.5f;
    y1234 = (y123 + y234)*0.5f;

    d = distPtSeg(x1234, y1234, x1, y1, x4, y4);             //check if curr curve is flat
    if (d > tol * tol) {
        pdfcubicBez(page, x1, y1, x12, y12, x123, 
            y123, x1234, y1234, tol, level + 1, startPoint); //dividing first piece
        pdfcubicBez(page, x1234, y1234, x234, y234, x34, 
           y34, x4, y4, tol, level + 1, startPoint);         // dividing second piece
    }
    else HPDF_Page_LineTo(page, startPoint.x + x4 / 3.0f, 
                   startPoint.y - y4 / 3.0f);                //curr piece of curve is enough flat
    // appends a path from the curr point to x4,y4 
}

void SvgConverter::pdfPath(HPDF_Page page, float* pts, int npts, char closed, 
                           float tol, bool bFilled, Vector2f startPoint)
{
    HPDF_Page_MoveTo(page, startPoint.x + pts[0] / 3.0f, 
               startPoint.y - pts[1] / 3.0f); // moving to first point of bezier curve

    for (int i = 0; i < npts - 1; i += 3) {
        float* p = &pts[i * 2];
        pdfcubicBez(page, p[0], p[1], p[2], p[3], p[4], p[5], 
                  p[6], p[7], tol, 0, startPoint); // draw a cubic bezier curve
    }
    if (closed) HPDF_Page_LineTo(page, startPoint.x + pts[0] / 3.0f, startPoint.y - pts[1] / 3.0f);
}

You can use an application from console to include in some batch processing. Application receives two command line arguments, where the first argument is SVG file path and the second is PDF file path.
For example:

./svgtopdf cat.svg cat.pdf

Points of Interest

While trying to link C library to C++ project, first you have to disable precompiled headers for C files. This can be done by selecting multiple C files first in Solution Explorer, right click, then go to "Precompiled Headers" tab and select "Not using Precompiled Headers".

Precompiled headers

Also, you need to wrap the C include files in an extern "C" directive:

C++
extern "C" {
#include "libharu\hpdf.h"
#include "libharu\hpdf_utils.h"
}

Conclusion

Feel free to send me any comments and suggestions. If you find this approach useful, feel free to upvote the article. If you find yourself wanting to improve the code, feel free to clone the github repository.

Good luck!

History

  • 18th July, 2018 - First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)