/src/kio-extras/thumbnail/textcreator.cpp
Line | Count | Source |
1 | | /* This file is part of the KDE libraries |
2 | | SPDX-FileCopyrightText: 2000, 2002 Carsten Pfeiffer <pfeiffer@kde.org> |
3 | | SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org> |
4 | | |
5 | | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | | */ |
7 | | |
8 | | #include "textcreator.h" |
9 | | |
10 | | #include <QFile> |
11 | | #include <QFontDatabase> |
12 | | #include <QImage> |
13 | | #include <QPainter> |
14 | | #include <QPalette> |
15 | | #include <QStringDecoder> |
16 | | #include <QTextDocument> |
17 | | |
18 | | #include <KDesktopFile> |
19 | | #include <KPluginFactory> |
20 | | #include <KSyntaxHighlighting/Definition> |
21 | | #include <KSyntaxHighlighting/SyntaxHighlighter> |
22 | | #include <KSyntaxHighlighting/Theme> |
23 | | |
24 | | // TODO Fix or remove kencodingprober code |
25 | | // #include <kencodingprober.h> |
26 | | |
27 | 0 | K_PLUGIN_CLASS_WITH_JSON(TextCreator, "textthumbnail.json") Unexecuted instantiation: textthumbnail_factory::tr(char const*, char const*, int) Unexecuted instantiation: textthumbnail_factory::~textthumbnail_factory() |
28 | 0 |
|
29 | 0 | TextCreator::TextCreator(QObject *parent, const QVariantList &args) |
30 | 17.4k | : KIO::ThumbnailCreator(parent, args) |
31 | 17.4k | , m_data(nullptr) |
32 | 17.4k | , m_dataSize(0) |
33 | 17.4k | { |
34 | 17.4k | } |
35 | | |
36 | | TextCreator::~TextCreator() |
37 | 17.4k | { |
38 | 17.4k | delete[] m_data; |
39 | 17.4k | } |
40 | | |
41 | | static QStringDecoder codecFromContent(const char *data, int dataSize) |
42 | 17.4k | { |
43 | | #if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data) |
44 | | KEncodingProber prober; |
45 | | prober.feed(data, dataSize); |
46 | | return QStringDecoder(prober.encoding()); |
47 | | #else |
48 | | // try to detect UTF text, fall back to locale default (which is usually UTF-8) |
49 | 17.4k | return QStringDecoder(QStringDecoder::encodingForData(QByteArrayView(data, dataSize)).value_or(QStringDecoder::System)); |
50 | 17.4k | #endif |
51 | 17.4k | } |
52 | | |
53 | | KIO::ThumbnailResult TextCreator::create(const KIO::ThumbnailRequest &request) |
54 | 17.4k | { |
55 | 17.4k | const QString path = request.url().toLocalFile(); |
56 | | // Desktop files, .directory files, and flatpakrefs aren't traditional |
57 | | // text files, so their icons should be shown instead |
58 | 17.4k | if (KDesktopFile::isDesktopFile(path) || path.endsWith(QStringLiteral(".directory")) || path.endsWith(QStringLiteral(".flatpakref"))) { |
59 | 0 | return KIO::ThumbnailResult::fail(); |
60 | 0 | } |
61 | | |
62 | 17.4k | bool ok = false; |
63 | | |
64 | | // determine some sizes... |
65 | | // example: width: 60, height: 64 |
66 | | |
67 | 17.4k | const int width = request.targetSize().width(); |
68 | 17.4k | const int height = request.targetSize().height(); |
69 | 17.4k | const qreal dpr = request.devicePixelRatio(); |
70 | | |
71 | 17.4k | QImage img; |
72 | | |
73 | 17.4k | QSize pixmapSize(width, height); |
74 | 17.4k | if (height * 3 > width * 4) |
75 | 0 | pixmapSize.setHeight(width * 4 / 3); |
76 | 17.4k | else |
77 | 17.4k | pixmapSize.setWidth(height * 3 / 4); |
78 | | |
79 | 17.4k | if (pixmapSize != m_pixmap.size()) { |
80 | 17.4k | m_pixmap = QPixmap(pixmapSize); |
81 | 17.4k | m_pixmap.setDevicePixelRatio(dpr); |
82 | 17.4k | } |
83 | | |
84 | | // one pixel for the rectangle, the rest. whitespace |
85 | 17.4k | int xborder = 1 + pixmapSize.width() / 16 / dpr; // minimum x-border |
86 | 17.4k | int yborder = 1 + pixmapSize.height() / 16 / dpr; // minimum y-border |
87 | | |
88 | | // this font is supposed to look good at small sizes |
89 | 17.4k | QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont); |
90 | | |
91 | 17.4k | font.setPixelSize(qMax(7.0, qMin(10.0, (pixmapSize.height() / dpr - 2 * yborder) / 16))); |
92 | 17.4k | QFontMetrics fm(font); |
93 | | |
94 | | // calculate a better border so that the text is centered |
95 | 17.4k | const QSizeF canvasSize(pixmapSize.width() / dpr - 2 * xborder, pixmapSize.height() / dpr - 2 * yborder); |
96 | 17.4k | const int numLines = (int)(canvasSize.height() / fm.height()); |
97 | | |
98 | | // assumes an average line length of <= 120 chars |
99 | 17.4k | const int bytesToRead = 120 * numLines; |
100 | | |
101 | | // create text-preview |
102 | 17.4k | QFile file(path); |
103 | 17.4k | if (file.open(QIODevice::ReadOnly)) { |
104 | 17.4k | if (!m_data || m_dataSize < bytesToRead + 1) { |
105 | 17.4k | delete[] m_data; |
106 | 17.4k | m_data = new char[bytesToRead + 1]; |
107 | 17.4k | m_dataSize = bytesToRead + 1; |
108 | 17.4k | } |
109 | | |
110 | 17.4k | int read = file.read(m_data, bytesToRead); |
111 | 17.4k | if (read > 0) { |
112 | 17.4k | ok = true; |
113 | 17.4k | m_data[read] = '\0'; |
114 | 17.4k | QString text = QString(codecFromContent(m_data, read).decode(QByteArrayView(m_data, read))).trimmed(); |
115 | 17.4k | if (!text.isValidUtf16()) { |
116 | | // See if we are unlucky and the last byte we read was half of a utf16 character |
117 | 83 | text.chop(1); |
118 | 83 | if (!text.isValidUtf16()) { |
119 | 64 | return KIO::ThumbnailResult::fail(); |
120 | 64 | } |
121 | 83 | } |
122 | | // FIXME: maybe strip whitespace and read more? |
123 | | |
124 | | // If the text contains tabs or consecutive spaces, it is probably |
125 | | // formatted using white space. Use a fixed pitch font in this case. |
126 | 17.4k | const auto textLines = QStringView(text).split(QLatin1Char('\n')); |
127 | 252k | for (const auto &line : textLines) { |
128 | 252k | const auto trimmedLine = line.trimmed(); |
129 | 252k | if (trimmedLine.contains('\t') || trimmedLine.contains(QLatin1String(" "))) { |
130 | 2.95k | font.setFamily(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()); |
131 | 2.95k | break; |
132 | 2.95k | } |
133 | 252k | } |
134 | | |
135 | 17.4k | QColor bgColor = QColor(245, 245, 245); // light-grey background |
136 | 17.4k | m_pixmap.fill(bgColor); |
137 | | |
138 | 17.4k | QPainter painter(&m_pixmap); |
139 | | |
140 | 17.4k | QTextDocument textDocument(text); |
141 | | |
142 | | // QTextDocument only supports one margin value for all borders, |
143 | | // so we do a page-in-page behind its back, and do our own borders |
144 | 17.4k | textDocument.setDocumentMargin(0); |
145 | 17.4k | textDocument.setPageSize(canvasSize); |
146 | 17.4k | textDocument.setDefaultFont(font); |
147 | | |
148 | 17.4k | QTextOption textOption(Qt::AlignTop | Qt::AlignLeft); |
149 | 17.4k | textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' '))); |
150 | 17.4k | textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
151 | 17.4k | textDocument.setDefaultTextOption(textOption); |
152 | | |
153 | 17.4k | KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter; |
154 | 17.4k | syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path)); |
155 | 17.4k | const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme); |
156 | 17.4k | syntaxHighlighter.setTheme(highlightingTheme); |
157 | 17.4k | syntaxHighlighter.setDocument(&textDocument); |
158 | 17.4k | syntaxHighlighter.rehighlight(); |
159 | | |
160 | | // draw page-in-page, with clipping as needed |
161 | 17.4k | painter.translate(xborder, yborder); |
162 | 17.4k | textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize)); |
163 | | |
164 | 17.4k | painter.end(); |
165 | | |
166 | 17.4k | img = m_pixmap.toImage(); |
167 | 17.4k | } |
168 | | |
169 | 17.4k | file.close(); |
170 | 17.4k | } |
171 | 17.4k | return ok ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail(); |
172 | 17.4k | } |
173 | | |
174 | | #include "moc_textcreator.cpp" |
175 | | #include "textcreator.moc" |