4

I am trying to print the contents of a QGraphicsScene. The target printer could be anything - from the normal printers to custom size special printers. It must print things at actual size (inches, mm....).
In the QGraphicsScene I am using the assumption of 72 ppi.

I assumed that:
1) rendering the scene to a printer would do it based on printer resolution, so that I would get items at actual size (inches/mm) similar to what they show on screen.
2) I can set the paper size for the printer to the desired canvas size (which is a rectangle on a very large scene) and nothing beyond it would print
3) I can set margins, and the contents outside the "actual canvas" will not be printed, including what is on margins.

All my assumptions so far are wrong:
1) for different printers, it seems the rendering is for max fit (using aspect ratio), if I suggest a custom size close to its default paper size (or if I don't set a paper size);
If I set a paper size that is not close (like 4x4 inch on a printer with default "LETTER" size) it just prints a blank page.
2-3) In the case where there is a print, and the printer just stretches the canvas to its full page, any items that are outside the drawing area are still printed.
I tried to clip, either on the painter or by setting the target rectangle on render, and the result was very odd clipping of a small section of the scene.

I have tried on HP LaserJet, Adobe PDF, and some custom printers with specific sizes (like 4x6 inch). They all scale the scene to the max size based on whether I specify Portrait or Landscape, and completely ignore my paper size request or the actual sizes.

Here is a small sample program to reproduce what I am trying to do.
The comments in the code show some options I tried.

#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QGraphicsEllipseItem>
#include <QPrinter>
#include <QPrintDialog>


int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QGraphicsScene* s = new QGraphicsScene();
    s->setSceneRect(-500, -500, 1500, 1500);
    QGraphicsView* view = new QGraphicsView();
    view->setScene(s);
    view->show();

    int canvasSize = 288;    // 4 in
    QRectF canvasRect(0, 0, canvasSize, canvasSize);
    // this is to show actual scene
    QGraphicsRectItem* sss = new QGraphicsRectItem(canvasRect);
    sss->setBrush(Qt::blue);
    s->addItem(sss);
    // this item is partially outside top left
    QGraphicsEllipseItem* e1 = new QGraphicsEllipseItem(-50, -75, 100, 150);
    e1->setBrush(Qt::yellow);
    s->addItem(e1);
    // this item is partially outside center
    QGraphicsEllipseItem* e2 = new QGraphicsEllipseItem(100, 150, 250, 50);
    e2->setBrush(Qt::yellow);
    s->addItem(e2);
    // this item is partially outside right
    QGraphicsEllipseItem* e3 = new QGraphicsEllipseItem(200, 200, 75, 125);
    e3->setBrush(Qt::yellow);
    s->addItem(e3);

    QPrinter printer;
    // QPrinter printer(QPrinter::HighResolution);  // this makes no difference except it rotates the output, strange

    // without this just to use default printer, if you like
    QPrintDialog printDialog(&printer);
    if (printDialog.exec() != QDialog::Accepted)
        return 1;

    printer.setFullPage(false); // I see no diference between true and false

    // this results in empty page (or is ignored if my rect is 8 in)
    //printer.setPaperSize(canvasRect, QPrinter::Point);

    printer.setOrientation(QPrinter::Landscape);
    printer.setPageMargins(0, 0, 0, 0, QPrinter::Point);

    QPainter painter;

    if (painter.begin(&printer))
    {
//        painter.setClipRect(canvasRect);  // this creates a small clipping, only a tiny corner
        s->render(&painter, QRectF(), canvasRect, Qt::KeepAspectRatio);
        // doing this instead clips to a tiny rectangle also
//        s->render(&painter, canvasRect, canvasRect, Qt::KeepAspectRatio);
        painter.end();
    }

    return app.exec();
}

Doing:

QPrinter printer(QPrinter::HighResolution);
qreal resolutionFactor = printer.resolution() / 1200.;
...
painter.scale(resolutionFactor, resolutionFactor);

fixes the LaserJet print (the scaling - not the painting outside the actual page) - but results into a tiny almost invisible print on a printer with 300 dpi resolution.

How can I get the printed output to be to actual scale (so that I can measure inches/mm on paper and have them be correct) ?

Also how can I get the output to be clipped to the actual canvas rectangle ?

Thalia
  • 13,637
  • 22
  • 96
  • 190
  • 1
    `dpi` is a conversion factor. It's not a physical measurement. it's like asking "how far is it to drive to New York" and you answer "60 mph". – Marc B Jun 08 '16 at 16:52
  • @MarcB I meant PPI (Pixels per inch - or Points per inch ?) - I am drawing on scene items of size that is 72*inch size. The `QPrinter` seems to agree, if I do `printer.setPaperSize(canvasRect, QPrinter::Point);` and `qDebug() << printer.paperSize(QPrinter::Inch);` it gives me the expected size. – Thalia Jun 08 '16 at 17:02
  • The units in the scene are not pixels, so speaking of PPI makes no sense. You can simply say that your scene units are 1/72 inch. – Kuba hasn't forgotten Monica Jun 08 '16 at 18:30
  • I appreciate your self-contained, to-the-point test cases. Kudos and big thanks! – Kuba hasn't forgotten Monica Jun 08 '16 at 20:22

1 Answers1

5

It's all really simple. The render method does two things only:

  1. It maps from the source rectangle, in scene units, to a target rectangle, in device units.
  2. It draws within the target rectangle only.

Your mistake was passing a null target rectangle: there's no effective clipping then (it clips to device size), and you're printing at a wrong scaling as well unless your scene happens to be exactly the same size as the device size.

The DPI mapping between device units and inches is given by QPrinter::resolution, in terms of DPI (device units per inch).

To print the canvasRect at the correct scale, within and clipped to a chosen page rectangle, do the following, where in is 1 inch in scene units (72.0f in your case):

auto source = canvasRect;
auto scale = printer.resolution()/in;
auto page = printer.pageRect(QPrinter::DevicePixel);
auto target = QRectF(page.topLeft(), source.size()*scale);
target &= page; // clip target rect to page
qDebug() << page << scale << source << target;
scene.render(&painter, target, source);

Printer device units seem rectangular in Qt, but perhaps that's because I haven't tried on wierd enough devices. In case they were not rectangular, you could deduce them from the output of pageRect:

qreal resolution(QPrinter & printer, Qt::Orientation orientation) {
  auto in = printer.pageRect(QPrinter::Inch);
  auto dev = printer.pageRect(QPrinter::DevicePixel);
  return (orientation == Qt::Horizontal) ? dev.width()/in.width()
         : dev.height()/in.height();
}
...
auto scaleX = resolution(printer, Qt::Horizontal);
auto scaleY = resolution(printer, Qt::Vertical);
auto target = QRectF(page.left(), page.top(),
                     source.width()*scaleX, source.height()*scaleY);
...

Complete example follows. The output is identical no matter what is the value of in, since we use an explicit, non-cosmetic pen for the outlines of the shapes. There's no reason to set in to any particular value, if your natural units are inches then simply set in=1.0f.

// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-print-37708423
#include <QtWidgets>
#include <QtPrintSupport>

int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   QGraphicsScene scene;
   QGraphicsView view(&scene);

   auto in = 72.0f;
   auto pen = QPen(Qt::black, 0.01*in);
   QRectF canvasRect(0, 0, 4*in, 4*in);
   // this is to show actual scene
   QGraphicsRectItem sss(canvasRect);
   sss.setPen(pen);
   sss.setBrush(Qt::blue);
   scene.addItem(&sss);
   // this item is partially outside top left
   QGraphicsEllipseItem e1(-0.5*in, -0.5*in, 1*in, 1*in);
   e1.setPen(pen);
   e1.setBrush(Qt::yellow);
   scene.addItem(&e1);
   // this item is partially outside center
   QGraphicsEllipseItem e2(2*in, 2*in, 2.5*in, 1*in);
   e2.setPen(pen);
   e2.setBrush(Qt::yellow);
   scene.addItem(&e2);
   // this item is partially outside right
   QGraphicsEllipseItem e3(3.5*in, 3.5*in, 1*in, 1*in);
   e3.setPen(pen);
   e3.setBrush(Qt::yellow);
   scene.addItem(&e3);

   view.fitInView(scene.sceneRect(), Qt::KeepAspectRatio);
   view.show();

   QPrinter printer;
   QPrintDialog printDialog(&printer);
   QObject::connect(&printDialog, &QDialog::accepted, [&]{
      printer.setOrientation(QPrinter::Landscape);
      QPainter painter(&printer);

      auto source = canvasRect;
      auto scale = printer.resolution()/in;
      auto page = printer.pageRect(QPrinter::DevicePixel);
      auto target = QRectF(page.topLeft(), source.size()*scale);
      target &= page; // clip target rect to page
      qDebug() << page << scale << source << target;
      scene.render(&painter, target, source);
   });
   printDialog.show(); // modal on OS X thus must follow `connect` above
   return app.exec();
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • This is great, including the different horizontal and vertical resolution ! Is there a way to also tell the printer what paper size to use, without risking that it doesn't recognize it and prints a blank page ? – Thalia Jun 08 '16 at 21:51
  • @Thalia When `printer.setPageSize(QPageSize::A4))` (e.g.) returns true, than you know that it succeeded. You can set a default page size, then show the print dialog but expect that it might change as the user should be able to change it and the change shouldn't be ignored. You don't care what the page size is, it's whatever the user has set, and `pageRect` reflects the chosen page size. – Kuba hasn't forgotten Monica Jun 08 '16 at 22:01
  • This assumes that I can set paper size from an enum - but for custom printers, that is not the case... Especially for continuous roll-fed. I am trying to tell the printer what the paper size is (or at least suggest it to the print dialog). – Thalia Jun 08 '16 at 22:28
  • @Thalia You can set a custom paper size, too. `QPageSize` accepts custom sizes. – Kuba hasn't forgotten Monica Jun 09 '16 at 03:08