File indexing completed on 2024-04-28 04:32:47
0001 /* 0002 SPDX-FileCopyrightText: 2012 Mailson Menezes <mailson@gmail.com> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "tilesmanager_p.h" 0007 0008 #include <QList> 0009 #include <QPainter> 0010 #include <QPixmap> 0011 #include <qmath.h> 0012 0013 #include "tile.h" 0014 0015 #define TILES_MAXSIZE 2000000 0016 0017 using namespace Okular; 0018 0019 static bool rankedTilesLessThan(TileNode *t1, TileNode *t2) 0020 { 0021 // Order tiles by its dirty state and then by distance from the viewport. 0022 if (t1->dirty == t2->dirty) { 0023 return t1->distance < t2->distance; 0024 } 0025 0026 return !t1->dirty; 0027 } 0028 0029 class TilesManager::Private 0030 { 0031 public: 0032 Private(); 0033 0034 bool hasPixmap(const NormalizedRect &rect, const TileNode &tile) const; 0035 void tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf); 0036 void setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap); 0037 0038 /** 0039 * Mark @p tile and all its children as dirty 0040 */ 0041 static void markDirty(TileNode &tile); 0042 0043 /** 0044 * Deletes all tiles, recursively 0045 */ 0046 void deleteTiles(const TileNode &tile); 0047 0048 void markParentDirty(const TileNode &tile); 0049 void rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber); 0050 /** 0051 * Since the tile can be large enough to occupy a significant amount of 0052 * space, they may be split in more tiles. This operation is performed 0053 * when the tiles of a certain region is requested and they are bigger 0054 * than an arbitrary value. Only tiles intersecting the desired region 0055 * are split. There's no need to do this for the entire page. 0056 */ 0057 void split(TileNode &tile, const NormalizedRect &rect); 0058 0059 /** 0060 * Checks whether the tile's size is bigger than an arbitrary value and 0061 * performs the split operation returning true. 0062 * Otherwise it just returns false, without performing any operation. 0063 */ 0064 bool splitBigTiles(TileNode &tile, const NormalizedRect &rect); 0065 0066 // The page is split in a 4x4 grid of tiles 0067 TileNode tiles[16]; 0068 int width; 0069 int height; 0070 int pageNumber; 0071 qulonglong totalPixels; 0072 Rotation rotation; 0073 NormalizedRect visibleRect; 0074 NormalizedRect requestRect; 0075 int requestWidth; 0076 int requestHeight; 0077 }; 0078 0079 TilesManager::Private::Private() 0080 : width(0) 0081 , height(0) 0082 , pageNumber(0) 0083 , totalPixels(0) 0084 , rotation(Rotation0) 0085 , requestRect(NormalizedRect()) 0086 , requestWidth(0) 0087 , requestHeight(0) 0088 { 0089 } 0090 0091 TilesManager::TilesManager(int pageNumber, int width, int height, Rotation rotation) 0092 : d(new Private) 0093 { 0094 d->pageNumber = pageNumber; 0095 d->width = width; 0096 d->height = height; 0097 d->rotation = rotation; 0098 0099 // The page is split in a 4x4 grid of tiles 0100 const double dim = 0.25; 0101 for (int i = 0; i < 16; ++i) { 0102 int x = i % 4; 0103 int y = i / 4; 0104 d->tiles[i].rect = NormalizedRect(x * dim, y * dim, x * dim + dim, y * dim + dim); 0105 } 0106 } 0107 0108 TilesManager::~TilesManager() 0109 { 0110 for (const TileNode &tile : d->tiles) { 0111 d->deleteTiles(tile); 0112 } 0113 0114 delete d; 0115 } 0116 0117 void TilesManager::Private::deleteTiles(const TileNode &tile) 0118 { 0119 if (tile.pixmap) { 0120 totalPixels -= tile.pixmap->width() * tile.pixmap->height(); 0121 delete tile.pixmap; 0122 } 0123 0124 if (tile.nTiles > 0) { 0125 for (int i = 0; i < tile.nTiles; ++i) { 0126 deleteTiles(tile.tiles[i]); 0127 } 0128 0129 delete[] tile.tiles; 0130 } 0131 } 0132 0133 void TilesManager::setSize(int width, int height) 0134 { 0135 if (width == d->width && height == d->height) { 0136 return; 0137 } 0138 0139 d->width = width; 0140 d->height = height; 0141 0142 markDirty(); 0143 } 0144 0145 int TilesManager::width() const 0146 { 0147 return d->width; 0148 } 0149 0150 int TilesManager::height() const 0151 { 0152 return d->height; 0153 } 0154 0155 void TilesManager::setRotation(Rotation rotation) 0156 { 0157 if (rotation == d->rotation) { 0158 return; 0159 } 0160 0161 d->rotation = rotation; 0162 } 0163 0164 Rotation TilesManager::rotation() const 0165 { 0166 return d->rotation; 0167 } 0168 0169 void TilesManager::markDirty() 0170 { 0171 for (TileNode &tile : d->tiles) { 0172 TilesManager::Private::markDirty(tile); 0173 } 0174 } 0175 0176 void TilesManager::Private::markDirty(TileNode &tile) 0177 { 0178 tile.dirty = true; 0179 0180 for (int i = 0; i < tile.nTiles; ++i) { 0181 markDirty(tile.tiles[i]); 0182 } 0183 } 0184 0185 void TilesManager::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap) 0186 { 0187 const NormalizedRect rotatedRect = TilesManager::fromRotatedRect(rect, d->rotation); 0188 if (!d->requestRect.isNull()) { 0189 if (!(d->requestRect == rect)) { 0190 return; 0191 } 0192 0193 if (pixmap) { 0194 // Check whether the pixmap has the same absolute size of the expected 0195 // request. 0196 // If the document is rotated, rotate requestRect back to the original 0197 // rotation before comparing to pixmap's size. This is to avoid 0198 // conversion issues. The pixmap request was made using an unrotated 0199 // rect. 0200 QSize pixmapSize = pixmap->size(); 0201 int w = width(); 0202 int h = height(); 0203 if (d->rotation % 2) { 0204 std::swap(w, h); 0205 pixmapSize.transpose(); 0206 } 0207 0208 if (rotatedRect.geometry(w, h).size() != pixmapSize) { 0209 return; 0210 } 0211 } 0212 0213 d->requestRect = NormalizedRect(); 0214 } 0215 0216 for (TileNode &tile : d->tiles) { 0217 d->setPixmap(pixmap, rotatedRect, tile, isPartialPixmap); 0218 } 0219 } 0220 0221 void TilesManager::Private::setPixmap(const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap) 0222 { 0223 QRect pixmapRect = TilesManager::toRotatedRect(rect, rotation).geometry(width, height); 0224 0225 // Exclude tiles outside the viewport 0226 if (!tile.rect.intersects(rect)) { 0227 return; 0228 } 0229 // Avoid painting partial pixmaps over tiles that already have a fully rendered pixmap, even if dirty 0230 if (isPartialPixmap && tile.pixmap != nullptr && !tile.partial) { 0231 return; 0232 } 0233 0234 // if the tile is not entirely within the viewport (the tile intersects an 0235 // edged of the viewport), attempt to set the pixmap in the children tiles 0236 if (!((tile.rect & rect) == tile.rect)) { 0237 // paint children tiles 0238 if (tile.nTiles > 0) { 0239 for (int i = 0; i < tile.nTiles; ++i) { 0240 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap); 0241 } 0242 0243 delete tile.pixmap; 0244 tile.pixmap = nullptr; 0245 } 0246 // We could paint the pixmap over part of the tile here, but 0247 // there is little reason to as it will usually be offscreen 0248 // and it will be overwritten later if more tiles enter the screen, 0249 // as we only track the dirty state of whole tiles, not rects. 0250 return; 0251 } 0252 0253 // the tile lies entirely within the viewport 0254 if (tile.nTiles == 0) { 0255 tile.dirty = isPartialPixmap; 0256 tile.partial = isPartialPixmap; 0257 0258 // check whether the tile size is big and split it if necessary 0259 if (!splitBigTiles(tile, rect)) { 0260 if (tile.pixmap) { 0261 totalPixels -= tile.pixmap->width() * tile.pixmap->height(); 0262 delete tile.pixmap; 0263 } 0264 tile.rotation = rotation; 0265 if (pixmap) { 0266 const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation); 0267 tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft()))); 0268 totalPixels += tile.pixmap->width() * tile.pixmap->height(); 0269 } else { 0270 tile.pixmap = nullptr; 0271 } 0272 } else { 0273 if (tile.pixmap) { 0274 totalPixels -= tile.pixmap->width() * tile.pixmap->height(); 0275 delete tile.pixmap; 0276 tile.pixmap = nullptr; 0277 } 0278 0279 for (int i = 0; i < tile.nTiles; ++i) { 0280 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap); 0281 } 0282 } 0283 } else { 0284 QRect tileRect = tile.rect.geometry(width, height); 0285 // sets the pixmap of the children tiles. if the tile's size is too 0286 // small, discards the children tiles and use the current one 0287 // Never join small tiles during a partial update in order to 0288 // not lose existing image data 0289 if (tileRect.width() * tileRect.height() >= TILES_MAXSIZE || isPartialPixmap) { 0290 tile.dirty = isPartialPixmap; 0291 tile.partial = isPartialPixmap; 0292 if (tile.pixmap) { 0293 totalPixels -= tile.pixmap->width() * tile.pixmap->height(); 0294 delete tile.pixmap; 0295 tile.pixmap = nullptr; 0296 } 0297 0298 for (int i = 0; i < tile.nTiles; ++i) { 0299 setPixmap(pixmap, rect, tile.tiles[i], isPartialPixmap); 0300 } 0301 } else { 0302 // remove children tiles 0303 for (int i = 0; i < tile.nTiles; ++i) { 0304 deleteTiles(tile.tiles[i]); 0305 tile.tiles[i].pixmap = nullptr; 0306 } 0307 0308 delete[] tile.tiles; 0309 tile.tiles = nullptr; 0310 tile.nTiles = 0; 0311 0312 // paint tile 0313 if (tile.pixmap) { 0314 totalPixels -= tile.pixmap->width() * tile.pixmap->height(); 0315 delete tile.pixmap; 0316 } 0317 tile.rotation = rotation; 0318 if (pixmap) { 0319 const NormalizedRect rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation); 0320 tile.pixmap = new QPixmap(pixmap->copy(rotatedRect.geometry(width, height).translated(-pixmapRect.topLeft()))); 0321 totalPixels += tile.pixmap->width() * tile.pixmap->height(); 0322 } else { 0323 tile.pixmap = nullptr; 0324 } 0325 tile.dirty = isPartialPixmap; 0326 tile.partial = isPartialPixmap; 0327 } 0328 } 0329 } 0330 0331 bool TilesManager::hasPixmap(const NormalizedRect &rect) 0332 { 0333 NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation); 0334 for (const TileNode &tile : std::as_const(d->tiles)) { 0335 if (!d->hasPixmap(rotatedRect, tile)) { 0336 return false; 0337 } 0338 } 0339 0340 return true; 0341 } 0342 0343 bool TilesManager::Private::hasPixmap(const NormalizedRect &rect, const TileNode &tile) const 0344 { 0345 const NormalizedRect rectIntersection = tile.rect & rect; 0346 if (rectIntersection.width() <= 0 || rectIntersection.height() <= 0) { 0347 return true; 0348 } 0349 0350 if (tile.nTiles == 0) { 0351 return tile.isValid(); 0352 } 0353 0354 // all children tiles are clean. doesn't need to go deeper 0355 if (!tile.dirty) { 0356 return true; 0357 } 0358 0359 for (int i = 0; i < tile.nTiles; ++i) { 0360 if (!hasPixmap(rect, tile.tiles[i])) { 0361 return false; 0362 } 0363 } 0364 0365 return true; 0366 } 0367 0368 QList<Tile> TilesManager::tilesAt(const NormalizedRect &rect, TileLeaf tileLeaf) 0369 { 0370 QList<Tile> result; 0371 0372 NormalizedRect rotatedRect = fromRotatedRect(rect, d->rotation); 0373 for (TileNode &tile : d->tiles) { 0374 d->tilesAt(rotatedRect, tile, result, tileLeaf); 0375 } 0376 0377 return result; 0378 } 0379 0380 void TilesManager::Private::tilesAt(const NormalizedRect &rect, TileNode &tile, QList<Tile> &result, TileLeaf tileLeaf) 0381 { 0382 if (!tile.rect.intersects(rect)) { 0383 return; 0384 } 0385 0386 // split big tiles before the requests are made, otherwise we would end up 0387 // requesting huge areas unnecessarily 0388 splitBigTiles(tile, rect); 0389 0390 if ((tileLeaf == TerminalTile && tile.nTiles == 0) || (tileLeaf == PixmapTile && tile.pixmap)) { 0391 NormalizedRect rotatedRect; 0392 if (rotation != Rotation0) { 0393 rotatedRect = TilesManager::toRotatedRect(tile.rect, rotation); 0394 } else { 0395 rotatedRect = tile.rect; 0396 } 0397 0398 if (tile.pixmap && tileLeaf == PixmapTile && tile.rotation != rotation) { 0399 // Lazy tiles rotation 0400 int angleToRotate = (rotation - tile.rotation) * 90; 0401 int xOffset = 0, yOffset = 0; 0402 int w = 0, h = 0; 0403 switch (angleToRotate) { 0404 case 0: 0405 xOffset = 0; 0406 yOffset = 0; 0407 w = tile.pixmap->width(); 0408 h = tile.pixmap->height(); 0409 break; 0410 case 90: 0411 case -270: 0412 xOffset = 0; 0413 yOffset = -tile.pixmap->height(); 0414 w = tile.pixmap->height(); 0415 h = tile.pixmap->width(); 0416 break; 0417 case 180: 0418 case -180: 0419 xOffset = -tile.pixmap->width(); 0420 yOffset = -tile.pixmap->height(); 0421 w = tile.pixmap->width(); 0422 h = tile.pixmap->height(); 0423 break; 0424 case 270: 0425 case -90: 0426 xOffset = -tile.pixmap->width(); 0427 yOffset = 0; 0428 w = tile.pixmap->height(); 0429 h = tile.pixmap->width(); 0430 break; 0431 } 0432 QPixmap *rotatedPixmap = new QPixmap(w, h); 0433 QPainter p(rotatedPixmap); 0434 p.rotate(angleToRotate); 0435 p.translate(xOffset, yOffset); 0436 p.drawPixmap(0, 0, *tile.pixmap); 0437 p.end(); 0438 0439 delete tile.pixmap; 0440 tile.pixmap = rotatedPixmap; 0441 tile.rotation = rotation; 0442 } 0443 result.append(Tile(rotatedRect, tile.pixmap, tile.isValid())); 0444 } else { 0445 for (int i = 0; i < tile.nTiles; ++i) { 0446 tilesAt(rect, tile.tiles[i], result, tileLeaf); 0447 } 0448 } 0449 } 0450 0451 qulonglong TilesManager::totalMemory() const 0452 { 0453 return 4 * d->totalPixels; 0454 } 0455 0456 void TilesManager::cleanupPixmapMemory(qulonglong numberOfBytes, const NormalizedRect &visibleRect, int visiblePageNumber) 0457 { 0458 QList<TileNode *> rankedTiles; 0459 for (TileNode &tile : d->tiles) { 0460 d->rankTiles(tile, rankedTiles, visibleRect, visiblePageNumber); 0461 } 0462 std::sort(rankedTiles.begin(), rankedTiles.end(), rankedTilesLessThan); 0463 0464 while (numberOfBytes > 0 && !rankedTiles.isEmpty()) { 0465 TileNode *tile = rankedTiles.takeLast(); 0466 if (!tile->pixmap) { 0467 continue; 0468 } 0469 0470 // do not evict visible pixmaps 0471 if (tile->rect.intersects(visibleRect)) { 0472 continue; 0473 } 0474 0475 qulonglong pixels = tile->pixmap->width() * tile->pixmap->height(); 0476 d->totalPixels -= pixels; 0477 if (numberOfBytes < 4 * pixels) { 0478 numberOfBytes = 0; 0479 } else { 0480 numberOfBytes -= 4 * pixels; 0481 } 0482 0483 delete tile->pixmap; 0484 tile->pixmap = nullptr; 0485 0486 tile->partial = true; 0487 0488 d->markParentDirty(*tile); 0489 } 0490 } 0491 0492 void TilesManager::Private::markParentDirty(const TileNode &tile) 0493 { 0494 if (!tile.parent) { 0495 return; 0496 } 0497 0498 if (!tile.parent->dirty) { 0499 tile.parent->dirty = true; 0500 markParentDirty(*tile.parent); 0501 } 0502 } 0503 0504 void TilesManager::Private::rankTiles(TileNode &tile, QList<TileNode *> &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber) 0505 { 0506 // If the page is visible, visibleRect is not null. 0507 // Otherwise we use the number of one of the visible pages to calculate the 0508 // distance. 0509 // Note that the current page may be visible and yet its pageNumber is 0510 // different from visiblePageNumber. Since we only use this value on hidden 0511 // pages, any visible page number will fit. 0512 if (visibleRect.isNull() && visiblePageNumber < 0) { 0513 return; 0514 } 0515 0516 if (tile.pixmap) { 0517 // Update distance 0518 if (!visibleRect.isNull()) { 0519 NormalizedPoint viewportCenter = visibleRect.center(); 0520 NormalizedPoint tileCenter = tile.rect.center(); 0521 // Manhattan distance. It's a good and fast approximation. 0522 tile.distance = qAbs(viewportCenter.x - tileCenter.x) + qAbs(viewportCenter.y - tileCenter.y); 0523 } else { 0524 // For non visible pages only the vertical distance is used 0525 if (pageNumber < visiblePageNumber) { 0526 tile.distance = 1 - tile.rect.bottom; 0527 } else { 0528 tile.distance = tile.rect.top; 0529 } 0530 } 0531 rankedTiles.append(&tile); 0532 } else { 0533 for (int i = 0; i < tile.nTiles; ++i) { 0534 rankTiles(tile.tiles[i], rankedTiles, visibleRect, visiblePageNumber); 0535 } 0536 } 0537 } 0538 0539 bool TilesManager::isRequesting(const NormalizedRect &rect, int pageWidth, int pageHeight) const 0540 { 0541 return rect == d->requestRect && pageWidth == d->requestWidth && pageHeight == d->requestHeight; 0542 } 0543 0544 void TilesManager::setRequest(const NormalizedRect &rect, int pageWidth, int pageHeight) 0545 { 0546 d->requestRect = rect; 0547 d->requestWidth = pageWidth; 0548 d->requestHeight = pageHeight; 0549 } 0550 0551 bool TilesManager::Private::splitBigTiles(TileNode &tile, const NormalizedRect &rect) 0552 { 0553 QRect tileRect = tile.rect.geometry(width, height); 0554 if (tileRect.width() * tileRect.height() < TILES_MAXSIZE) { 0555 return false; 0556 } 0557 0558 split(tile, rect); 0559 return true; 0560 } 0561 0562 void TilesManager::Private::split(TileNode &tile, const NormalizedRect &rect) 0563 { 0564 if (tile.nTiles != 0) { 0565 return; 0566 } 0567 0568 if (rect.isNull() || !tile.rect.intersects(rect)) { 0569 return; 0570 } 0571 0572 tile.nTiles = 4; 0573 tile.tiles = new TileNode[4]; 0574 double hCenter = (tile.rect.left + tile.rect.right) / 2; 0575 double vCenter = (tile.rect.top + tile.rect.bottom) / 2; 0576 0577 tile.tiles[0].rect = NormalizedRect(tile.rect.left, tile.rect.top, hCenter, vCenter); 0578 tile.tiles[1].rect = NormalizedRect(hCenter, tile.rect.top, tile.rect.right, vCenter); 0579 tile.tiles[2].rect = NormalizedRect(tile.rect.left, vCenter, hCenter, tile.rect.bottom); 0580 tile.tiles[3].rect = NormalizedRect(hCenter, vCenter, tile.rect.right, tile.rect.bottom); 0581 0582 for (int i = 0; i < tile.nTiles; ++i) { 0583 tile.tiles[i].parent = &tile; 0584 splitBigTiles(tile.tiles[i], rect); 0585 } 0586 } 0587 0588 NormalizedRect TilesManager::fromRotatedRect(const NormalizedRect &rect, Rotation rotation) 0589 { 0590 if (rotation == Rotation0) { 0591 return rect; 0592 } 0593 0594 NormalizedRect newRect; 0595 switch (rotation) { 0596 case Rotation90: 0597 newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left); 0598 break; 0599 case Rotation180: 0600 newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top); 0601 break; 0602 case Rotation270: 0603 newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right); 0604 break; 0605 default: 0606 newRect = rect; 0607 break; 0608 } 0609 0610 return newRect; 0611 } 0612 0613 NormalizedRect TilesManager::toRotatedRect(const NormalizedRect &rect, Rotation rotation) 0614 { 0615 if (rotation == Rotation0) { 0616 return rect; 0617 } 0618 0619 NormalizedRect newRect; 0620 switch (rotation) { 0621 case Rotation90: 0622 newRect = NormalizedRect(1 - rect.bottom, rect.left, 1 - rect.top, rect.right); 0623 break; 0624 case Rotation180: 0625 newRect = NormalizedRect(1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top); 0626 break; 0627 case Rotation270: 0628 newRect = NormalizedRect(rect.top, 1 - rect.right, rect.bottom, 1 - rect.left); 0629 break; 0630 default: 0631 newRect = rect; 0632 break; 0633 } 0634 0635 return newRect; 0636 } 0637 0638 TileNode::TileNode() 0639 : pixmap(nullptr) 0640 , rotation(Rotation0) 0641 , dirty(true) 0642 , partial(true) 0643 , distance(-1) 0644 , tiles(nullptr) 0645 , nTiles(0) 0646 , parent(nullptr) 0647 { 0648 } 0649 0650 bool TileNode::isValid() const 0651 { 0652 return pixmap && !dirty; 0653 } 0654 0655 class Tile::Private 0656 { 0657 public: 0658 Private(); 0659 0660 NormalizedRect rect; 0661 QPixmap *pixmap; 0662 bool isValid; 0663 }; 0664 0665 Tile::Private::Private() 0666 : pixmap(nullptr) 0667 , isValid(false) 0668 { 0669 } 0670 0671 Tile::Tile(const NormalizedRect &rect, QPixmap *pixmap, bool isValid) 0672 : d(new Tile::Private) 0673 { 0674 d->rect = rect; 0675 d->pixmap = pixmap; 0676 d->isValid = isValid; 0677 } 0678 0679 Tile::Tile(const Tile &t) 0680 : d(new Tile::Private) 0681 { 0682 d->rect = t.d->rect; 0683 d->pixmap = t.d->pixmap; 0684 d->isValid = t.d->isValid; 0685 } 0686 0687 Tile &Tile::operator=(const Tile &other) 0688 { 0689 if (this == &other) { 0690 return *this; 0691 } 0692 0693 d->rect = other.d->rect; 0694 d->pixmap = other.d->pixmap; 0695 d->isValid = other.d->isValid; 0696 0697 return *this; 0698 } 0699 0700 Tile::~Tile() 0701 { 0702 delete d; 0703 } 0704 0705 NormalizedRect Tile::rect() const 0706 { 0707 return d->rect; 0708 } 0709 0710 QPixmap *Tile::pixmap() const 0711 { 0712 return d->pixmap; 0713 } 0714 0715 bool Tile::isValid() const 0716 { 0717 return d->isValid; 0718 }