Okular

tilesmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Mailson Menezes <mailson@gmail.com>
3 SPDX-License-Identifier: GPL-2.0-or-later
4*/
5
6#include "tilesmanager_p.h"
7
8#include <QList>
9#include <QPainter>
10#include <QPixmap>
11#include <qmath.h>
12
13#include "tile.h"
14
15#define TILES_MAXSIZE 2000000
16
17using namespace Okular;
18
19static bool rankedTilesLessThan(const TileNode *t1, const TileNode *t2)
20{
21 // Order tiles by its dirty state and then by distance from the viewport.
22 if (t1->dirty == t2->dirty) {
23 return t1->distance < t2->distance;
24 }
25
26 return !t1->dirty;
27}
28
29class TilesManager::Private
30{
31public:
32 Private();
33
34 bool hasPixmap(const NormalizedRect &rect, const TileNode &tile) const;
35 void tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf);
36 void setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap);
37
38 /**
39 * Mark @p tile and all its children as dirty
40 */
41 static void markDirty(TileNode &tile);
42
43 /**
44 * Deletes all tiles, recursively
45 */
46 void deleteTiles(const TileNode &tile);
47
48 void markParentDirty(const TileNode &tile);
49 void rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber);
50 /**
51 * Since the tile can be large enough to occupy a significant amount of
52 * space, they may be split in more tiles. This operation is performed
53 * when the tiles of a certain region is requested and they are bigger
54 * than an arbitrary value. Only tiles intersecting the desired region
55 * are split. There's no need to do this for the entire page.
56 */
57 void split(TileNode &tile, const NormalizedRect &rect);
58
59 /**
60 * Checks whether the tile's size is bigger than an arbitrary value and
61 * performs the split operation returning true.
62 * Otherwise it just returns false, without performing any operation.
63 */
64 bool splitBigTiles(TileNode &tile, const NormalizedRect &rect);
65
66 // The page is split in a 4x4 grid of tiles
67 TileNode tiles[16];
68 int width;
69 int height;
70 int pageNumber;
71 qulonglong totalPixels;
72 Rotation rotation;
73 NormalizedRect visibleRect;
74 NormalizedRect requestRect;
75 int requestWidth;
76 int requestHeight;
77};
78
79TilesManager::Private::Private()
80 : width(0)
81 , height(0)
82 , pageNumber(0)
83 , totalPixels(0)
84 , rotation(Rotation0)
85 , requestRect(NormalizedRect())
86 , requestWidth(0)
87 , requestHeight(0)
88{
89}
90
91TilesManager::TilesManager(int pageNumber, int width, int height, Rotation rotation)
92 : d(new Private)
93{
94 d->pageNumber = pageNumber;
95 d->width = width;
96 d->height = height;
97 d->rotation = rotation;
98
99 // The page is split in a 4x4 grid of tiles
100 const double dim = 0.25;
101 for (int i = 0; i < 16; ++i) {
102 int x = i % 4;
103 int y = i / 4;
104 d->tiles[i].rect = NormalizedRect(x * dim, y * dim, x * dim + dim, y * dim + dim);
105 }
106}
107
108TilesManager::~TilesManager()
109{
110 for (const TileNode &tile : d->tiles) {
111 d->deleteTiles(tile);
112 }
113
114 delete d;
115}
116
117void TilesManager::Private::deleteTiles(const TileNode &tile)
118{
119 if (tile.pixmap) {
120 totalPixels -= tile.pixmap->width() * tile.pixmap->height();
121 delete tile.pixmap;
122 }
123
124 if (tile.nTiles > 0) {
125 for (int i = 0; i < tile.nTiles; ++i) {
126 deleteTiles(tile.tiles[i]);
127 }
128
129 delete[] tile.tiles;
130 }
131}
132
133void TilesManager::setSize(int width, int height)
134{
135 if (width == d->width && height == d->height) {
136 return;
137 }
138
139 d->width = width;
140 d->height = height;
141
142 markDirty();
143}
144
145int TilesManager::width() const
146{
147 return d->width;
148}
149
150int TilesManager::height() const
151{
152 return d->height;
153}
154
155void TilesManager::setRotation(Rotation rotation)
156{
157 if (rotation == d->rotation) {
158 return;
159 }
160
161 d->rotation = rotation;
162}
163
164Rotation TilesManager::rotation() const
165{
166 return d->rotation;
167}
168
169void TilesManager::markDirty()
170{
171 for (TileNode &tile : d->tiles) {
172 TilesManager::Private::markDirty(tile);
173 }
174}
175
176void TilesManager::Private::markDirty(TileNode &tile)
177{
178 tile.dirty = true;
179
180 for (int i = 0; i < tile.nTiles; ++i) {
181 markDirty(tile.tiles[i]);
182 }
183}
184
185void TilesManager::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap)
186{
187 const NormalizedRect rotatedRect = TilesManager::fromRotatedRect(rect, d->rotation);
188 if (!d->requestRect.isNull()) {
189 if (!(d->requestRect == rect)) {
190 return;
191 }
192
193 if (pixmap) {
194 // Check whether the pixmap has the same absolute size of the expected
195 // request.
196 // If the document is rotated, rotate requestRect back to the original
197 // rotation before comparing to pixmap's size. This is to avoid
198 // conversion issues. The pixmap request was made using an unrotated
199 // rect.
200 QSize pixmapSize = pixmap->size();
201 int w = width();
202 int h = height();
203 if (d->rotation % 2) {
204 std::swap(w, h);
205 pixmapSize.transpose();
206 }
207
208 if (rotatedRect.geometry(w, h).size() != pixmapSize) {
209 return;
210 }
211 }
212
213 d->requestRect = NormalizedRect();
214 }
215
216 for (TileNode &tile : d->tiles) {
217 d->setPixmap(pixmap, rotatedRect, tile, isPartialPixmap);
218 }
219}
220
221void TilesManager::Private::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap)
222{
223 QRect pixmapRect = TilesManager::toRotatedRect(rect, rotation).geometry(width, height);
224
225 // Exclude tiles outside the viewport
226 if (!tile.rect.intersects(rect)) {
227 return;
228 }
229 // Avoid painting partial pixmaps over tiles that already have a fully rendered pixmap, even if dirty
230 if (isPartialPixmap && tile.pixmap != nullptr && !tile.partial) {
231 return;
232 }
233
234 // if the tile is not entirely within the viewport (the tile intersects an
235 // edged of the viewport), attempt to set the pixmap in the children tiles
236 if (!((tile.rect & rect) == tile.rect)) {
237 // paint children tiles
238 if (tile.nTiles > 0) {
239 for (int i = 0; i < tile.nTiles; ++i) {
240 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
241 }
242
243 delete tile.pixmap;
244 tile.pixmap = nullptr;
245 }
246 // We could paint the pixmap over part of the tile here, but
247 // there is little reason to as it will usually be offscreen
248 // and it will be overwritten later if more tiles enter the screen,
249 // as we only track the dirty state of whole tiles, not rects.
250 return;
251 }
252
253 // the tile lies entirely within the viewport
254 if (tile.nTiles == 0) {
255 tile.dirty = isPartialPixmap;
256 tile.partial = isPartialPixmap;
257
258 // check whether the tile size is big and split it if necessary
259 if (!splitBigTiles(tile, rect)) {
260 if (tile.pixmap) {
261 totalPixels -= tile.pixmap->width() * tile.pixmap->height();
262 delete tile.pixmap;
263 }
264 tile.rotation = rotation;
265 if (pixmap) {
266 const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
267 tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft())));
268 totalPixels += tile.pixmap->width() * tile.pixmap->height();
269 } else {
270 tile.pixmap = nullptr;
271 }
272 } else {
273 if (tile.pixmap) {
274 totalPixels -= tile.pixmap->width() * tile.pixmap->height();
275 delete tile.pixmap;
276 tile.pixmap = nullptr;
277 }
278
279 for (int i = 0; i < tile.nTiles; ++i) {
280 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
281 }
282 }
283 } else {
284 QRect tileRect = tile.rect.geometry(width, height);
285 // sets the pixmap of the children tiles. if the tile's size is too
286 // small, discards the children tiles and use the current one
287 // Never join small tiles during a partial update in order to
288 // not lose existing image data
289 if (tileRect.width() * tileRect.height() >= TILES_MAXSIZE || isPartialPixmap) {
290 tile.dirty = isPartialPixmap;
291 tile.partial = isPartialPixmap;
292 if (tile.pixmap) {
293 totalPixels -= tile.pixmap->width() * tile.pixmap->height();
294 delete tile.pixmap;
295 tile.pixmap = nullptr;
296 }
297
298 for (int i = 0; i < tile.nTiles; ++i) {
299 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
300 }
301 } else {
302 // remove children tiles
303 for (int i = 0; i < tile.nTiles; ++i) {
304 deleteTiles(tile.tiles[i]);
305 tile.tiles[i].pixmap = nullptr;
306 }
307
308 delete[] tile.tiles;
309 tile.tiles = nullptr;
310 tile.nTiles = 0;
311
312 // paint tile
313 if (tile.pixmap) {
314 totalPixels -= tile.pixmap->width() * tile.pixmap->height();
315 delete tile.pixmap;
316 }
317 tile.rotation = rotation;
318 if (pixmap) {
319 const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
320 tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft())));
321 totalPixels += tile.pixmap->width() * tile.pixmap->height();
322 } else {
323 tile.pixmap = nullptr;
324 }
325 tile.dirty = isPartialPixmap;
326 tile.partial = isPartialPixmap;
327 }
328 }
329}
330
331bool TilesManager::hasPixmap(const NormalizedRect &rect)
332{
333 NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation);
334 for (const TileNode &tile : std::as_const(d->tiles)) {
335 if (!d->hasPixmap(rotatedRect, tile)) {
336 return false;
337 }
338 }
339
340 return true;
341}
342
343bool TilesManager::Private::hasPixmap(const NormalizedRect &rect, const TileNode &tile) const
344{
345 const NormalizedRect rectIntersection = tile.rect & rect;
346 if (rectIntersection.width() <= 0 || rectIntersection.height() <= 0) {
347 return true;
348 }
349
350 if (tile.nTiles == 0) {
351 return tile.isValid();
352 }
353
354 // all children tiles are clean. doesn't need to go deeper
355 if (!tile.dirty) {
356 return true;
357 }
358
359 for (int i = 0; i < tile.nTiles; ++i) {
360 if (!hasPixmap(rect, tile.tiles[i])) {
361 return false;
362 }
363 }
364
365 return true;
366}
367
368QList<Tile> TilesManager::tilesAt(const NormalizedRect &rect, TileLeaf tileLeaf)
369{
370 QList<Tile> result;
371
372 NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation);
373 for (TileNode &tile : d->tiles) {
374 d->tilesAt(rotatedRect, tile, result, tileLeaf);
375 }
376
377 return result;
378}
379
380void TilesManager::Private::tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf)
381{
382 if (!tile.rect.intersects(rect)) {
383 return;
384 }
385
386 // split big tiles before the requests are made, otherwise we would end up
387 // requesting huge areas unnecessarily
388 splitBigTiles(tile, rect);
389
390 if ((tileLeaf == TerminalTile && tile.nTiles == 0) || (tileLeaf == PixmapTile && tile.pixmap)) {
391 NormalizedRect rotatedRect;
392 if (rotation != Rotation0) {
393 rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
394 } else {
395 rotatedRect = tile.rect;
396 }
397
398 if (tile.pixmap && tileLeaf == PixmapTile && tile.rotation != rotation) {
399 // Lazy tiles rotation
400 int angleToRotate = (rotation - tile.rotation) * 90;
401 int xOffset = 0, yOffset = 0;
402 int w = 0, h = 0;
403 switch (angleToRotate) {
404 case 0:
405 xOffset = 0;
406 yOffset = 0;
407 w = tile.pixmap->width();
408 h = tile.pixmap->height();
409 break;
410 case 90:
411 case -270:
412 xOffset = 0;
413 yOffset = -tile.pixmap->height();
414 w = tile.pixmap->height();
415 h = tile.pixmap->width();
416 break;
417 case 180:
418 case -180:
419 xOffset = -tile.pixmap->width();
420 yOffset = -tile.pixmap->height();
421 w = tile.pixmap->width();
422 h = tile.pixmap->height();
423 break;
424 case 270:
425 case -90:
426 xOffset = -tile.pixmap->width();
427 yOffset = 0;
428 w = tile.pixmap->height();
429 h = tile.pixmap->width();
430 break;
431 }
432 QPixmap *rotatedPixmap = new QPixmap(w, h);
433 QPainter p(rotatedPixmap);
434 p.rotate(angleToRotate);
435 p.translate(xOffset, yOffset);
436 p.drawPixmap(0, 0, *tile.pixmap);
437 p.end();
438
439 delete tile.pixmap;
440 tile.pixmap = rotatedPixmap;
441 tile.rotation = rotation;
442 }
443 result.append(Tile(rotatedRect, tile.pixmap, tile.isValid()));
444 } else {
445 for (int i = 0; i < tile.nTiles; ++i) {
446 tilesAt(rect, tile.tiles[i], result, tileLeaf);
447 }
448 }
449}
450
451qulonglong TilesManager::totalMemory() const
452{
453 return 4 * d->totalPixels;
454}
455
456void TilesManager::cleanupPixmapMemory(qulonglong numberOfBytes, const NormalizedRect &visibleRect, int visiblePageNumber)
457{
458 QList<TileNode *> rankedTiles;
459 for (TileNode &tile : d->tiles) {
460 d->rankTiles(tile, rankedTiles, visibleRect, visiblePageNumber);
461 }
462 std::sort(rankedTiles.begin(), rankedTiles.end(), rankedTilesLessThan);
463
464 while (numberOfBytes > 0 && !rankedTiles.isEmpty()) {
465 TileNode *tile = rankedTiles.takeLast();
466 if (!tile->pixmap) {
467 continue;
468 }
469
470 // do not evict visible pixmaps
471 if (tile->rect.intersects(visibleRect)) {
472 continue;
473 }
474
475 qulonglong pixels = tile->pixmap->width() * tile->pixmap->height();
476 d->totalPixels -= pixels;
477 if (numberOfBytes < 4 * pixels) {
478 numberOfBytes = 0;
479 } else {
480 numberOfBytes -= 4 * pixels;
481 }
482
483 delete tile->pixmap;
484 tile->pixmap = nullptr;
485
486 tile->partial = true;
487
488 d->markParentDirty(*tile);
489 }
490}
491
492void TilesManager::Private::markParentDirty(const TileNode &tile)
493{
494 if (!tile.parent) {
495 return;
496 }
497
498 if (!tile.parent->dirty) {
499 tile.parent->dirty = true;
500 markParentDirty(*tile.parent);
501 }
502}
503
504void TilesManager::Private::rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber)
505{
506 // If the page is visible, visibleRect is not null.
507 // Otherwise we use the number of one of the visible pages to calculate the
508 // distance.
509 // Note that the current page may be visible and yet its pageNumber is
510 // different from visiblePageNumber. Since we only use this value on hidden
511 // pages, any visible page number will fit.
512 if (visibleRect.isNull() && visiblePageNumber < 0) {
513 return;
514 }
515
516 if (tile.pixmap) {
517 // Update distance
518 if (!visibleRect.isNull()) {
519 NormalizedPoint viewportCenter = visibleRect.center();
520 NormalizedPoint tileCenter = tile.rect.center();
521 // Manhattan distance. It's a good and fast approximation.
522 tile.distance = qAbs(viewportCenter.x - tileCenter.x) + qAbs(viewportCenter.y - tileCenter.y);
523 } else {
524 // For non visible pages only the vertical distance is used
525 if (pageNumber < visiblePageNumber) {
526 tile.distance = 1 - tile.rect.bottom;
527 } else {
528 tile.distance = tile.rect.top;
529 }
530 }
531 rankedTiles.append(&tile);
532 } else {
533 for (int i = 0; i < tile.nTiles; ++i) {
534 rankTiles(tile.tiles[i], rankedTiles, visibleRect, visiblePageNumber);
535 }
536 }
537}
538
539bool TilesManager::isRequesting(const NormalizedRect &rect, int pageWidth, int pageHeight) const
540{
541 return rect == d->requestRect && pageWidth == d->requestWidth && pageHeight == d->requestHeight;
542}
543
544void TilesManager::setRequest(const NormalizedRect &rect, int pageWidth, int pageHeight)
545{
546 d->requestRect = rect;
547 d->requestWidth = pageWidth;
548 d->requestHeight = pageHeight;
549}
550
551bool TilesManager::Private::splitBigTiles(TileNode &tile, const NormalizedRect &rect)
552{
553 QRect tileRect = tile.rect.geometry(width, height);
554 if (tileRect.width() * tileRect.height() < TILES_MAXSIZE) {
555 return false;
556 }
557
558 split(tile, rect);
559 return true;
560}
561
562void TilesManager::Private::split(TileNode &tile, const NormalizedRect &rect)
563{
564 if (tile.nTiles != 0) {
565 return;
566 }
567
568 if (rect.isNull() || !tile.rect.intersects(rect)) {
569 return;
570 }
571
572 tile.nTiles = 4;
573 tile.tiles = new TileNode[4];
574 double hCenter = (tile.rect.left + tile.rect.right) / 2;
575 double vCenter = (tile.rect.top + tile.rect.bottom) / 2;
576
577 tile.tiles[0].rect = NormalizedRect(tile.rect.left, tile.rect.top, hCenter, vCenter);
578 tile.tiles[1].rect = NormalizedRect(hCenter, tile.rect.top, tile.rect.right, vCenter);
579 tile.tiles[2].rect = NormalizedRect(tile.rect.left, vCenter, hCenter, tile.rect.bottom);
580 tile.tiles[3].rect = NormalizedRect(hCenter, vCenter, tile.rect.right, tile.rect.bottom);
581
582 for (int i = 0; i < tile.nTiles; ++i) {
583 tile.tiles[i].parent = &tile;
584 splitBigTiles(tile.tiles[i], rect);
585 }
586}
587
588NormalizedRect TilesManager::fromRotatedRect(const NormalizedRect &rect, Rotation rotation)
589{
590 if (rotation == Rotation0) {
591 return rect;
592 }
593
594 NormalizedRect newRect;
595 switch (rotation) {
596 case Rotation90:
597 newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left);
598 break;
599 case Rotation180:
600 newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top);
601 break;
602 case Rotation270:
603 newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right);
604 break;
605 default:
606 newRect = rect;
607 break;
608 }
609
610 return newRect;
611}
612
613NormalizedRect TilesManager::toRotatedRect(const NormalizedRect &rect, Rotation rotation)
614{
615 if (rotation == Rotation0) {
616 return rect;
617 }
618
619 NormalizedRect newRect;
620 switch (rotation) {
621 case Rotation90:
622 newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right);
623 break;
624 case Rotation180:
625 newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top);
626 break;
627 case Rotation270:
628 newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left);
629 break;
630 default:
631 newRect = rect;
632 break;
633 }
634
635 return newRect;
636}
637
638TileNode::TileNode()
639 : pixmap(nullptr)
640 , rotation(Rotation0)
641 , dirty(true)
642 , partial(true)
643 , distance(-1)
644 , tiles(nullptr)
645 , nTiles(0)
646 , parent(nullptr)
647{
648}
649
650bool TileNode::isValid() const
651{
652 return pixmap && !dirty;
653}
654
655class Tile::Private
656{
657public:
658 Private();
659
660 NormalizedRect rect;
661 QPixmap *pixmap;
662 bool isValid;
663};
664
665Tile::Private::Private()
666 : pixmap(nullptr)
667 , isValid(false)
668{
669}
670
671Tile::Tile(const NormalizedRect &rect, QPixmap *pixmap, bool isValid)
672 : d(new Tile::Private)
673{
674 d->rect = rect;
675 d->pixmap = pixmap;
676 d->isValid = isValid;
677}
678
679Tile::Tile(const Tile &t)
680 : d(new Tile::Private)
681{
682 d->rect = t.d->rect;
683 d->pixmap = t.d->pixmap;
684 d->isValid = t.d->isValid;
685}
686
687Tile &Tile::operator=(const Tile &other)
688{
689 if (this == &other) {
690 return *this;
691 }
692
693 d->rect = other.d->rect;
694 d->pixmap = other.d->pixmap;
695 d->isValid = other.d->isValid;
696
697 return *this;
698}
699
700Tile::~Tile()
701{
702 delete d;
703}
704
706{
707 return d->rect;
708}
709
711{
712 return d->pixmap;
713}
714
715bool Tile::isValid() const
716{
717 return d->isValid;
718}
NormalizedPoint is a helper class which stores the coordinates of a normalized point.
Definition area.h:117
double x
The normalized x coordinate.
Definition area.h:166
double y
The normalized y coordinate.
Definition area.h:171
A NormalizedRect is a rectangle which can be defined by two NormalizedPoints.
Definition area.h:189
double bottom
The normalized bottom coordinate.
Definition area.h:435
double right
The normalized right coordinate.
Definition area.h:430
double height() const
Definition area.h:412
NormalizedPoint center() const
Returns the center of the rectangle.
Definition area.cpp:220
double left
The normalized left coordinate.
Definition area.h:420
double width() const
Definition area.h:406
QRect geometry(int xScale, int yScale) const
Returns the rectangle mapped to a reference area of xScale x yScale.
Definition area.cpp:232
double top
The normalized top coordinate.
Definition area.h:425
bool isNull() const
Returns whether this normalized rectangle is a null normalized rect.
Definition area.cpp:155
This class represents a rectangular portion of a page.
Definition tile.h:23
NormalizedRect rect() const
Location of the tile.
bool isValid() const
True if the pixmap is available and updated.
QPixmap * pixmap() const
Pixmap (may also be NULL)
bool isValid(QStringView ifopt)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
global.h
Definition action.h:17
Rotation
A rotation.
Definition global.h:46
@ Rotation270
Rotated 2700 degrees clockwise.
Definition global.h:50
@ Rotation90
Rotated 90 degrees clockwise.
Definition global.h:48
@ Rotation180
Rotated 180 degrees clockwise.
Definition global.h:49
@ Rotation0
Not rotated.
Definition global.h:47
void append(QList< T > &&value)
iterator begin()
iterator end()
bool isEmpty() const const
value_type takeLast()
QPixmap copy(const QRect &rectangle) const const
QSize size() const const
int height() const const
QSize size() const const
QPoint topLeft() const const
QRect translated(const QPoint &offset) const const
int width() const const
void transpose()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:02:13 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.