From 3f96675c8faa0738fa41ab0bbc0e3ff73b6f5288 Mon Sep 17 00:00:00 2001
From: ale rimoldi <ale@graphicslab.org>
Date: Sun, 15 Feb 2026 20:42:00 +0100
Subject: scripter command to export preflight information


diff --git a/scribus/plugins/scriptplugin/cmddoc.cpp b/scribus/plugins/scriptplugin/cmddoc.cpp
index 00b63cd7e..ef858d963 100644
--- a/scribus/plugins/scriptplugin/cmddoc.cpp
+++ b/scribus/plugins/scriptplugin/cmddoc.cpp
@@ -6,6 +6,7 @@ for which a new license (GPL+exception) is in place.
 */
 #include "cmddoc.h"
 #include "cmdutil.h"
+#include "documentchecker.h"
 #include "documentinformation.h"
 #include "pyesstring.h"
 #include "scribuscore.h"
@@ -14,6 +15,11 @@ for which a new license (GPL+exception) is in place.
 #include "units.h"
 
 #include <QApplication>
+#include <qdebug.h>
+#include <qjsonarray.h>
+#include <qjsondocument.h>
+#include <qjsonobject.h>
+#include <qobject.h>
 
 /*
 newDocument(size, margins, orientation, firstPageNumber,
@@ -562,6 +568,172 @@ PyObject* scribus_applymasterpage(PyObject* /* self */, PyObject* args)
 	Py_RETURN_NONE;
 }
 
+PyObject* scribus_exportdocumentcheck(PyObject* /* self */, PyObject* args, PyObject* kw)
+{
+	PyESString targetFilenameArg;
+	PyESString checkProfileNameArg;
+	bool showNonPrintingLayerErrors = false;
+	char *kwargs[] = {const_cast<char*>("jsonFilename"), const_cast<char*>("checkProfile"),
+		const_cast<char*>("nonPrintingLayers"), nullptr};
+	if (!PyArg_ParseTupleAndKeywords(args, kw, "|es$esp", kwargs,
+			"utf-8", targetFilenameArg.ptr(), "utf-8", checkProfileNameArg.ptr(),
+			&showNonPrintingLayerErrors))
+		return nullptr;
+
+	if (!checkHaveDocument())
+		return nullptr;
+	const QString targetFileName(targetFilenameArg.c_str());
+	const QString checkProfileName(checkProfileNameArg.c_str());
+
+	ScribusDoc* currentDoc = ScCore->primaryMainWindow()->doc;
+
+	if (checkProfileName.isEmpty())
+		DocumentChecker::checkDocument(currentDoc);
+	else
+		DocumentChecker::checkDocument(currentDoc, checkProfileName);
+
+	// "Standard" Errors
+	// (Taken from PreflightError in scribusstruct.h)
+	QMap<PreflightError, QString> errorsList = {
+		{PreflightError::MissingGlyph, "MissingGlyph"},
+		{PreflightError::TextOverflow, "TextOverflow"},
+		{PreflightError::ObjectNotOnPage, "ObjectNotOnPage"},
+		{PreflightError::MissingImage, "MissingImage"},
+		{PreflightError::ImageDPITooLow, "ImageDPITooLow"},
+		{PreflightError::Transparency, "Transparency"},
+		{PreflightError::PDFAnnotField, "PDFAnnotField"},
+		{PreflightError::PlacedPDF, "PlacedPDF"},
+		{PreflightError::ImageDPITooHigh, "ImageDPITooHigh"},
+		{PreflightError::ImageIsGIF, "ImageIsGIF"},
+		{PreflightError::BlendMode, "BlendMode"},
+		{PreflightError::WrongFontInAnnotation, "WrongFontInAnnotation"},
+		{PreflightError::NotCMYKOrSpot, "NotCMYKOrSpot"},
+		{PreflightError::DeviceColorsAndOutputIntent, "DeviceColorsAndOutputIntent"},
+		{PreflightError::FontNotEmbedded, "FontNotEmbedded"},
+		{PreflightError::EmbeddedFontIsOpenType, "EmbeddedFontIsOpenType"},
+		{PreflightError::OffConflictLayers, "OffConflictLayers"},
+		{PreflightError::PartFilledImageFrame, "PartFilledImageFrame"},
+		{PreflightError::MarksChanged, "MarksChanged"},
+		{PreflightError::AppliedMasterDifferentSide, "AppliedMasterDifferentSide"},
+		{PreflightError::EmptyTextFrame, "EmptyTextFrame"},
+		{PreflightError::ImageHasProgressiveEncoding, "ImageHasProgressiveEncoding"},
+	};
+	// Custom Errors
+	// "DocumentModifiedAfterMarksUpdate"
+
+	QJsonObject json;
+
+	if (currentDoc->notesChanged())
+	{
+		json["marks"] = QJsonObject{{"", "DocumentModifiedAfterMarksUpdate"}};
+	}
+	else
+	{
+		json["marks"] = QJsonObject{};
+	}
+
+	QJsonArray jsonLayers;
+	for (const auto& [layerId, layerErrors]: currentDoc->docLayerErrors.asKeyValueRange())
+	{
+		for (const auto& [key, errorLevel]: layerErrors.asKeyValueRange())
+		{
+			jsonLayers.push_back(QJsonObject{{"layer", currentDoc->layerName(layerId)}, {"error", errorsList[key]}});
+		}
+	}
+	json["layers"] = jsonLayers;
+
+	QMap<int, QVector<QPair<QString, QString>>> pagesWithErrors;
+
+	for (auto [pageItem, itemError]: currentDoc->masterItemErrors.asKeyValueRange())
+	{
+		const int pageNumber = pageItem->OwnPage;
+		for (auto [errorCode, errorLevel]: itemError.asKeyValueRange())
+		{
+			if (!showNonPrintingLayerErrors && !currentDoc->layerPrintable(pageItem->m_layerID))
+				continue;
+
+			if (!pagesWithErrors.contains(pageNumber))
+				pagesWithErrors[pageNumber] = {};
+
+			pagesWithErrors[pageNumber].push_back({pageItem->itemName(), errorsList[errorCode]});
+		}
+	}
+
+	QJsonObject jsonMasterPages;
+	for (auto [pageNumber, value]: pagesWithErrors.asKeyValueRange())
+	{
+		QJsonArray pageItems;
+		for (const auto& [item, error]: value)
+		{
+			pageItems.push_back(QJsonObject{{{"item", item}, {"error", error}}});
+		}
+		jsonMasterPages[currentDoc->MasterPages.at(pageNumber)->pageName()] = pageItems;
+	}
+	json["masterPages"] = jsonMasterPages;
+
+
+	pagesWithErrors.clear();
+
+	for (auto [pageNumber, pageErrors]: currentDoc->pageErrors.asKeyValueRange())
+	{
+		pagesWithErrors[pageNumber] = {};
+
+		for (auto [errorCode, value]: pageErrors.asKeyValueRange())
+		{
+			pagesWithErrors[pageNumber].push_back({"", errorsList[errorCode]});
+		}
+	}
+	QJsonArray jsonFreeItems;
+	for (auto [pageItem, itemErrors]: currentDoc->docItemErrors.asKeyValueRange())
+	{
+		if (!showNonPrintingLayerErrors && !currentDoc->layerPrintable(pageItem->m_layerID))
+			continue;
+		const int pageNumber = pageItem->OwnPage;
+		if (!pagesWithErrors.contains(pageNumber))
+			pagesWithErrors[pageNumber] = {};
+		for (auto [errorCode, errorLevel]: itemErrors.asKeyValueRange())
+		{
+			pagesWithErrors[pageNumber].push_back({pageItem->itemName(), errorsList[errorCode]});
+		}
+		if (currentDoc->OnPage(pageItem) == -1)
+		{
+			jsonFreeItems.push_back(pageItem->itemName());
+		}
+	}
+
+	QJsonObject jsonPages;
+	for (auto [pageNumber, value]: pagesWithErrors.asKeyValueRange())
+	{
+		QJsonArray pageItems;
+		for (const auto& [item, error]: value)
+		{
+			pageItems.push_back(QJsonObject{{{"item", item}, {"error", error}}});
+		}
+		jsonPages[QString::number(pageNumber + 1)] = pageItems;
+	}
+	json["pages"] = jsonPages;
+
+	json["freeItems"] = jsonFreeItems;
+
+	currentDoc->pageErrors.clear();
+	currentDoc->docItemErrors.clear();
+	currentDoc->masterItemErrors.clear();
+	currentDoc->docLayerErrors.clear();
+
+	if (targetFileName.isEmpty())
+		return PyUnicode_FromString(QJsonDocument(json).toJson());
+
+	QFile saveFile(targetFileName);
+	if (!saveFile.open(QIODevice::WriteOnly))
+	{
+		PyErr_SetString(ScribusException, QObject::tr("Failed to open the file '%1' for writing","python error").arg(targetFileName).toUtf8().constData());
+		return nullptr;
+	}
+	saveFile.write(QJsonDocument(json).toJson());
+
+	Py_RETURN_NONE;
+}
+
 /*! HACK: this removes "warning: 'blah' defined but not used" compiler warnings
 with header files structure untouched (docstrings are kept near declarations)
 PV */
diff --git a/scribus/plugins/scriptplugin/cmddoc.h b/scribus/plugins/scriptplugin/cmddoc.h
index 8a01a35e5..6638b2a0c 100644
--- a/scribus/plugins/scriptplugin/cmddoc.h
+++ b/scribus/plugins/scriptplugin/cmddoc.h
@@ -385,6 +385,49 @@ Apply master page masterPageName on page pageNumber.\n\
 "));
 PyObject* scribus_applymasterpage(PyObject* self, PyObject* args);
 
+PyDoc_STRVAR(scribus_exportdocumentcheck__doc__,
+QT_TR_NOOP("exportDocumentCheck([jsonFilePath, checkProfile=\"\", nonPrintingLayers=False])\n\
+\n\
+Export the result of the preflight verifier into a JSON string or a JSON file.\n\
+\n\
+jsonFilePath: the path to the json file to be written. If empty, a JSON string \n\
+is returned.\n\
+checkProfileName is the name of an existing preflight verifier profile.\n\
+\n\
+The resulting JSON always has five sections:\n\
+- freeItems: a list of page items that are in no page;\n\
+- layers: a list of errors in layers;\n\
+- marks: errors relating the the marks;\n\
+- masterPages: list of erroneous page items for each master page;\n\
+- pages: list of erroneous page items for each page.\n\
+\n\
+The errors are strings, mostly names from the PreflightError enum in scribusstruct.h:\n\
+- MissingGlyph\n\
+- TextOverflow\n\
+- ObjectNotOnPage\n\
+- MissingImage\n\
+- ImageDPITooLow\n\
+- Transparency\n\
+- PDFAnnotField\n\
+- PlacedPDF\n\
+- ImageDPITooHigh\n\
+- ImageIsGIF\n\
+- BlendMode\n\
+- WrongFontInAnnotation\n\
+- NotCMYKOrSpot\n\
+- DeviceColorsAndOutputIntent\n\
+- FontNotEmbedded\n\
+- EmbeddedFontIsOpenType\n\
+- OffConflictLayers\n\
+- PartFilledImageFrame\n\
+- MarksChanged\n\
+- AppliedMasterDifferentSide\n\
+- EmptyTextFrame\n\
+- ImageHasProgressiveEncoding\n\
+- DocumentModifiedAfterMarksUpdate\n\
+"));
+PyObject* scribus_exportdocumentcheck(PyObject* self, PyObject* args, PyObject* kw);
+
 #endif
 
 
diff --git a/scribus/plugins/scriptplugin/scriptplugin.cpp b/scribus/plugins/scriptplugin/scriptplugin.cpp
index fabb7350a..bc790d097 100644
--- a/scribus/plugins/scriptplugin/scriptplugin.cpp
+++ b/scribus/plugins/scriptplugin/scriptplugin.cpp
@@ -326,6 +326,7 @@ PyMethodDef scribus_methods[] = {
 	{ "deselectAll", (PyCFunction) scribus_deselectall, METH_NOARGS, tr(scribus_deselectall__doc__)},
 	{ "docChanged", scribus_docchanged, METH_VARARGS, tr(scribus_docchanged__doc__)},
 	{ "editMasterPage", scribus_editmasterpage, METH_VARARGS, tr(scribus_editmasterpage__doc__)},
+	{ "exportDocumentCheck", (PyCFunction) scribus_exportdocumentcheck, METH_VARARGS|METH_KEYWORDS, tr(scribus_exportdocumentcheck__doc__)},
 	{ "fileDialog", (PyCFunction) scribus_filedialog, METH_VARARGS|METH_KEYWORDS, tr(scribus_filedialog__doc__)},
 	{ "fileQuit", scribus_filequit, METH_VARARGS, tr(scribus_filequit__doc__)},
 	{ "flipObject", scribus_flipobject, METH_VARARGS, tr(scribus_flipobject__doc__)},
