mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-14 17:44:13 +02:00
Merge pull request #1481 from ispyisail/fix/pdf-orientation-per-folio
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m39s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m39s
Add per-folio page orientation to the PDF export (#1480)
This commit is contained in:
@@ -232,6 +232,20 @@ void ProjectPrintWindow::requestPaint()
|
||||
}
|
||||
}
|
||||
|
||||
// A PDF export turns each page to its folio's orientation (#1480): a
|
||||
// portrait folio gets a portrait page instead of being shrunk onto a
|
||||
// landscape one. Only the real export, with one page per folio: the
|
||||
// preview draws every page at one paper size, and only Qt's PDF engine
|
||||
// accepts a new page layout once printing has started.
|
||||
const bool per_folio_orientation = m_printing
|
||||
&& m_printer->outputFormat() == QPrinter::PdfFormat
|
||||
&& ui->m_fit_in_page_cb->isChecked();
|
||||
const auto chosen_orientation = m_printer->pageLayout().orientation();
|
||||
const auto diagrams = selectedDiagram();
|
||||
if (per_folio_orientation && !diagrams.isEmpty()) {
|
||||
setOrientationForDiagram(diagrams.first());
|
||||
}
|
||||
|
||||
bool first = true;
|
||||
QPainter painter(m_printer);
|
||||
int annotIndex = 0;
|
||||
@@ -245,12 +259,23 @@ void ProjectPrintWindow::requestPaint()
|
||||
// should be fixed by original author by evaluating function-result!
|
||||
(void)pdfExport;
|
||||
|
||||
for (auto diagram : selectedDiagram())
|
||||
for (auto diagram : diagrams)
|
||||
{
|
||||
first ? first = false : m_printer->newPage();
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
if (per_folio_orientation) {
|
||||
setOrientationForDiagram(diagram);
|
||||
}
|
||||
m_printer->newPage();
|
||||
}
|
||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
|
||||
}
|
||||
|
||||
if (per_folio_orientation) {
|
||||
m_printer->setPageOrientation(chosen_orientation);
|
||||
}
|
||||
|
||||
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
||||
// We are inside the paintRequested slot: the QPrintPreviewWidget still
|
||||
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
|
||||
@@ -259,6 +284,19 @@ void ProjectPrintWindow::requestPaint()
|
||||
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ProjectPrintWindow::setOrientationForDiagram
|
||||
* Turn the next page to portrait or landscape, as @diagram is drawn.
|
||||
* @param diagram
|
||||
*/
|
||||
void ProjectPrintWindow::setOrientationForDiagram(Diagram *diagram)
|
||||
{
|
||||
const QRect rect = diagramRect(diagram, exportProperties());
|
||||
m_printer->setPageOrientation(rect.height() > rect.width()
|
||||
? QPageLayout::Portrait
|
||||
: QPageLayout::Landscape);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ProjectPrintWindow::printDiagram
|
||||
* Print @diagram on the @printer
|
||||
@@ -356,8 +394,12 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
||||
// est revenu a l'identite ici. On reconstruit DONC explicitement la
|
||||
// transform appliquee par :
|
||||
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
|
||||
// cible vide => painter->viewport() ; source = diagram_rect ; centre.
|
||||
const QRectF target = QRectF(painter->viewport());
|
||||
// cible vide => taille du device ; source = diagram_rect ; centre.
|
||||
// The target render() used for an empty one: the device's
|
||||
// size. Not painter->viewport(), which is set once when the
|
||||
// painter opens and misses a page turned since (#1480).
|
||||
const QRectF target(0, 0, painter->device()->width(),
|
||||
painter->device()->height());
|
||||
const QRectF source = QRectF(diagram_rect); // meme source que render()
|
||||
|
||||
// render() ANCRE en haut-gauche (pas de centrage) :
|
||||
@@ -852,8 +894,10 @@ void ProjectPrintWindow::print()
|
||||
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
|
||||
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
|
||||
|
||||
m_printing = true;
|
||||
m_preview->print(); // triggers requestPaint() synchronously; painter
|
||||
// is created/destroyed inside that call
|
||||
m_printing = false;
|
||||
|
||||
savePageSetupForCurrentPrinter();
|
||||
savePrintProperties();
|
||||
|
||||
@@ -96,6 +96,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
void saveReloadDiagramParameters(Diagram *diagram, const ExportProperties &options, bool save);
|
||||
QList<Diagram *> selectedDiagram() const;
|
||||
void exportToPDF();
|
||||
void setOrientationForDiagram(Diagram *diagram);
|
||||
|
||||
|
||||
private:
|
||||
@@ -106,6 +107,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
QColor m_backup_diagram_background_color;
|
||||
QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
|
||||
QList<PdfLinks::ComponentInfo> m_componentInfoList;
|
||||
bool m_printing = false;
|
||||
};
|
||||
|
||||
#endif // PROJECTPRINTWINDOW_H
|
||||
|
||||
@@ -748,6 +748,13 @@ target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_pdfpagesize PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The print window's PDF export turns each page to its folio when "Fit in
|
||||
# page" is on (#1480), and keeps the chosen orientation when it is off. The
|
||||
# links on a turned page are checked on examples/industrial.qet.
|
||||
qet_add_app_test(tst_printpdforientation tst_printpdforientation.cpp)
|
||||
target_compile_definitions(tst_printpdforientation PRIVATE
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# --export-pdf writes the cross-reference links in the same order in every
|
||||
# run, rather than in Element pointer order. Exports examples/industrial.qet
|
||||
# several times, in separate processes.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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"
|
||||
Reference in New Issue
Block a user