Hacking:Plotter format

From Valentina Project's Wiki
Jump to navigation Jump to search

The goal of this page is to describe process of development support for export in a plotter format. It will include some code segments as well as tips about development process.

Plotter format[edit | edit source]

By plotter format we mean here Hewlett-Packard Graphics Language or HPGL. There are two versions of the format we particularly interested in: HP-GL and HP-GL/2. The third one, HP RTL, not covered by this document. HP-GL used for vector data for pen plotter control. And HP-GL/2 for vector data for raster plotter control. Both of them are suited for our purposes, thus needed to be supported.

According to this document a file extension for both versions should be .plt.

Development preparations[edit | edit source]

License template[edit | edit source]

We encourage developers to always include license header in each file. To automate this routine process Qt Creator allows you specify a license template. Find such in <root>/share/copyright_template.txt. Open Qt Creator's options dialog and find section C++. In tab File naming you will find field to define a path to your license template. We recommend to copy our template, modify it and place somewhere where Qt Creator will be able to find it.

Branch[edit | edit source]

For development purposes create a new branch plotter_format on top of develop branch.

Static library[edit | edit source]

Initialization[edit | edit source]

Code of export classes must be placed in static library with a name vhpgl. For this:

  1. Go to the project tree.
  2. Find path src/libs.
  3. Call context menu on libs and select Add new subproject.
  4. Select type C++ library.
  5. Name it and check if path is correct. The libs folder must be the root for new library.
  6. Select qmake.
  7. Select type of new library to Statically Linked Library. Left everything else by default. We will change it later.
  8. No translation support needed.
  9. Select a kit by default.
  10. Select Git and libs.pro.

Congratulations! You have successfully added static library to the project tree. It of course needs some tweaking that we will start doing immediately.

Tweaking[edit | edit source]

You will find that qmake script for all libraries are more or less standardized across whole project. Knowing how we define one library make it easier to understand other.

# vhpgl.pro
# File with common stuff for whole project
include(../../../common.pri)

QT -= gui

# Name of library
TEMPLATE = lib

# We want create a library
CONFIG += staticlib

# Since Q5.12 available support for C++17
equals(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 11) {
    CONFIG += c++17
} else {
    CONFIG += c++14
}

# Use out-of-source builds (shadow builds)
CONFIG -= debug_and_release debug_and_release_target

# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

include(vhpgl.pri)

# This is static library so no need in "make install"

# directory for executable file
DESTDIR = bin

# files created moc
MOC_DIR = moc

# objecs files
OBJECTS_DIR = obj

# Set using ccache. Function enable_ccache() defined in common.pri.
$$enable_ccache()

include(warnings.pri)

CONFIG(release, debug|release){
    # Release mode
    CONFIG += silent

    !unix:*g++*{
        QMAKE_CXXFLAGS += -fno-omit-frame-pointer # Need for exchndl.dll
    }

    noDebugSymbols{ # For enable run qmake with CONFIG+=noDebugSymbols
        # do nothing
    } else {
        !macx:!*msvc*{
            # Turn on debug symbols in release mode on Unix systems.
            # On Mac OS X temporarily disabled. TODO: find way how to strip binary file.
            QMAKE_CXXFLAGS_RELEASE += -g -gdwarf-3
            QMAKE_CFLAGS_RELEASE += -g -gdwarf-3
            QMAKE_LFLAGS_RELEASE =
        }
    }
}

include (../libs.pri)

Most of the time this code is self explanatory, but few things must be covered anyway. With include(vhpgl.pri) we move our definitions of .h and .cpp file to separate file. It create clean source tree structure. You need to create it manually.

# ADD TO EACH PATH $$PWD VARIABLE!!!!!!
# This need for corect working file translations.pro

SOURCES += \
    $$PWD/vhpgl.cpp

*msvc*:SOURCES += $$PWD/stable.cpp

HEADERS += \
    $$PWD/stable.h \
    $$PWD/vhpgl.h

Copy files stable.h and stable.cpp from any library and place in vhpgl library.

Replace name of a library in stable.h to VHPGL.

#pragma message("Compiling precompiled headers for VHPGL library.\n")


include(warnings.pri) -- copy paste from vdxf library.

include (../libs.pri) -- add these lines:

#VHPGL static library
INCLUDEPATH += $${PWD}/vhpgl

That's all, now your library is ready to start developing new code. Build project to see if everything works as intended.

Development[edit | edit source]

Architecture[edit | edit source]

We will make our architecture compatible with Qt's Paint System, but not based on it. This has few benefits: nice and recognizable architecture, standardize way to add new export format.

The reason for not inheriting from the Paint System is simple, we don't need support for Qt Graphics Scene. We will draw our images directly with much higher level of control over the process.

Paint System template[edit | edit source]

For our purposes we need to create two new classes VHPGLEngine and VHPGLPaintDevice.

 1// vhpglpaintdevice.h
 2#ifndef VHPGLPAINTDEVICE_H
 3#define VHPGLPAINTDEVICE_H
 4
 5#include <QtGlobal>
 6
 7class VHPGLEngine;
 8class QSize;
 9class VLayoutPiece;
10
11class VHPGLPaintDevice
12{
13public:
14    VHPGLPaintDevice();
15    ~VHPGLPaintDevice();
16
17    QSize GetSize() const;
18    void  SetSize(QSize size);
19
20    QString GetFileName() const;
21    void    SetFileName(const QString &filename);
22
23    bool ExportToHPGL(const QVector<VLayoutPiece> &details) const;
24    bool ExportToHPGL2(const QVector<VLayoutPiece> &details) const;
25private:
26    Q_DISABLE_COPY(VHPGLPaintDevice)
27    VHPGLEngine *m_engine;
28};
29
30#endif // VHPGLPAINTDEVICE_H
 1// vhpglpaintdevice.cpp
 2#include "vhpglpaintdevice.h"
 3#include "vhpglengine.h"
 4
 5//---------------------------------------------------------------------------------------------------------------------
 6VHPGLPaintDevice::VHPGLPaintDevice()
 7    : m_engine(new VHPGLEngine())
 8{}
 9
10//---------------------------------------------------------------------------------------------------------------------
11VHPGLPaintDevice::~VHPGLPaintDevice()
12{
13    delete m_engine;
14}
15
16//---------------------------------------------------------------------------------------------------------------------
17void VHPGLPaintDevice::SetFileName(const QString &filename)
18{
19    if (m_engine->isActive())
20    {
21        qWarning("VHPGLPaintDevice::SetFileName(), cannot set file name while HPGL is being generated");
22        return;
23    }
24
25    m_engine->SetFileName(filename);
26}
27
28//---------------------------------------------------------------------------------------------------------------------
29bool VHPGLPaintDevice::ExportToHPGL(const QVector<VLayoutPiece> &details) const
30{
31    m_engine->setActive(true);
32    const bool res = m_engine->GenerateHPGL(details);
33    m_engine->setActive(false);
34    return res;
35}
36
37//---------------------------------------------------------------------------------------------------------------------
38bool VHPGLPaintDevice::ExportToHPGL2(const QVector<VLayoutPiece> &details) const
39{
40    m_engine->setActive(true);
41    const bool res = m_engine->GenerateHPGL2(details);
42    m_engine->setActive(false);
43    return res;
44}
45
46//---------------------------------------------------------------------------------------------------------------------
47QString VHPGLPaintDevice::GetFileName() const
48{
49    return m_engine->GetFileName();
50}
51
52//---------------------------------------------------------------------------------------------------------------------
53QSize VHPGLPaintDevice::GetSize() const
54{
55    return m_engine->GetSize();
56}
57
58//---------------------------------------------------------------------------------------------------------------------
59void VHPGLPaintDevice::SetSize(QSize size)
60{
61    if (m_engine->isActive())
62    {
63        qWarning("VHPGLPaintDevice::setSize(), cannot set size while HPGL is being generated");
64        return;
65    }
66    m_engine->SetSize(size);
67}
 1// vhpglengine.h
 2#ifndef VHPGLENGINE_H
 3#define VHPGLENGINE_H
 4
 5#include "../ifc/ifcdef.h"
 6
 7class VLayoutPiece;
 8
 9class VHPGLEngine
10{
11    friend class VHPGLPaintDevice;
12public:
13    VHPGLEngine();
14
15    bool isActive() const;
16    void setActive(bool newState);
17
18    QSize GetSize() const;
19    void  SetSize(QSize size);
20
21    QString GetFileName() const;
22    void    SetFileName(const QString &filename);
23
24private:
25    Q_DISABLE_COPY(VHPGLEngine)
26    uint         m_active{0};
27    QSize        m_size{};
28    QString      m_fileName{};
29
30    bool GenerateHPGL(const QVector<VLayoutPiece> &details) const;
31    bool GenerateHPGL2(const QVector<VLayoutPiece> &details) const;
32};
33
34//---------------------------------------------------------------------------------------------------------------------
35inline bool VHPGLEngine::isActive() const
36{
37    return m_active;
38}
39
40//---------------------------------------------------------------------------------------------------------------------
41inline void VHPGLEngine::setActive(bool newState)
42{
43    m_active = newState;
44}
45
46//---------------------------------------------------------------------------------------------------------------------
47inline QSize VHPGLEngine::GetSize() const
48{
49    return m_size;
50}
51
52//---------------------------------------------------------------------------------------------------------------------
53inline void VHPGLEngine::SetSize(QSize size)
54{
55    Q_ASSERT(not isActive());
56    m_size = size;
57}
58
59//---------------------------------------------------------------------------------------------------------------------
60inline QString VHPGLEngine::GetFileName() const
61{
62    return m_fileName;
63}
64
65//---------------------------------------------------------------------------------------------------------------------
66inline void VHPGLEngine::SetFileName(const QString &filename)
67{
68    Q_ASSERT(not isActive());
69    m_fileName = filename;
70}
71
72#endif // VHPGLENGINE_H
 1// vhpglengine.cpp
 2#include "vhpglengine.h"
 3#include "../vlayout/vlayoutpiece.h"
 4
 5//---------------------------------------------------------------------------------------------------------------------
 6VHPGLEngine::VHPGLEngine()
 7{}
 8
 9//---------------------------------------------------------------------------------------------------------------------
10bool VHPGLEngine::GenerateHPGL(const QVector<VLayoutPiece> &details) const
11{
12    if (details.isEmpty())
13    {
14        qCritical()<<"VHPGLEngine::GenerateHPGL(), details list is empty";
15        return false;
16    }
17
18    if (not m_size.isValid())
19    {
20        qCritical()<<"VHPGLEngine::GenerateHPGL(), size is not valid";
21        return false;
22    }
23
24    QFile data(m_fileName);
25    if (data.open(QFile::WriteOnly | QFile::Truncate))
26    {
27        QTextStream out(&data);
28
29        // Generate HPGL here
30
31        data.close();
32    }
33    else
34    {
35        qCritical() << "VHPGLEngine::GenerateHPGL(), cannot open file " << m_fileName << ". Reason " << data.error()
36                    << ".";
37        return false;
38    }
39
40    return true;
41}
42
43//---------------------------------------------------------------------------------------------------------------------
44bool VHPGLEngine::GenerateHPGL2(const QVector<VLayoutPiece> &details) const
45{
46    if (details.isEmpty())
47    {
48        qCritical()<<"VHPGLEngine::ExportToHPGL2(), details list is empty";
49        return false;
50    }
51
52    if (not m_size.isValid())
53    {
54        qCritical()<<"VHPGLEngine::ExportToHPGL2(), size is not valid";
55        return false;
56    }
57
58    QFile data(m_fileName);
59    if (data.open(QFile::WriteOnly | QFile::Truncate))
60    {
61        QTextStream out(&data);
62
63        // Generate HPGL/2 here
64
65        data.close();
66    }
67    else
68    {
69        qCritical() << "VHPGLEngine::ExportToHPGL2(), cannot open file " << m_fileName << ". Reason " << data.error()
70                    << ".";
71        return false;
72    }
73
74    return true;
75}

On next stage we will start generating HPGL code.

HPGL format[edit | edit source]

Few useful links:

Use one of HP-GL viewers you will find in the Internet to debug your images. I propose to use this HP-GL Viewer. Select version for Windows. Doesn't need installation.

Before we will start generating code we need to agree on some assumptions we will have.

  • We don't know which plotter a user has ans which options it supports. So, until then we will assume most common commands will work.
  • All mnemonics are in uppercase, ie. PU;SP1;LT;PU0,0;
  • As a separator we will use comma ','
  • As an instruction terminator we will use semicolon ';'

Process of plotting consist of three parts: header, plotting and footer.

Header section. Initialization of a plotter.

PU;
SP1;
LT;
PU0,0;

There are several approaches to define plotting. One of them is a series of pen up and pen down commands.

PU3936,55371;
PD3912,55417;
PD3912,55463;
PD3936,55509;
PD3982,55509;
PD4006,55463;
PD4006,55417;
PD3982,55371;
PD3936,55371;

All coordinates are integer numbers. Footer. Finishing plotting.

PU;
SP0;
PG;

We will plot everything in plotter coordinate system. 40 plotter units = 1 mm.

Implementing[edit | edit source]

As was mentioned above all coordinates must be integer numbers.

out.setRealNumberPrecision(0);

Plotting process

GenerateHPGLHeader(out);
ExportDetails(out, SortDetails(details));
GenerateHPGLFooter(out);

Basic method for writing HPGL command

void HPComand(QTextStream &out, const QString &mnemonic, const QString &parameters=QString()) const;
void VHPGLEngine::HPComand(QTextStream &out, const QString &mnemonic, const QString &parameters) const
{
    out << qPrintable(mnemonic + parameters) << ';';
    if (m_inserNewLine)
    {
        out << endl;
    }
}

I propose to define mnemonic literals in one place like this

namespace
{
// mnemonics
Q_GLOBAL_STATIC_WITH_ARGS(const QString, mPU, (QLatin1String("PU"))) // pen up

Pen up and pen down commands are our basic commands for plotting. You will need two methods for each type: one just with command, and one with coordinate (QPoint class). Keep track of current position with m_currentPos variable and if we already in this position skip the command. Users have option to setup an order in which pieces should be plotted.

public:
    static QList<VLayoutPiece> SortDetails(const QVector<VLayoutPiece> &details);
    
QList<VLayoutPiece> VHPGLEngine::SortDetails(const QVector<VLayoutPiece> &details)
{
    QList<VLayoutPiece> sorted;

    for (auto &detail : details)
    {
        if (detail.GetPriority() == 0 || sorted.isEmpty())
        {
            sorted.append(detail);
        }
        else
        {
            bool found = false;
            for (int i=0; i < sorted.size(); ++i)
            {
                if (detail.GetPriority() < sorted.at(i).GetPriority() || sorted.at(i).GetPriority() == 0)
                {
                    sorted.insert(i, detail);
                    found = true;
                    break;
                }
            }

            if (not found)
            {
                sorted.append(detail);
            }
        }
    }

    return sorted;
}

There is no particular way parts of a piece must be plotted. Possible order

void VHPGLEngine::ExportDetail(QTextStream &out, const VLayoutPiece &detail)
{
    PlotMainPath(out, detail);
    PlotSeamAllowance(out, detail);
    PlotInternalPaths(out, detail);
    PlotPlaceLabels(out, detail);
    PlotPassmarks(out, detail);
    PlotLabel(out, detail);
}

Plotting a seam line

void VHPGLEngine::PlotMainPath(QTextStream &out, const VLayoutPiece &detail)
{
    if (not detail.IsSeamAllowance() ||
        (detail.IsSeamAllowance() && not detail.IsSeamAllowanceBuiltIn() && not detail.IsHideMainPath()))
    {
        QVector<QPoint> points = ConvertPath(detail.GetMappedContourPoints());

        if (points.size() > 1 && points.first() != points.last())
        {
            points.append(points.first()); // must be closed
        }

        PlotPath(out, points);
    }
}

Plotting a seam allowance

void VHPGLEngine::PlotSeamAllowance(QTextStream &out, const VLayoutPiece &detail)
{
    QVector<QPoint> points = detail.IsSeamAllowance() && not detail.IsSeamAllowanceBuiltIn() ?
                             ConvertPath(detail.GetMappedSeamAllowancePoints()) :
                             ConvertPath(detail.GetMappedContourPoints());

    if (points.size() > 1 && points.first() != points.last())
    {
        points.append(points.first()); // must be closed
    }

    PlotPath(out, points);
}

Plotting internal paths

void VHPGLEngine::PlotInternalPaths(QTextStream &out, const VLayoutPiece &detail)
{
    const QTransform matrix = detail.GetMatrix();
    const QVector<VLayoutPiecePath> paths = detail.GetInternalPaths();
    for (auto &path : paths)
    {
        PlotPath(out, ConvertPath(matrix.map(path.Points())));
    }
}

Plotting placelabels

void VHPGLEngine::PlotPlaceLabels(QTextStream &out, const VLayoutPiece &detail)
{
    const QTransform matrix = detail.GetMatrix();
    const QVector<VLayoutPlaceLabel> placeLabels = detail.GetPlaceLabels();
    for(auto &pLabel : placeLabels)
    {
        for(auto &subShape: pLabel.shape)
        {
            PlotPath(out, ConvertPath(matrix.map(subShape)));
        }
    }
}

Plotting passmarks

void VHPGLEngine::PlotPassmarks(QTextStream &out, const VLayoutPiece &detail)
{
    QVector<VLayoutPassmark> passmarks = detail.GetPassmarks();
    for(auto &passmark : passmarks)
    {
        for(auto &subLine: passmark.lines)
        {
            HPPenUp(out, ConvertPoint(detail.GetMatrix().map(subLine.p1())));
            HPPenDown(out, ConvertPoint(detail.GetMatrix().map(subLine.p2())));
        }
    }
}

For now we miss two important methods. All our coordinates are in internal for the program coordinates. They must be converted to plotter coordinates.

namespace
{
Q_DECL_RELAXED_CONSTEXPR inline int ConvertPixels(double pix)
{
    // 40 plotter units = 1 mm
    return qRound(FromPixel(pix, Unit::Mm) * 40.);
}
}

QPoint VHPGLEngine::ConvertPoint(QPointF point) const
{
    point.setY(point.y() * -1 + m_size.height());

    return QPoint(ConvertPixels(point.x()), ConvertPixels(point.y()));
}

Plotting path

void VHPGLEngine::PlotPath(QTextStream &out, QVector<QPoint> path)
{
    if (path.size() < 2)
    {
        return;
    }

    path = OptimizePath(path, m_accuracyPointOnLine);

    HPPenUp(out, path.first());

    for (int i = 1; i < path.size(); ++i)
    {
        HPPenDown(out, path.at(i));
    }
}

As with all paths in the program we optimize paths to make them look nice and contain minimum points.

qreal        m_accuracyPointOnLine{1};

namespace
{
QVector<QPoint> RemoveDublicates(QVector<QPoint> points)
{
    if (points.size() < 3)
    {
        return points;
    }

    for (int i = 0; i < points.size()-1; ++i)
    {
        if (points.at(i) == points.at(i+1))
        {
            points.erase(points.begin() + i + 1);
            --i;
        }
    }

    return points;
}

//---------------------------------------------------------------------------------------------------------------------
QVector<QPoint> OptimizePath(QVector<QPoint> path, qreal accuracy)
{
    if (path.size() < 3)
    {
        return path;
    }

    path = RemoveDublicates(path);

    if (path.size() < 3)
    {
        return path;
    }

    int prev = -1;

    QVector<QPoint> cleared;
    //Remove point on line
    for (qint32 i = 0; i < path.size(); ++i)
    {
        if (prev == -1)
        {
            prev = (i == 0) ?  path.size() - 1 : i-1;
        }

        const int next = (i == path.size() - 1) ? 0 : i+1;

        const QPoint &iPoint = path.at(i);
        const QPoint &prevPoint = path.at(prev);
        const QPoint &nextPoint = path.at(next);

        // If RemoveDublicates does not remove these points it is a valid case.
        // Case where last point equal first point
        if (((i == 0 || i == path.size() - 1) && (iPoint == prevPoint || iPoint == nextPoint)) ||
            not VGObject::IsPointOnLineviaPDP(iPoint, prevPoint, nextPoint, accuracy))
        {
            cleared.append(iPoint);
            prev = -1;
        }
    }

    cleared = RemoveDublicates(cleared);

    return cleared;
}
}

Labels[edit | edit source]

There are two ways to define plotting labels: as paths or as plain text. This option controlled by DialogSaveLayout::IsTextAsPaths().

When user selects insert labels as paths instead of actual text inside of a file we must place image of the label. Number of lines or polygons that visualize the label. This approach has advantages and disadvantages. If a label defined this way a human still can read it, but a program no longer can. But this way allow full control over look. No longer need to worry about right fonts on the target machine.

HPGL format itself allows to define ASCII-label with mnemonic LB. And we should probably support this option.

Because plotting usually done with a pen, a plotter really limited in its capability of plotting arbitrary fonts. To improve plotting speed time used special single line fonts. Example of such we will use in our development. User has ability to define label text font. Use it to start working with this font.

There will be no ready for use code, instead we will discuss main idea behind plotting labels.

Piece has two methods you need to plot a label.

QPointF VLayoutPiece::GetPieceTextPosition() const;

QStringList VLayoutPiece::GetPieceText() const;

Here is example from DXF export

const QStringList list = detail.GetPieceText();
const QPointF startPos = detail.GetPieceTextPosition();

for (int i = 0; i < list.size(); ++i)
{
    QPointF pos(startPos.x(), startPos.y() - ToPixel(AAMATextHeight * m_yscale, varInsunits)*(list.size() - i-1));
    detailBlock->ent.push_back(AAMAText(pos, list.at(i), QChar('1')));
}

All we need is start position and height of text. This example doesn't show how to convert text into paths.

More interesting example can be found in VLayoutPiece class. Method

void VLayoutPiece::CreateLabelStrings(QGraphicsItem *parent, const QVector<QPointF> &labelShape, const VTextManager &tm, bool textAsPaths) const gives us with some clues.

We need to use QPainterPath class.

QPainterPath path;
path.addText(0, - static_cast<qreal>(fm.ascent())/6., fnt, qsText);

And then convert it to polygon. That's it, we have data to plot in plt file.

What's next?[edit | edit source]

  • Described implementation doesn't cover HPGL/2 format.
  • Plotting of internal paths doesn't take into account line type.
  • Probably it will be a good idea to separate implementations for HPGL and HPGL/2 formats in separate classes with one common parent class.

Development on this doesn't finish. Export code must be connected to dialogs. Man page must be improved etc.