pdf: text that names no colour comes out black, not the palette

A QTextDocument takes its default text colour from the application
palette, and markdown names no colour at all: in a window running a dark
palette -- which is what a phone in dark mode hands Qt -- every plain
paragraph was laid out #eeeeee on a white page. Printed, that is a blank
document; on screen it reads as "the font is oddly faint". The first
PDFs off the phone were exactly that.

- PdfTools: paperDocument() sets the default stylesheet (body colour
  #000000) before setHtml, and writePdf() lays that out. A colour the
  document does name still wins, so an .html handed to the tool keeps its
  own -- checked with a #cc0000 document under both palettes.
- --uitest: draws that document on a white image under a dark palette and
  looks for ink (238 before the fix, 0 after).

Verified on the NX789J: md_to_pdf on the device now writes CID TrueType
NotoSansCJKHK-Regular/-Bold text whose page renders at min=0.
This commit is contained in:
WHC
2026-10-11 20:35:41 +08:00
parent da04913071
commit 4c69ef39ab
3 changed files with 67 additions and 8 deletions
+26 -8
View File
@@ -35,6 +35,7 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <memory>
#include "core/AgentCore.h" #include "core/AgentCore.h"
#include "core/ToolProvider.h" #include "core/ToolProvider.h"
@@ -168,10 +169,9 @@ bool sourceHtml(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
* strip. */ * strip. */
bool writePdf(const QString &html, const QString &path, const QString &title, bool writePdf(const QString &html, const QString &path, const QString &title,
QString *err) { QString *err) {
QTextDocument doc; std::unique_ptr<QTextDocument> doc(paperDocument(html));
doc.setHtml(html);
if (!title.isEmpty()) if (!title.isEmpty())
doc.setMetaInformation(QTextDocument::DocumentTitle, title); doc->setMetaInformation(QTextDocument::DocumentTitle, title);
QPdfWriter writer(path); QPdfWriter writer(path);
writer.setPageSize(QPageSize(QPageSize::A4)); writer.setPageSize(QPageSize(QPageSize::A4));
@@ -186,17 +186,17 @@ bool writePdf(const QString &html, const QString &path, const QString &title,
return false; return false;
} }
const QRectF page = writer.pageLayout().paintRectPixels(writer.resolution()); const QRectF page = writer.pageLayout().paintRectPixels(writer.resolution());
doc.setPageSize(page.size()); doc->setPageSize(page.size());
const int pages = qMax(1, doc.pageCount()); const int pages = qMax(1, doc->pageCount());
for (int i = 0; i < pages; i++) { for (int i = 0; i < pages; i++) {
if (i) if (i)
writer.newPage(); writer.newPage();
painter.save(); painter.save();
painter.translate(page.topLeft()); painter.translate(page.topLeft());
painter.translate(0, -i * page.height()); painter.translate(0, -i * page.height());
doc.drawContents(&painter, doc->drawContents(&painter,
QRectF(0, 0, page.width(), page.height()) QRectF(0, 0, page.width(), page.height())
.translated(0, i * page.height())); .translated(0, i * page.height()));
painter.restore(); painter.restore();
} }
painter.end(); painter.end();
@@ -285,6 +285,24 @@ int mdToPdfRun(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
} // namespace } // namespace
/* The document writePdf lays out, with a colour filled in for text that names
* none of its own.
*
* Paper is white and has no theme, but a QTextDocument takes its default text
* colour from the application palette: in a window running a dark palette --
* which is what a phone in dark mode hands Qt -- every plain markdown
* paragraph came out #eeeeee on a white page, i.e. a PDF that looks blank
* when printed, and reads as "the font is oddly faint" on screen. Naming the
* colour makes the page depend on the paper rather than on the window. A
* colour the document does name still wins (an .html file somebody handed us
* keeps its own), so this only fills the holes. */
QTextDocument *paperDocument(const QString &html) {
auto *doc = new QTextDocument;
doc->setDefaultStyleSheet(QStringLiteral("body { color: #000000; }"));
doc->setHtml(html);
return doc;
}
int registerQtTools(AgentCore *core) { int registerQtTools(AgentCore *core) {
if (!core) if (!core)
return 0; return 0;
+7
View File
@@ -19,6 +19,9 @@
/* The engine wrapper of the front end (core/AgentCore.h). Only a pointer /* The engine wrapper of the front end (core/AgentCore.h). Only a pointer
* travels through this header, so the class stays incomplete here. */ * travels through this header, so the class stays incomplete here. */
class AgentCore; class AgentCore;
/* Same for the document md_to_pdf lays out (PdfTools.cpp): the caller owns
* it, and the test in ui/UiTest.cpp inspects how it draws. */
class QTextDocument;
namespace app { namespace app {
@@ -48,6 +51,10 @@ int registerPlatformTools(AgentCore *core);
* (they are Qt, not a platform API): today md_to_pdf, owned by "app:qt". * (they are Qt, not a platform API): today md_to_pdf, owned by "app:qt".
* Called next to registerPlatformTools(). */ * Called next to registerPlatformTools(). */
int registerQtTools(AgentCore *core); int registerQtTools(AgentCore *core);
/* The document md_to_pdf lays out from `html`: the page's own defaults
* applied (text with no colour of its own is black, not the window's palette
* colour). The caller owns the result. */
QTextDocument *paperDocument(const QString &html);
/* Drop them again (they belong to the owners "app:*"). */ /* Drop them again (they belong to the owners "app:*"). */
void unregisterPlatformTools(AgentCore *core); void unregisterPlatformTools(AgentCore *core);
+34
View File
@@ -13,6 +13,7 @@
#include <QBuffer> #include <QBuffer>
#include <QFile> #include <QFile>
#include <QImage> #include <QImage>
#include <QPalette>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QPainter> #include <QPainter>
@@ -22,14 +23,17 @@
#include <QQmlContext> #include <QQmlContext>
#include <QQuickWindow> #include <QQuickWindow>
#include <QTest> #include <QTest>
#include <QTextDocument>
#include <QVariantMap> #include <QVariantMap>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory>
#include "cst_agent.h" #include "cst_agent.h"
#include "cst_attach.h" #include "cst_attach.h"
#include "core/ToolProvider.h"
#include "ui/SessionImages.h" #include "ui/SessionImages.h"
#include "ui/ThemeIcons.h" #include "ui/ThemeIcons.h"
@@ -482,6 +486,36 @@ int runUiTest(AgentCore &core, const QString &size) {
} }
} }
/* Paper is white whatever the window's palette says: markdown carries no
* colour, so a dark window turned every plain paragraph #eeeeee -- a PDF
* that looked blank. Draw it on a white image and look for ink. */
{
const QPalette was = QGuiApplication::palette();
QPalette darkPalette = was;
darkPalette.setColor(QPalette::Text, QColor(0xee, 0xee, 0xee));
darkPalette.setColor(QPalette::WindowText, QColor(0xee, 0xee, 0xee));
QGuiApplication::setPalette(darkPalette);
std::unique_ptr<QTextDocument> page(app::paperDocument(
QStringLiteral("<html><body>text that names no colour of its own"
"</body></html>")));
QImage paper(320, 60, QImage::Format_RGB32);
paper.fill(0xffffffffu);
QPainter ink(&paper);
page->drawContents(&ink);
ink.end();
int darkest = 255;
for (int y = 0; y < paper.height(); y++) {
const QRgb *row = reinterpret_cast<const QRgb *>(paper.constScanLine(y));
for (int x = 0; x < paper.width(); x++)
darkest = qMin(darkest, qGray(row[x]));
}
check(darkest < 64,
QStringLiteral("a dark window still lays out black ink on paper "
"(darkest pixel %1)")
.arg(darkest));
QGuiApplication::setPalette(was);
}
/* new sessions: the engine moves, the view empties, the store grows */ /* new sessions: the engine moves, the view empties, the store grows */
const int before = core.sessions().size(); const int before = core.sessions().size();
clickMouse(win, QStringLiteral("newSessionButton")); clickMouse(win, QStringLiteral("newSessionButton"));