Search code examples
c++qtpdfprintingqwt

Can't generate pdf with acceptable output quality using Qt


I'm trying to generate a pdf using Qt5 under Windows. My document contains texts, images and charts. As I'm familiar with Qt and Qwt, I believed the best strategy was to create a QWidget with my document layout and simply print it. But I face problems and could not end up with an acceptable result.

Here is my MCVE, a simple page document with:

  • A header with title and image
  • A piece of text
  • A simple chart

Based on Qt document and How can I print a QWidget in Qt?, I ended up with this code:

main.cpp:

#include <QApplication>
#include <QIcon>
#include <QDesktopServices>
#include <QWidget>
#include <QPrinter>
#include <QPainter>
#include <QPagedPaintDevice>
#include <QUrl>
#include "ui_report.h"

#include "qwt_plot.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_canvas.h"
#include "qwt_point_data.h"
#include "qwt_legend.h"

#include <sstream>
#include <memory>

bool printWidget( QWidget& widget, bool highResolution, const std::string& fileName )
{
    QPrinter printer( highResolution ? QPrinter::HighResolution : QPrinter::ScreenResolution );

    printer.setOutputFormat(QPrinter::PdfFormat);
    printer.setOrientation(QPrinter::Portrait);
    printer.setPaperSize(QPrinter::A4);
    printer.setPageMargins(15,15,15,15,QPrinter::Millimeter);
    printer.setFullPage(true);
    printer.setOutputFileName(fromSDEString(fileName.c_str()));

    QPainter painter(&printer);

    double xscale = printer.pageRect().width()/double(widget.width());
    double yscale = printer.pageRect().height()/double(widget.height());
    double scale = qMin(xscale, yscale);
    painter.translate(printer.paperRect().x() + printer.pageRect().width()/2,
                      printer.paperRect().y() + printer.pageRect().height()/2);
    painter.scale(scale, scale);
    painter.translate(-widget.width()/2, -widget.height()/2);

    widget.render(&painter, QPoint(), QRegion(), QWidget::DrawChildren);

    return painter.end();
}

bool generateReport( bool drawWithPrinterResolution, bool printHighResolution, const std::string& fileName )
{
    QWidget widget;
    Ui::Report ui;
    ui.setupUi( &widget );

    if ( drawWithPrinterResolution )
    {
        QPrinter printer( printHighResolution ? QPrinter::HighResolution : QPrinter::ScreenResolution );

        printer.setOrientation(QPrinter::Portrait);
        printer.setPaperSize(QPrinter::A4);
        printer.setPageMargins(15,15,15,15,QPrinter::Millimeter);
        printer.setFullPage(true);

        // force printer page size to be used:
        QSize pageSize = printer.pageRect().size();
        widget.resize( pageSize );
    }

    ui.header->setFrameShape( QFrame::Shape::Box );

    QHBoxLayout* headerLayout = new QHBoxLayout(ui.header);

    QLabel* icon = new QLabel(ui.header);
    QSize size = ui.header->size();
    icon->setPixmap( QPixmap( ":/gui_test/mainframe.png" ).scaledToHeight( size.height() ) );

    headerLayout->addWidget( icon );
    headerLayout->addWidget( new QLabel("Document title",ui.header) );

    headerLayout->setStretch( 0, 0 );
    headerLayout->setStretch( 1, 1 );

    ui.inputs->setText( "<b>Info</b>: Information" );

    QwtPlot* plot = new QwtPlot( &widget );

    QwtPlotCurve* curve = new QwtPlotCurve("Plots");
    curve->setStyle( QwtPlotCurve::Lines );

    QVector<QPointF> samples;
    for ( size_t i = 0; i != 100; ++i )
    {
        samples.push_back(QPointF(i,20*i+10));
    }
    curve->setData(new QwtPointSeriesData(samples));

    curve->attach(plot);
    plot->setTitle( "Result" );
    plot->setAxisScale( QwtPlot::xBottom, samples.front().rx(), samples.back().rx() );

    plot->replot();

    ui.graphLayout->addWidget( plot );

    if ( printWidget( widget, printHighResolution, fileName ) )
    {
        QDesktopServices::openUrl(QUrl::fromLocalFile(fileName.c_str()));
        return true;
    }
    else
    {
        return false;
    }
}

int main( int argc, char* argv[] )
{
    QApplication app( argc, argv );
    app.setWindowIcon( QIcon( ":/gui_test/mainframe.png" ) );

    generateReport( false, false, "report_small_widget_to_screen.pdf" );
    generateReport( false, true, "report_small_widget_to_high.pdf" );
    generateReport( true, false, "report_big_widget_to_screen.pdf" );
    generateReport( true, true, "report_big_widget_to_high.pdf" );

    return 0;
}

report.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Report</class>
 <widget class="QWidget" name="Report">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>525</width>
    <height>742</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,100,0">
   <item>
    <widget class="QFrame" name="header"/>
   </item>
   <item>
    <widget class="QLabel" name="inputs">
     <property name="text">
      <string>TextLabel</string>
     </property>
    </widget>
   </item>
   <item>
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>40</height>
      </size>
     </property>
    </spacer>
   </item>
   <item>
    <layout class="QVBoxLayout" name="graphLayout"/>
   </item>
   <item>
    <widget class="QWidget" name="footer" native="true"/>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

mainframe.png: A Qt icon of 256x256 pixels: http://icons.iconarchive.com/icons/alecive/flatwoken/256/Apps-Qt-icon.png.

As you can see, this generates 4 files:

  • report_small_widget_to_screen.pdf: Where widget is created with a small A4 ratio from ui file (252x742) and then printed with QPrinter::ScreenResolution
  • report_small_widget_to_high.pdf: Where widget is created with a small A4 ratio from ui file (252x742) and then printed with QPrinter::HighResolution
  • report_big_widget_to_screen.pdf: Where widget is scaled to printer's page size (793x1123) and then printed with QPrinter::ScreenResolution
  • report_big_widget_to_high.pdf: Where widget is scaled to printer's page size (9917x14033) and then printed with QPrinter::HighResolution

None gives me an acceptable result:

  • report_small_widget_to_screen.pdf: Text, Qt icon and Qwt chart are pixelated
  • report_small_widget_to_high.pdf: Text is OK, but Qt icon and Qwt chart are pixelated
  • report_big_widget_to_screen.pdf: Text, Qt icon and Qwt chart are pixelated
  • report_big_widget_to_high.pdf: Text, Qt icon and Qwt chart are so small they are hardly readable. But now Qwt plot is not pixelated anymore

What should I change to get a nice output?

  • With text drawn in high resolution (not pixelated) as in report_small_widget_to_high.pdf
  • With icon being drawn with high resolution (not pixelated)
  • With chart being plotted with high resolution (not pixelated)

report_small_widget_to_screen.pdf looks like (everything is pixelated):

enter image description here

report_small_widget_to_high.pdf looks like (only text is not pixelated):

enter image description here

report_big_widget_to_high.pdf looks like (everything is too small):

enter image description here


Note: I just ran the same tests with a bigger .ui ressource (2100x2970), then image resolution looks better, but text appears very small. I'm wondering the QWidget printing is the appropriate solution here, because it looks like text size depends on ui size, so you can't control the size of your text (as you do with font sizes in a document like Word...)


Solution

  • Personally I would strongly consider creating the content as a QTextDocument and then printing it. You can (IIRC, haven't used QwtPlot in a while) render the charts to images. Then it is easy to add the image to the document wherever you want. Same with your static image(s), of course. You can use HTML/CSS in the document (despite "Text" in the class name), and lots of other options.

    Example added on request:

    This is going to be very simplified and not fully formed, but hope it helps...

    QTextDocument qtdoc;  // start with a QTextDocument
    
    // prepare standard html with embedded image
    QString html = "<html><body>" \
        "<h1>Hello World!</h1>" \
        "<img src='mydata://myimg.png' border='0' />" \
    "</body></html>";  
    
    QImage image = getChartImage();  // theoretical function
    
    // here we add the actual image data to the document
    qtdoc.addResource(QTextDocument::ImageResource, QUrl("mydata://myimg.png"), image);
    
    qtdoc.setHtml(html);
    // document is now fully formed and ready for display/print
    

    To print to a PDF or HTML file:

    void printToFile(const QTextDocument & qtdoc)
    {
      QString fn = QFileDialog::getSaveFileName(this, tr("Select output file"), QString(), tr("PDF Files(*.pdf);;HTML-Files (*.htm *.html)"));
      if (fn.isEmpty())
        return;
      if (fn.endsWith(".pdf", Qt::CaseInsensitive)) {
        QPrinter printer;
        printer.setPageMargins(10.0,10.0,10.0,10.0,printer.Millimeter);
        printer.setOutputFormat(QPrinter::PdfFormat);
        printer.setColorMode(QPrinter::Color);
        printer.setOutputFileName(fn);
        qtdoc.print(&printer);
      }
      else {  // HTML
        QTextDocumentWriter writer(fn);
        writer.write(qtdoc);
      }
    }
    

    Output to a printer:

    void print(const QTextDocument & qtdoc)
    {
      QPrinter printer;
      printer.setPageMargins(10.0,10.0,10.0,10.0,printer.Millimeter);
      QPrintDialog *dialog = new QPrintDialog(&printer, this);
      dialog->setWindowTitle(tr("Print Document"));
      if (dialog->exec() != QDialog::Accepted)
        return;
      qtdoc.print(&printer);
    }
    

    An actual screenshot of a working version (code for it starts here but is a bit of a chore to follow):

    QTextDocument with embedded images

    And the PDF export: http://max.wdg.us/docs/so/SO-47879329.pdf