File indexing completed on 2024-04-28 04:58:04

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2000, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
0003     SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "textcreator.h"
0009 
0010 #include <QFile>
0011 #include <QFontDatabase>
0012 #include <QImage>
0013 #include <QPainter>
0014 #include <QPalette>
0015 #include <QTextCodec>
0016 #include <QTextDocument>
0017 
0018 #include <KDesktopFile>
0019 #include <KPluginFactory>
0020 #include <KSyntaxHighlighting/Definition>
0021 #include <KSyntaxHighlighting/SyntaxHighlighter>
0022 #include <KSyntaxHighlighting/Theme>
0023 
0024 // TODO Fix or remove kencodingprober code
0025 // #include <kencodingprober.h>
0026 
0027 K_PLUGIN_CLASS_WITH_JSON(TextCreator, "textthumbnail.json")
0028 
0029 TextCreator::TextCreator(QObject *parent, const QVariantList &args)
0030     : KIO::ThumbnailCreator(parent, args)
0031     , m_data(nullptr)
0032     , m_dataSize(0)
0033 {
0034 }
0035 
0036 TextCreator::~TextCreator()
0037 {
0038     delete[] m_data;
0039 }
0040 
0041 static QTextCodec *codecFromContent(const char *data, int dataSize)
0042 {
0043 #if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data)
0044     KEncodingProber prober;
0045     prober.feed(data, dataSize);
0046     return QTextCodec::codecForName(prober.encoding());
0047 #else
0048     QByteArray ba = QByteArray::fromRawData(data, dataSize);
0049     // try to detect UTF text, fall back to locale default (which is usually UTF-8)
0050     return QTextCodec::codecForUtfText(ba, QTextCodec::codecForLocale());
0051 #endif
0052 }
0053 
0054 KIO::ThumbnailResult TextCreator::create(const KIO::ThumbnailRequest &request)
0055 {
0056     const QString path = request.url().toLocalFile();
0057     // Desktop files, .directory files, and flatpakrefs aren't traditional
0058     // text files, so their icons should be shown instead
0059     if (KDesktopFile::isDesktopFile(path) || path.endsWith(QStringLiteral(".directory")) || path.endsWith(QStringLiteral(".flatpakref"))) {
0060         return KIO::ThumbnailResult::fail();
0061     }
0062 
0063     bool ok = false;
0064 
0065     // determine some sizes...
0066     // example: width: 60, height: 64
0067 
0068     const int width = request.targetSize().width();
0069     const int height = request.targetSize().height();
0070     const qreal dpr = request.devicePixelRatio();
0071 
0072     QImage img;
0073 
0074     QSize pixmapSize(width, height);
0075     if (height * 3 > width * 4)
0076         pixmapSize.setHeight(width * 4 / 3);
0077     else
0078         pixmapSize.setWidth(height * 3 / 4);
0079 
0080     if (pixmapSize != m_pixmap.size()) {
0081         m_pixmap = QPixmap(pixmapSize);
0082         m_pixmap.setDevicePixelRatio(dpr);
0083     }
0084 
0085     // one pixel for the rectangle, the rest. whitespace
0086     int xborder = 1 + pixmapSize.width() / 16 / dpr; // minimum x-border
0087     int yborder = 1 + pixmapSize.height() / 16 / dpr; // minimum y-border
0088 
0089     // this font is supposed to look good at small sizes
0090     QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
0091 
0092     font.setPixelSize(qMax(7.0, qMin(10.0, (pixmapSize.height() / dpr - 2 * yborder) / 16)));
0093     QFontMetrics fm(font);
0094 
0095     // calculate a better border so that the text is centered
0096     const QSizeF canvasSize(pixmapSize.width() / dpr - 2 * xborder, pixmapSize.height() / dpr - 2 * yborder);
0097     const int numLines = (int)(canvasSize.height() / fm.height());
0098 
0099     // assumes an average line length of <= 120 chars
0100     const int bytesToRead = 120 * numLines;
0101 
0102     // create text-preview
0103     QFile file(path);
0104     if (file.open(QIODevice::ReadOnly)) {
0105         if (!m_data || m_dataSize < bytesToRead + 1) {
0106             delete[] m_data;
0107             m_data = new char[bytesToRead + 1];
0108             m_dataSize = bytesToRead + 1;
0109         }
0110 
0111         int read = file.read(m_data, bytesToRead);
0112         if (read > 0) {
0113             ok = true;
0114             m_data[read] = '\0';
0115             QString text = codecFromContent(m_data, read)->toUnicode(m_data, read).trimmed();
0116             // FIXME: maybe strip whitespace and read more?
0117 
0118             // If the text contains tabs or consecutive spaces, it is probably
0119             // formatted using white space. Use a fixed pitch font in this case.
0120             const auto textLines = QStringView(text).split(QLatin1Char('\n'));
0121             for (const auto &line : textLines) {
0122                 const auto trimmedLine = line.trimmed();
0123                 if (trimmedLine.contains('\t') || trimmedLine.contains(QLatin1String("  "))) {
0124                     font.setFamily(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
0125                     break;
0126                 }
0127             }
0128 
0129             QColor bgColor = QColor(245, 245, 245); // light-grey background
0130             m_pixmap.fill(bgColor);
0131 
0132             QPainter painter(&m_pixmap);
0133 
0134             QTextDocument textDocument(text);
0135 
0136             // QTextDocument only supports one margin value for all borders,
0137             // so we do a page-in-page behind its back, and do our own borders
0138             textDocument.setDocumentMargin(0);
0139             textDocument.setPageSize(canvasSize);
0140             textDocument.setDefaultFont(font);
0141 
0142             QTextOption textOption(Qt::AlignTop | Qt::AlignLeft);
0143             textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' ')));
0144             textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
0145             textDocument.setDefaultTextOption(textOption);
0146 
0147             KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter;
0148             syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path));
0149             const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
0150             syntaxHighlighter.setTheme(highlightingTheme);
0151             syntaxHighlighter.setDocument(&textDocument);
0152             syntaxHighlighter.rehighlight();
0153 
0154             // draw page-in-page, with clipping as needed
0155             painter.translate(xborder, yborder);
0156             textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize));
0157 
0158             painter.end();
0159 
0160             img = m_pixmap.toImage();
0161         }
0162 
0163         file.close();
0164     }
0165     return ok ? KIO::ThumbnailResult::pass(img) : KIO::ThumbnailResult::fail();
0166 }
0167 
0168 #include "moc_textcreator.cpp"
0169 #include "textcreator.moc"