okular/core/tilesmanager.cpp
Sune Vuorela 792425fc77 s/qAsConst/std::as_const/
We now definitely has std::as_const available and Qt has started nagging
about converting to std::as_const.

Implementation is the same for both functions, and qAsConst was a
stop-gap measure until std::as_const was sufficiently available.
2023-12-19 11:46:24 +00:00

718 lines
20 KiB
C++

/*
SPDX-FileCopyrightText: 2012 Mailson Menezes <mailson@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "tilesmanager_p.h"
#include <QList>
#include <QPainter>
#include <QPixmap>
#include <qmath.h>
#include "tile.h"
#define TILES_MAXSIZE 2000000
using namespace Okular;
static bool rankedTilesLessThan(TileNode *t1, TileNode *t2)
{
// Order tiles by its dirty state and then by distance from the viewport.
if (t1->dirty == t2->dirty) {
return t1->distance < t2->distance;
}
return !t1->dirty;
}
class TilesManager::Private
{
public:
Private();
bool hasPixmap(const NormalizedRect &rect, const TileNode &tile) const;
void tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf);
void setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap);
/**
* Mark @p tile and all its children as dirty
*/
static void markDirty(TileNode &tile);
/**
* Deletes all tiles, recursively
*/
void deleteTiles(const TileNode &tile);
void markParentDirty(const TileNode &tile);
void rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber);
/**
* Since the tile can be large enough to occupy a significant amount of
* space, they may be split in more tiles. This operation is performed
* when the tiles of a certain region is requested and they are bigger
* than an arbitrary value. Only tiles intersecting the desired region
* are split. There's no need to do this for the entire page.
*/
void split(TileNode &tile, const NormalizedRect &rect);
/**
* Checks whether the tile's size is bigger than an arbitrary value and
* performs the split operation returning true.
* Otherwise it just returns false, without performing any operation.
*/
bool splitBigTiles(TileNode &tile, const NormalizedRect &rect);
// The page is split in a 4x4 grid of tiles
TileNode tiles[16];
int width;
int height;
int pageNumber;
qulonglong totalPixels;
Rotation rotation;
NormalizedRect visibleRect;
NormalizedRect requestRect;
int requestWidth;
int requestHeight;
};
TilesManager::Private::Private()
: width(0)
, height(0)
, pageNumber(0)
, totalPixels(0)
, rotation(Rotation0)
, requestRect(NormalizedRect())
, requestWidth(0)
, requestHeight(0)
{
}
TilesManager::TilesManager(int pageNumber, int width, int height, Rotation rotation)
: d(new Private)
{
d->pageNumber = pageNumber;
d->width = width;
d->height = height;
d->rotation = rotation;
// The page is split in a 4x4 grid of tiles
const double dim = 0.25;
for (int i = 0; i < 16; ++i) {
int x = i % 4;
int y = i / 4;
d->tiles[i].rect = NormalizedRect(x * dim, y * dim, x * dim + dim, y * dim + dim);
}
}
TilesManager::~TilesManager()
{
for (const TileNode &tile : d->tiles) {
d->deleteTiles(tile);
}
delete d;
}
void TilesManager::Private::deleteTiles(const TileNode &tile)
{
if (tile.pixmap) {
totalPixels -= tile.pixmap->width() * tile.pixmap->height();
delete tile.pixmap;
}
if (tile.nTiles > 0) {
for (int i = 0; i < tile.nTiles; ++i) {
deleteTiles(tile.tiles[i]);
}
delete[] tile.tiles;
}
}
void TilesManager::setSize(int width, int height)
{
if (width == d->width && height == d->height) {
return;
}
d->width = width;
d->height = height;
markDirty();
}
int TilesManager::width() const
{
return d->width;
}
int TilesManager::height() const
{
return d->height;
}
void TilesManager::setRotation(Rotation rotation)
{
if (rotation == d->rotation) {
return;
}
d->rotation = rotation;
}
Rotation TilesManager::rotation() const
{
return d->rotation;
}
void TilesManager::markDirty()
{
for (TileNode &tile : d->tiles) {
TilesManager::Private::markDirty(tile);
}
}
void TilesManager::Private::markDirty(TileNode &tile)
{
tile.dirty = true;
for (int i = 0; i < tile.nTiles; ++i) {
markDirty(tile.tiles[i]);
}
}
void TilesManager::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap)
{
const NormalizedRect rotatedRect = TilesManager::fromRotatedRect(rect, d->rotation);
if (!d->requestRect.isNull()) {
if (!(d->requestRect == rect)) {
return;
}
if (pixmap) {
// Check whether the pixmap has the same absolute size of the expected
// request.
// If the document is rotated, rotate requestRect back to the original
// rotation before comparing to pixmap's size. This is to avoid
// conversion issues. The pixmap request was made using an unrotated
// rect.
QSize pixmapSize = pixmap->size();
int w = width();
int h = height();
if (d->rotation % 2) {
std::swap(w, h);
pixmapSize.transpose();
}
if (rotatedRect.geometry(w, h).size() != pixmapSize) {
return;
}
}
d->requestRect = NormalizedRect();
}
for (TileNode &tile : d->tiles) {
d->setPixmap(pixmap, rotatedRect, tile, isPartialPixmap);
}
}
void TilesManager::Private::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap)
{
QRect pixmapRect = TilesManager::toRotatedRect(rect, rotation).geometry(width, height);
// Exclude tiles outside the viewport
if (!tile.rect.intersects(rect)) {
return;
}
// Avoid painting partial pixmaps over tiles that already have a fully rendered pixmap, even if dirty
if (isPartialPixmap && tile.pixmap != nullptr && !tile.partial) {
return;
}
// if the tile is not entirely within the viewport (the tile intersects an
// edged of the viewport), attempt to set the pixmap in the children tiles
if (!((tile.rect & rect) == tile.rect)) {
// paint children tiles
if (tile.nTiles > 0) {
for (int i = 0; i < tile.nTiles; ++i) {
setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
}
delete tile.pixmap;
tile.pixmap = nullptr;
}
// We could paint the pixmap over part of the tile here, but
// there is little reason to as it will usually be offscreen
// and it will be overwritten later if more tiles enter the screen,
// as we only track the dirty state of whole tiles, not rects.
return;
}
// the tile lies entirely within the viewport
if (tile.nTiles == 0) {
tile.dirty = isPartialPixmap;
tile.partial = isPartialPixmap;
// check whether the tile size is big and split it if necessary
if (!splitBigTiles(tile, rect)) {
if (tile.pixmap) {
totalPixels -= tile.pixmap->width() * tile.pixmap->height();
delete tile.pixmap;
}
tile.rotation = rotation;
if (pixmap) {
const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft())));
totalPixels += tile.pixmap->width() * tile.pixmap->height();
} else {
tile.pixmap = nullptr;
}
} else {
if (tile.pixmap) {
totalPixels -= tile.pixmap->width() * tile.pixmap->height();
delete tile.pixmap;
tile.pixmap = nullptr;
}
for (int i = 0; i < tile.nTiles; ++i) {
setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
}
}
} else {
QRect tileRect = tile.rect.geometry(width, height);
// sets the pixmap of the children tiles. if the tile's size is too
// small, discards the children tiles and use the current one
// Never join small tiles during a partial update in order to
// not lose existing image data
if (tileRect.width() * tileRect.height() >= TILES_MAXSIZE || isPartialPixmap) {
tile.dirty = isPartialPixmap;
tile.partial = isPartialPixmap;
if (tile.pixmap) {
totalPixels -= tile.pixmap->width() * tile.pixmap->height();
delete tile.pixmap;
tile.pixmap = nullptr;
}
for (int i = 0; i < tile.nTiles; ++i) {
setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap);
}
} else {
// remove children tiles
for (int i = 0; i < tile.nTiles; ++i) {
deleteTiles(tile.tiles[i]);
tile.tiles[i].pixmap = nullptr;
}
delete[] tile.tiles;
tile.tiles = nullptr;
tile.nTiles = 0;
// paint tile
if (tile.pixmap) {
totalPixels -= tile.pixmap->width() * tile.pixmap->height();
delete tile.pixmap;
}
tile.rotation = rotation;
if (pixmap) {
const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft())));
totalPixels += tile.pixmap->width() * tile.pixmap->height();
} else {
tile.pixmap = nullptr;
}
tile.dirty = isPartialPixmap;
tile.partial = isPartialPixmap;
}
}
}
bool TilesManager::hasPixmap(const NormalizedRect &rect)
{
NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation);
for (const TileNode &tile : std::as_const(d->tiles)) {
if (!d->hasPixmap(rotatedRect, tile)) {
return false;
}
}
return true;
}
bool TilesManager::Private::hasPixmap(const NormalizedRect &rect, const TileNode &tile) const
{
const NormalizedRect rectIntersection = tile.rect & rect;
if (rectIntersection.width() <= 0 || rectIntersection.height() <= 0) {
return true;
}
if (tile.nTiles == 0) {
return tile.isValid();
}
// all children tiles are clean. doesn't need to go deeper
if (!tile.dirty) {
return true;
}
for (int i = 0; i < tile.nTiles; ++i) {
if (!hasPixmap(rect, tile.tiles[i])) {
return false;
}
}
return true;
}
QList<Tile> TilesManager::tilesAt(const NormalizedRect &rect, TileLeaf tileLeaf)
{
QList<Tile> result;
NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation);
for (TileNode &tile : d->tiles) {
d->tilesAt(rotatedRect, tile, result, tileLeaf);
}
return result;
}
void TilesManager::Private::tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf)
{
if (!tile.rect.intersects(rect)) {
return;
}
// split big tiles before the requests are made, otherwise we would end up
// requesting huge areas unnecessarily
splitBigTiles(tile, rect);
if ((tileLeaf == TerminalTile && tile.nTiles == 0) || (tileLeaf == PixmapTile && tile.pixmap)) {
NormalizedRect rotatedRect;
if (rotation != Rotation0) {
rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation);
} else {
rotatedRect = tile.rect;
}
if (tile.pixmap && tileLeaf == PixmapTile && tile.rotation != rotation) {
// Lazy tiles rotation
int angleToRotate = (rotation - tile.rotation) * 90;
int xOffset = 0, yOffset = 0;
int w = 0, h = 0;
switch (angleToRotate) {
case 0:
xOffset = 0;
yOffset = 0;
w = tile.pixmap->width();
h = tile.pixmap->height();
break;
case 90:
case -270:
xOffset = 0;
yOffset = -tile.pixmap->height();
w = tile.pixmap->height();
h = tile.pixmap->width();
break;
case 180:
case -180:
xOffset = -tile.pixmap->width();
yOffset = -tile.pixmap->height();
w = tile.pixmap->width();
h = tile.pixmap->height();
break;
case 270:
case -90:
xOffset = -tile.pixmap->width();
yOffset = 0;
w = tile.pixmap->height();
h = tile.pixmap->width();
break;
}
QPixmap *rotatedPixmap = new QPixmap(w, h);
QPainter p(rotatedPixmap);
p.rotate(angleToRotate);
p.translate(xOffset, yOffset);
p.drawPixmap(0, 0, *tile.pixmap);
p.end();
delete tile.pixmap;
tile.pixmap = rotatedPixmap;
tile.rotation = rotation;
}
result.append(Tile(rotatedRect, tile.pixmap, tile.isValid()));
} else {
for (int i = 0; i < tile.nTiles; ++i) {
tilesAt(rect, tile.tiles[i], result, tileLeaf);
}
}
}
qulonglong TilesManager::totalMemory() const
{
return 4 * d->totalPixels;
}
void TilesManager::cleanupPixmapMemory(qulonglong numberOfBytes, const NormalizedRect &visibleRect, int visiblePageNumber)
{
QList<TileNode *> rankedTiles;
for (TileNode &tile : d->tiles) {
d->rankTiles(tile, rankedTiles, visibleRect, visiblePageNumber);
}
std::sort(rankedTiles.begin(), rankedTiles.end(), rankedTilesLessThan);
while (numberOfBytes > 0 && !rankedTiles.isEmpty()) {
TileNode *tile = rankedTiles.takeLast();
if (!tile->pixmap) {
continue;
}
// do not evict visible pixmaps
if (tile->rect.intersects(visibleRect)) {
continue;
}
qulonglong pixels = tile->pixmap->width() * tile->pixmap->height();
d->totalPixels -= pixels;
if (numberOfBytes < 4 * pixels) {
numberOfBytes = 0;
} else {
numberOfBytes -= 4 * pixels;
}
delete tile->pixmap;
tile->pixmap = nullptr;
tile->partial = true;
d->markParentDirty(*tile);
}
}
void TilesManager::Private::markParentDirty(const TileNode &tile)
{
if (!tile.parent) {
return;
}
if (!tile.parent->dirty) {
tile.parent->dirty = true;
markParentDirty(*tile.parent);
}
}
void TilesManager::Private::rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber)
{
// If the page is visible, visibleRect is not null.
// Otherwise we use the number of one of the visible pages to calculate the
// distance.
// Note that the current page may be visible and yet its pageNumber is
// different from visiblePageNumber. Since we only use this value on hidden
// pages, any visible page number will fit.
if (visibleRect.isNull() && visiblePageNumber < 0) {
return;
}
if (tile.pixmap) {
// Update distance
if (!visibleRect.isNull()) {
NormalizedPoint viewportCenter = visibleRect.center();
NormalizedPoint tileCenter = tile.rect.center();
// Manhattan distance. It's a good and fast approximation.
tile.distance = qAbs(viewportCenter.x - tileCenter.x) + qAbs(viewportCenter.y - tileCenter.y);
} else {
// For non visible pages only the vertical distance is used
if (pageNumber < visiblePageNumber) {
tile.distance = 1 - tile.rect.bottom;
} else {
tile.distance = tile.rect.top;
}
}
rankedTiles.append(&tile);
} else {
for (int i = 0; i < tile.nTiles; ++i) {
rankTiles(tile.tiles[i], rankedTiles, visibleRect, visiblePageNumber);
}
}
}
bool TilesManager::isRequesting(const NormalizedRect &rect, int pageWidth, int pageHeight) const
{
return rect == d->requestRect && pageWidth == d->requestWidth && pageHeight == d->requestHeight;
}
void TilesManager::setRequest(const NormalizedRect &rect, int pageWidth, int pageHeight)
{
d->requestRect = rect;
d->requestWidth = pageWidth;
d->requestHeight = pageHeight;
}
bool TilesManager::Private::splitBigTiles(TileNode &tile, const NormalizedRect &rect)
{
QRect tileRect = tile.rect.geometry(width, height);
if (tileRect.width() * tileRect.height() < TILES_MAXSIZE) {
return false;
}
split(tile, rect);
return true;
}
void TilesManager::Private::split(TileNode &tile, const NormalizedRect &rect)
{
if (tile.nTiles != 0) {
return;
}
if (rect.isNull() || !tile.rect.intersects(rect)) {
return;
}
tile.nTiles = 4;
tile.tiles = new TileNode[4];
double hCenter = (tile.rect.left + tile.rect.right) / 2;
double vCenter = (tile.rect.top + tile.rect.bottom) / 2;
tile.tiles[0].rect = NormalizedRect(tile.rect.left, tile.rect.top, hCenter, vCenter);
tile.tiles[1].rect = NormalizedRect(hCenter, tile.rect.top, tile.rect.right, vCenter);
tile.tiles[2].rect = NormalizedRect(tile.rect.left, vCenter, hCenter, tile.rect.bottom);
tile.tiles[3].rect = NormalizedRect(hCenter, vCenter, tile.rect.right, tile.rect.bottom);
for (int i = 0; i < tile.nTiles; ++i) {
tile.tiles[i].parent = &tile;
splitBigTiles(tile.tiles[i], rect);
}
}
NormalizedRect TilesManager::fromRotatedRect(const NormalizedRect &rect, Rotation rotation)
{
if (rotation == Rotation0) {
return rect;
}
NormalizedRect newRect;
switch (rotation) {
case Rotation90:
newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left);
break;
case Rotation180:
newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top);
break;
case Rotation270:
newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right);
break;
default:
newRect = rect;
break;
}
return newRect;
}
NormalizedRect TilesManager::toRotatedRect(const NormalizedRect &rect, Rotation rotation)
{
if (rotation == Rotation0) {
return rect;
}
NormalizedRect newRect;
switch (rotation) {
case Rotation90:
newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right);
break;
case Rotation180:
newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top);
break;
case Rotation270:
newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left);
break;
default:
newRect = rect;
break;
}
return newRect;
}
TileNode::TileNode()
: pixmap(nullptr)
, rotation(Rotation0)
, dirty(true)
, partial(true)
, distance(-1)
, tiles(nullptr)
, nTiles(0)
, parent(nullptr)
{
}
bool TileNode::isValid() const
{
return pixmap && !dirty;
}
class Tile::Private
{
public:
Private();
NormalizedRect rect;
QPixmap *pixmap;
bool isValid;
};
Tile::Private::Private()
: pixmap(nullptr)
, isValid(false)
{
}
Tile::Tile(const NormalizedRect &rect, QPixmap *pixmap, bool isValid)
: d(new Tile::Private)
{
d->rect = rect;
d->pixmap = pixmap;
d->isValid = isValid;
}
Tile::Tile(const Tile &t)
: d(new Tile::Private)
{
d->rect = t.d->rect;
d->pixmap = t.d->pixmap;
d->isValid = t.d->isValid;
}
Tile &Tile::operator=(const Tile &other)
{
if (this == &other) {
return *this;
}
d->rect = other.d->rect;
d->pixmap = other.d->pixmap;
d->isValid = other.d->isValid;
return *this;
}
Tile::~Tile()
{
delete d;
}
NormalizedRect Tile::rect() const
{
return d->rect;
}
QPixmap *Tile::pixmap() const
{
return d->pixmap;
}
bool Tile::isValid() const
{
return d->isValid;
}