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 <QPixmap>
13 #include <QImage>
14 #include <QPainter>
15 #include <QPalette>
16 #include <QTextCodec>
17 #include <QTextDocument>
18 
19 #include <KSyntaxHighlighting/SyntaxHighlighter>
20 #include <KSyntaxHighlighting/Theme>
21 #include <KSyntaxHighlighting/Definition>
22 #include <KDesktopFile>
23 
24 // TODO Fix or remove kencodingprober code
25 // #include <kencodingprober.h>
26 
27 extern "C"
28 {
new_creator()29 Q_DECL_EXPORT KIO::ThumbDevicePixelRatioDependentCreator *new_creator()
30 {
31     return new TextCreator;
32 }
33 }
34 
TextCreator()35 TextCreator::TextCreator()
36     : KIO::ThumbDevicePixelRatioDependentCreator(),
37       m_data(nullptr),
38       m_dataSize(0) {}
39 
~TextCreator()40 TextCreator::~TextCreator()
41 {
42     delete [] m_data;
43 }
44 
codecFromContent(const char * data,int dataSize)45 static QTextCodec *codecFromContent(const char *data, int dataSize)
46 {
47 #if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data)
48     KEncodingProber prober;
49     prober.feed(data, dataSize);
50     return QTextCodec::codecForName(prober.encoding());
51 #else
52     QByteArray ba = QByteArray::fromRawData(data, dataSize);
53     // try to detect UTF text, fall back to locale default (which is usually UTF-8)
54     return QTextCodec::codecForUtfText(ba, QTextCodec::codecForLocale());
55 #endif
56 }
57 
create(const QString & path,int width,int height,QImage & img)58 bool TextCreator::create(const QString &path, int width, int height, QImage &img)
59 {
60     // Desktop files, .directory files, and flatpakrefs aren't traditional
61     // text files, so their icons should be shown instead
62     if (KDesktopFile::isDesktopFile(path)
63             || path.endsWith(QStringLiteral(".directory"))
64             || path.endsWith(QStringLiteral(".flatpakref"))
65        ) {
66         return false;
67     }
68 
69     bool ok = false;
70 
71     // determine some sizes...
72     // example: width: 60, height: 64
73     QSize pixmapSize( width * devicePixelRatio(), height * devicePixelRatio() );
74     if (height * 3 > width * 4)
75         pixmapSize.setHeight( width * 4 / 3 * devicePixelRatio());
76     else
77         pixmapSize.setWidth( height * 3 / 4 * devicePixelRatio());
78 
79     if ( pixmapSize != m_pixmap.size() ) {
80         m_pixmap = QPixmap( pixmapSize );
81         m_pixmap.setDevicePixelRatio(devicePixelRatio());
82     }
83 
84     // one pixel for the rectangle, the rest. whitespace
85     int xborder = 1 + pixmapSize.width()/16 / devicePixelRatio();  // minimum x-border
86     int yborder = 1 + pixmapSize.height()/16 / devicePixelRatio(); // minimum y-border
87 
88     // this font is supposed to look good at small sizes
89     QFont font = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
90 
91     font.setPixelSize( qMax(7, qMin( 10, ( pixmapSize.height()/ devicePixelRatio() - 2 * yborder ) / 16 ) ) );
92     QFontMetrics fm( font );
93 
94     // calculate a better border so that the text is centered
95     const QSizeF canvasSize(pixmapSize.width() / devicePixelRatio() - 2 * xborder, pixmapSize.height() / devicePixelRatio() - 2 * yborder);
96     const int numLines = (int) (canvasSize.height() / fm.height());
97 
98     // assumes an average line length of <= 120 chars
99     const int bytesToRead = 120 * numLines;
100 
101     // create text-preview
102     QFile file( path );
103     if ( file.open( QIODevice::ReadOnly ))
104     {
105         if ( !m_data || m_dataSize < bytesToRead + 1 )
106         {
107             delete [] m_data;
108             m_data = new char[bytesToRead+1];
109             m_dataSize = bytesToRead + 1;
110         }
111 
112         int read = file.read( m_data, bytesToRead );
113         if ( read > 0 )
114         {
115             ok = true;
116             m_data[read] = '\0';
117             QString text = codecFromContent( m_data, read )->toUnicode( m_data, read ).trimmed();
118             // FIXME: maybe strip whitespace and read more?
119 
120             // If the text contains tabs or consecutive spaces, it is probably
121             // formatted using white space. Use a fixed pitch font in this case.
122             const auto textLines = text.splitRef(QLatin1Char('\n'));
123             for (const auto& line : textLines) {
124                 const auto trimmedLine = line.trimmed();
125                 if ( trimmedLine.contains( '\t' ) || trimmedLine.contains( "  " ) ) {
126                     font.setFamily( QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
127                     break;
128                 }
129             }
130 
131             QColor bgColor = QColor ( 245, 245, 245 ); // light-grey background
132             m_pixmap.fill( bgColor );
133 
134             QPainter painter( &m_pixmap );
135 
136             QTextDocument textDocument(text);
137 
138             // QTextDocument only supports one margin value for all borders,
139             // so we do a page-in-page behind its back, and do our own borders
140             textDocument.setDocumentMargin(0);
141             textDocument.setPageSize(canvasSize);
142             textDocument.setDefaultFont(font);
143 
144             QTextOption textOption( Qt::AlignTop | Qt::AlignLeft );
145             textOption.setTabStopDistance(8 * painter.fontMetrics().horizontalAdvance(QLatin1Char(' ')));
146             textOption.setWrapMode( QTextOption::WrapAtWordBoundaryOrAnywhere );
147             textDocument.setDefaultTextOption(textOption);
148 
149             KSyntaxHighlighting::SyntaxHighlighter syntaxHighlighter;
150             syntaxHighlighter.setDefinition(m_highlightingRepository.definitionForFileName(path));
151             const auto highlightingTheme = m_highlightingRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
152             syntaxHighlighter.setTheme(highlightingTheme);
153             syntaxHighlighter.setDocument(&textDocument);
154             syntaxHighlighter.rehighlight();
155 
156             // draw page-in-page, with clipping as needed
157             painter.translate(xborder, yborder);
158             textDocument.drawContents(&painter, QRectF(QPointF(0, 0), canvasSize));
159 
160             painter.end();
161 
162             img = m_pixmap.toImage();
163         }
164 
165         file.close();
166     }
167     return ok;
168 }
169