Files
qelectrotech-source-mirror/tests/qttest/tst_printpdforientation.cpp
T
ispyisail f42958d2c2 PDF export: turn each page to its folio's orientation (#1480)
The print window exported every page in one orientation, landscape
unless changed, so a portrait folio was shrunk onto a landscape page.
With "Fit in page" on, a PDF export now turns each page to portrait or
landscape as its folio is drawn. The preview keeps one orientation
(QPrintPreviewWidget lays out every page at one paper size), and a
printer keeps it too (only Qt's PDF engine accepts a new page layout
once printing has started). The orientation chosen in the window is
put back afterwards, so it is still the one saved.

The cross-reference links took their target from painter->viewport(),
which QPainter sets once when it opens, so on a turned page they missed
their text. They now use the device's size, as render() does.

tst_printpdforientation exports through the window: an A3 landscape and
an A3 portrait folio get one page each way; tiled pages keep the chosen
orientation; and the links on examples/industrial.qet's turned pages
land where they do when the export starts portrait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:38:00 +13:00

204 lines
7.3 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include "qetapptest.h"
#include "print/projectprintwindow.h"
#include "qetproject.h"
#include <QFile>
#include <QPrinter>
#include <QRegularExpression>
#include <QTemporaryDir>
// The print window's PDF export turns each page to its folio (#1480). It
// used one orientation for the whole document, landscape unless changed,
// so a portrait folio was shrunk onto a landscape page.
// fixtures/pdf_page_a3.qet has an A3 landscape folio, then an A3 portrait
// one.
class tst_printpdforientation : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
struct Page
{
QSizeF size;
QList<QRectF> links;
};
// Each page's MediaBox and the Rect of each of its links, in page
// order. Qt writes every object uncompressed, a page's annotations as
// "/Annots n 0 R" pointing at an array of annotation references.
static QList<Page> pages(const QString &pdf_path)
{
QFile file(pdf_path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QString pdf = QString::fromLatin1(file.readAll());
QHash<int, QString> objects;
static const QRegularExpression object(
QStringLiteral(R"((\d+) 0 obj(.*?)endobj)"),
QRegularExpression::DotMatchesEverythingOption);
for (auto it = object.globalMatch(pdf); it.hasNext();) {
const auto m = it.next();
objects.insert(m.captured(1).toInt(), m.captured(2));
}
static const QRegularExpression ref(QStringLiteral(R"((\d+) 0 R)"));
static const QRegularExpression kids(
QStringLiteral(R"(/Type\s*/Pages.*?/Kids\s*\[(.*?)\])"),
QRegularExpression::DotMatchesEverythingOption);
static const QRegularExpression box(
QStringLiteral(R"(/MediaBox \[0 0 ([0-9.]+) ([0-9.]+)\])"));
static const QRegularExpression annots(QStringLiteral(R"(/Annots (\d+) 0 R)"));
static const QRegularExpression link(
QStringLiteral(R"(/Subtype /Link\s*/Rect \[\s*([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s*\])"));
QList<Page> result;
for (auto k = ref.globalMatch(kids.match(pdf).captured(1)); k.hasNext();) {
const QString page = objects.value(k.next().captured(1).toInt());
const auto b = box.match(page);
Page p;
p.size = QSizeF(b.captured(1).toDouble(), b.captured(2).toDouble());
const QString list = objects.value(annots.match(page).captured(1).toInt());
for (auto a = ref.globalMatch(list); a.hasNext();) {
const auto l = link.match(objects.value(a.next().captured(1).toInt()));
if (l.hasMatch())
p.links << QRectF(QPointF(l.captured(1).toDouble(), l.captured(2).toDouble()),
QPointF(l.captured(3).toDouble(), l.captured(4).toDouble()));
}
result << p;
}
return result;
}
// Export @p project as the window's "Export in pdf" button does, from
// an A3 landscape printer with fixed margins.
QList<Page> exportPdf(QETProject *project, bool fit_in_page)
{
const QString out = m_dir.filePath(QStringLiteral("out.pdf"));
QFile::remove(out);
QSettings().setValue("print/default/fitinpage", fit_in_page);
auto printer = new QPrinter();
printer->setPageSize(QPageSize(QPageSize::A3));
printer->setPageOrientation(QPageLayout::Landscape);
printer->setOutputFormat(QPrinter::PdfFormat);
printer->setOutputFileName(out);
ProjectPrintWindow window(project, printer);
// After the window loaded the margins an earlier export saved.
printer->setPageMargins(QMarginsF(7, 7, 7, 7), QPageLayout::Millimeter);
if (!QMetaObject::invokeMethod(&window, "print"))
return {};
QCoreApplication::processEvents(); // the deferred PDF post-processing
return pages(out);
}
// examples/industrial.qet, its folios made portrait (taller rows), all
// of them or all but the first.
QString portraitIndustrial(bool first_too)
{
QFile file(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
if (!file.open(QIODevice::ReadOnly))
return {};
QString xml = QString::fromUtf8(file.readAll());
static const QRegularExpression diagram(QStringLiteral("<diagram [^>]*>"));
static const QRegularExpression rows(QStringLiteral(R"(\brows="\d+")"));
QString result;
qsizetype last = 0;
int n = 0;
for (auto it = diagram.globalMatch(xml); it.hasNext();) {
const auto m = it.next();
QString tag = m.captured();
if (n++ > 0 || first_too)
tag.replace(rows, QStringLiteral("rows=\"20\""));
result += xml.mid(last, m.capturedStart() - last) + tag;
last = m.capturedEnd();
}
result += xml.mid(last);
const QString path = m_dir.filePath(
first_too ? QStringLiteral("portrait.qet") : QStringLiteral("mixed.qet"));
QFile out(path);
if (!out.open(QIODevice::WriteOnly))
return {};
out.write(result.toUtf8());
return path;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
}
void pagesFollowFolios()
{
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
QETProject project(fixture);
QCOMPARE(project.state(), QETProject::Ok);
QCOMPARE(project.diagrams().size(), 2);
const QList<Page> result = exportPdf(&project, true);
QCOMPARE(result.size(), 2);
const QSize a3 = QPageSize(QPageSize::A3).sizePoints();
QCOMPARE(result.at(0).size, QSizeF(a3.height(), a3.width())); // landscape
QCOMPARE(result.at(1).size, QSizeF(a3)); // portrait
}
// Without "Fit in page" a folio is spread over several pages, all in the
// orientation chosen in the window, as before.
void tiledPagesKeepChosenOrientation()
{
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
QETProject project(fixture);
QCOMPARE(project.state(), QETProject::Ok);
const QList<Page> result = exportPdf(&project, false);
QVERIFY(!result.isEmpty());
for (const Page &page : result)
QVERIFY2(page.size.width() > page.size.height(), "a tiled page is not landscape");
}
// The cross-reference links on a page turned during the export land
// where they do when the export starts on that orientation: the same
// project with every folio portrait.
void linksOnTurnedPages()
{
const QString mixed_path = portraitIndustrial(false);
const QString portrait_path = portraitIndustrial(true);
QVERIFY(!mixed_path.isEmpty() && !portrait_path.isEmpty());
QETProject mixed_project(mixed_path);
QCOMPARE(mixed_project.state(), QETProject::Ok);
const QList<Page> mixed = exportPdf(&mixed_project, true);
QETProject portrait_project(portrait_path);
QCOMPARE(portrait_project.state(), QETProject::Ok);
const QList<Page> portrait = exportPdf(&portrait_project, true);
QCOMPARE(mixed.size(), portrait.size());
QVERIFY(mixed.size() > 1);
QVERIFY(mixed.first().size.width() > mixed.first().size.height());
int links = 0;
for (int i = 1; i < mixed.size(); ++i) {
QVERIFY(mixed.at(i).size.height() > mixed.at(i).size.width());
QCOMPARE(mixed.at(i).links.size(), portrait.at(i).links.size());
for (int j = 0; j < mixed.at(i).links.size(); ++j) {
const QRectF a = mixed.at(i).links.at(j);
const QRectF b = portrait.at(i).links.at(j);
QVERIFY2(qAbs(a.left() - b.left()) < 0.01 && qAbs(a.top() - b.top()) < 0.01
&& qAbs(a.right() - b.right()) < 0.01
&& qAbs(a.bottom() - b.bottom()) < 0.01,
qPrintable(QStringLiteral("page %1 link %2 moved").arg(i + 1).arg(j)));
++links;
}
}
QVERIFY2(links > 100, "too few links to test");
}
};
QET_APP_TEST_MAIN(tst_printpdforientation)
#include "tst_printpdforientation.moc"