1 /* This file is part of the KDE project
2  * Copyright (C) 2006, 2009 Thomas Zander <zander@kde.org>
3  * Copyright (C) 2007 Sebastian Sauer <mail@dipe.org>
4  * Copyright (C) 2007 Pierre Ducroquet <pinaraf@gmail.com>
5  * Copyright (C) 2008 Girish Ramakrishnan <girish@forwardbias.in>
6  * Copyright (C) 2009,2011 KO GmbH <cbo@kogmbh.com>
7  * Copyright (C) 2011-2012 Pierre Stirnweiss <pstirnweiss@googlemail.com>
8  * Copyright (C) 2012 Gopalakrishna Bhat A <gopalakbhat@gmail.com>
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Library General Public
12  * License as published by the Free Software Foundation; either
13  * version 2 of the License, or (at your option) any later version.
14  *
15  * This library is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Library General Public License for more details.
19  *
20  * You should have received a copy of the GNU Library General Public License
21  * along with this library; see the file COPYING.LIB.  If not, write to
22  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23  * Boston, MA 02110-1301, USA.
24  */
25 
26 #include "KoStyleThumbnailer.h"
27 
28 #include "KoParagraphStyle.h"
29 #include "KoCharacterStyle.h"
30 #include "KoTextDocumentLayout.h"
31 #include "KoTextLayoutRootArea.h"
32 #include "FrameIterator.h"
33 
34 #include <stdint.h>
35 
36 #include <klocalizedstring.h>
37 
38 #include <QCache>
39 #include <QFont>
40 #include <QImage>
41 #include <QPainter>
42 #include <QRect>
43 #include <QTextBlock>
44 #include <QTextCursor>
45 #include <QTextLength>
46 
47 #include <TextLayoutDebug.h>
48 
49 extern int qt_defaultDpiX();
50 extern int qt_defaultDpiY();
51 
52 class Q_DECL_HIDDEN KoStyleThumbnailer::Private
53 {
54 public:
Private()55     Private() :
56         thumbnailHelperDocument(new QTextDocument),
57         documentLayout(new KoTextDocumentLayout(thumbnailHelperDocument)),
58         defaultSize(QSize(250, 48))
59     {
60         thumbnailHelperDocument->setDocumentLayout(documentLayout);
61     }
62 
~Private()63     ~Private()
64     {
65         delete documentLayout;
66         delete thumbnailHelperDocument;
67     }
68 
69     QTextDocument *thumbnailHelperDocument;
70     KoTextDocumentLayout *documentLayout;
71     QCache<QString, QImage> thumbnailCache; // cache of QImage representations of the styles
72     QSize defaultSize;
73     QString thumbnailText;
74 };
75 
KoStyleThumbnailer()76 KoStyleThumbnailer::KoStyleThumbnailer()
77         : d(new Private())
78 {
79 }
80 
~KoStyleThumbnailer()81 KoStyleThumbnailer::~KoStyleThumbnailer()
82 {
83     delete d;
84 }
85 
thumbnail(KoParagraphStyle * style,const QSize & _size,bool recreateThumbnail,KoStyleThumbnailerFlags flags)86 QImage KoStyleThumbnailer::thumbnail(KoParagraphStyle *style, const QSize &_size, bool recreateThumbnail, KoStyleThumbnailerFlags flags)
87 {
88     if ((flags & UseStyleNameText)  && (!style || style->name().isNull())) {
89         return QImage();
90     } else if ((! (flags & UseStyleNameText)) && d->thumbnailText.isEmpty()) {
91         return QImage();
92     }
93 
94     const QSize &size = (!_size.isValid() || _size.isNull()) ? d->defaultSize : _size;
95 
96     QString imageKey = "p_" + QString::number(reinterpret_cast<uintptr_t>(style)) + "_" + QString::number(size.width()) + "_" + QString::number(size.height());
97 
98     if (!recreateThumbnail && d->thumbnailCache.object(imageKey)) {
99         return QImage(*(d->thumbnailCache.object(imageKey)));
100     }
101 
102     QImage *im = new QImage(size.width(), size.height(), QImage::Format_ARGB32_Premultiplied);
103     im->fill(QColor(Qt::transparent).rgba());
104 
105     KoParagraphStyle *clone = style->clone();
106     //TODO: make the following real options
107     //we ignore these properties when the thumbnail would not be sufficient to preview properly the whole paragraph with margins.
108     clone->setMargin(QTextLength(QTextLength::FixedLength, 0));
109     clone->setPadding(0);
110     //
111     QTextCursor cursor(d->thumbnailHelperDocument);
112     cursor.select(QTextCursor::Document);
113     cursor.setBlockFormat(QTextBlockFormat());
114     cursor.setBlockCharFormat(QTextCharFormat());
115     cursor.setCharFormat(QTextCharFormat());
116     QTextBlock block = cursor.block();
117     clone->applyStyle(block, true);
118 
119     QTextCharFormat format;
120     // Default to black as text color, to match what KoTextLayoutArea::paint(...)
121     // does, setting solid black if no brush is set. Otherwise the UI text color
122     // would be used, which might be too bright with dark UI color schemes
123     format.setForeground(QColor(Qt::black));
124     clone->KoCharacterStyle::applyStyle(format);
125     if (flags & UseStyleNameText) {
126         cursor.insertText(clone->name(), format);
127     } else {
128         cursor.insertText(d->thumbnailText, format);
129     }
130     layoutThumbnail(size, im, flags);
131     // Make a copy of the image before inserting in the cache
132     QImage res = QImage(*im);
133     // Because on inserting, QCache can decide to delete the object immediately
134     d->thumbnailCache.insert(imageKey, im);
135 
136     delete clone;
137     return res;
138 }
139 
thumbnail(KoCharacterStyle * characterStyle,KoParagraphStyle * paragraphStyle,const QSize & _size,bool recreateThumbnail,KoStyleThumbnailerFlags flags)140 QImage KoStyleThumbnailer::thumbnail(KoCharacterStyle *characterStyle, KoParagraphStyle *paragraphStyle, const QSize &_size, bool recreateThumbnail, KoStyleThumbnailerFlags flags)
141 {
142     if ((flags & UseStyleNameText)  && (!characterStyle || characterStyle->name().isNull())) {
143         return QImage();
144     } else if ((! (flags & UseStyleNameText)) && d->thumbnailText.isEmpty()) {
145         return QImage();
146     }
147     else if (characterStyle == 0) {
148         return QImage();
149     }
150 
151     const QSize &size = (!_size.isValid() || _size.isNull()) ? d->defaultSize : _size;
152 
153     QString imageKey = "c_" + QString::number(reinterpret_cast<uintptr_t>(characterStyle)) + "_"
154                      + "p_" + QString::number(reinterpret_cast<uintptr_t>(paragraphStyle)) + "_"
155                      + QString::number(size.width()) + "_" + QString::number(size.height());
156 
157     if (!recreateThumbnail && d->thumbnailCache.object(imageKey)) {
158         return QImage(*(d->thumbnailCache.object(imageKey)));
159     }
160 
161     QImage *im = new QImage(size.width(), size.height(), QImage::Format_ARGB32_Premultiplied);
162     im->fill(QColor(Qt::transparent).rgba());
163 
164     QTextCursor cursor(d->thumbnailHelperDocument);
165     QTextCharFormat format;
166     // Default to black as text color, to match what KoTextLayoutArea::paint(...)
167     // does, setting solid black if no brush is set. Otherwise the UI text color
168     // would be used, which might be too bright with dark UI color schemes
169     format.setForeground(QColor(Qt::black));
170     KoCharacterStyle *characterStyleClone = characterStyle->clone();
171     characterStyleClone->applyStyle(format);
172     cursor.select(QTextCursor::Document);
173     cursor.setBlockFormat(QTextBlockFormat());
174     cursor.setBlockCharFormat(QTextCharFormat());
175     cursor.setCharFormat(QTextCharFormat());
176 
177     if (paragraphStyle) {
178         KoParagraphStyle *paragraphStyleClone = paragraphStyle->clone();
179        // paragraphStyleClone->KoCharacterStyle::applyStyle(format);
180         QTextBlock block = cursor.block();
181         paragraphStyleClone->applyStyle(block, true);
182         delete paragraphStyleClone;
183         paragraphStyleClone = 0;
184     }
185 
186     if (flags & UseStyleNameText) {
187         cursor.insertText(characterStyleClone->name(), format);
188     } else {
189         cursor.insertText(d->thumbnailText, format);
190     }
191 
192     layoutThumbnail(size, im, flags);
193     QImage res = QImage(*im);
194     d->thumbnailCache.insert(imageKey, im);
195     delete characterStyleClone;
196     return res;
197 }
198 
setThumbnailSize(const QSize & size)199 void KoStyleThumbnailer::setThumbnailSize(const QSize &size)
200 {
201     d->defaultSize = size;
202 }
203 
layoutThumbnail(const QSize & size,QImage * im,KoStyleThumbnailerFlags flags)204 void KoStyleThumbnailer::layoutThumbnail(const QSize &size, QImage *im, KoStyleThumbnailerFlags flags)
205 {
206     QPainter p(im);
207     d->documentLayout->removeRootArea();
208     KoTextLayoutRootArea rootArea(d->documentLayout);
209     rootArea.setReferenceRect(0, size.width() * 72.0 / qt_defaultDpiX(), 0, 1E6);
210     rootArea.setNoWrap(1E6);
211 
212     FrameIterator frameCursor(d->thumbnailHelperDocument->rootFrame());
213     rootArea.layoutRoot(&frameCursor);
214 
215     QSizeF documentSize = rootArea.boundingRect().size();
216     documentSize.setWidth(documentSize.width() * qt_defaultDpiX() / 72.0);
217     documentSize.setHeight(documentSize.height() * qt_defaultDpiY() / 72.0);
218     if (documentSize.width() > size.width() || documentSize.height() > size.height()) {
219         //calculate the space needed for the font size indicator (should the preview be too big with the style's font size
220         QTextCursor cursor(d->thumbnailHelperDocument);
221         cursor.select(QTextCursor::Document);
222         QString sizeHint = "\t" + QString::number(cursor.charFormat().fontPointSize()) + "pt";
223         p.save();
224         QFont sizeHintFont = p.font();
225         sizeHintFont.setPointSize(8);
226         p.setFont(sizeHintFont);
227         QRectF sizeHintRect(p.boundingRect(0, 0, 1, 1, Qt::AlignCenter, sizeHint));
228         p.restore();
229         qreal width = qMax<qreal>(0., size.width()-sizeHintRect.width());
230 
231         QTextCharFormat fmt = cursor.charFormat();
232         if (flags & ScaleThumbnailFont) {
233             //calculate the font reduction factor so that the text + the sizeHint fits
234             qreal reductionFactor = qMin(width/documentSize.width(), size.height()/documentSize.height());
235 
236             fmt.setFontPointSize((int)(fmt.fontPointSize()*reductionFactor));
237         }
238 
239         cursor.mergeCharFormat(fmt);
240 
241         frameCursor = FrameIterator(d->thumbnailHelperDocument->rootFrame());
242         rootArea.setReferenceRect(0, width * 72.0 / qt_defaultDpiX(), 0, 1E6);
243         rootArea.setNoWrap(1E6);
244         rootArea.layoutRoot(&frameCursor);
245         documentSize = rootArea.boundingRect().size();
246         documentSize.setWidth(documentSize.width() * qt_defaultDpiX() / 72.0);
247         documentSize.setHeight(documentSize.height() * qt_defaultDpiY() / 72.0);
248         //center the preview in the pixmap
249         qreal yOffset = (size.height()-documentSize.height())/2;
250         p.save();
251         if ((flags & CenterAlignThumbnail) && yOffset) {
252             p.translate(0, yOffset);
253         }
254 
255         p.scale(qt_defaultDpiX() / 72.0, qt_defaultDpiY() / 72.0);
256 
257         KoTextDocumentLayout::PaintContext pc;
258         rootArea.paint(&p, pc);
259 
260         p.restore();
261 
262         p.setFont(sizeHintFont);
263         p.drawText(QRectF(size.width()-sizeHintRect.width(), 0, sizeHintRect.width(),
264                           size.height() /*because we want to be vertically centered in the pixmap, like the style name*/),Qt::AlignCenter, sizeHint);
265     }
266     else {
267         //center the preview in the pixmap
268         qreal yOffset = (size.height()-documentSize.height())/2;
269         if ((flags & CenterAlignThumbnail) && yOffset) {
270             p.translate(0, yOffset);
271         }
272 
273         p.scale(qt_defaultDpiX() / 72.0, qt_defaultDpiY() / 72.0);
274 
275         KoTextDocumentLayout::PaintContext pc;
276         rootArea.paint(&p, pc);
277     }
278 }
279 
removeFromCache(KoParagraphStyle * style)280 void KoStyleThumbnailer::removeFromCache(KoParagraphStyle *style)
281 {
282     QString imageKey = "p_" + QString::number(reinterpret_cast<uintptr_t>(style)) + "_";
283     removeFromCache(imageKey);
284 }
285 
removeFromCache(KoCharacterStyle * style)286 void KoStyleThumbnailer::removeFromCache(KoCharacterStyle *style)
287 {
288     QString imageKey = "c_" + QString::number(reinterpret_cast<uintptr_t>(style)) + "_";
289     removeFromCache(imageKey);
290 }
291 
setText(const QString & text)292 void KoStyleThumbnailer::setText(const QString &text)
293 {
294     d->thumbnailText = text;
295 }
296 
removeFromCache(const QString & expr)297 void KoStyleThumbnailer::removeFromCache(const QString &expr)
298 {
299     QList<QString> keys = d->thumbnailCache.keys();
300     foreach (const QString &key, keys) {
301         if (key.contains(expr)) {
302             d->thumbnailCache.remove(key);
303         }
304     }
305 }
306