Skip to content

Commit

Permalink
TMX format: Support embedded images on the tileset
Browse files Browse the repository at this point in the history
Previously, embedded images were only supported per-tile. Due to the
recent change to use sub-rects rather than physically cutting up the
tileset, when you called Tileset.loadFromImage(image), the entire
tileset image would get duplicated for each tile.

Now the image is embedded once at the tileset level instead.

Also, the Tileset.margin and Tileset.tileSpacing properties are now
writable in the scripting API.

Addresses part of mapeditor#3630.
  • Loading branch information
bjorn committed Nov 6, 2023
1 parent ce8fd77 commit 264e1ac
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 50 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* JSON format: Fixed tile order when loading a tileset using the old format
* tmxrasterizer: Added --hide-object and --show-object arguments (by Lars Luz, #3819)
* Scripting: Made Tileset.margin and Tileset.tileSpacing writable
* Windows: Fixed the support for WebP images (updated to Qt 6.5.3)

### Tiled 1.10.2 (4 August 2023)
Expand Down
12 changes: 10 additions & 2 deletions docs/scripting-doc/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3336,13 +3336,21 @@ declare class Tileset extends Asset {

/**
* Spacing between tiles in this tileset in pixels.
*
* @note Changing this property will cause an image-based tileset to update
* all its tiles. When setting up a tileset, you'll want to set this property
* before setting the {@link image} property.
*/
readonly tileSpacing : number
tileSpacing : number

/**
* Margin around the tileset in pixels (only used at the top and left sides of the tileset image).
*
* @note Changing this property will cause an image-based tileset to update
* all its tiles. When setting up a tileset, you'll want to set this property
* before setting the {@link image} property.
*/
readonly margin : number
margin : number

/**
* The alignment to use for tile objects (when Unspecified, uses Bottom alignment on isometric maps and BottomLeft alignment for all other maps).
Expand Down
9 changes: 4 additions & 5 deletions src/libtiled/imagereference.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
#include "imagereference.h"

#include "imagecache.h"
#include "tileset.h"

namespace Tiled {

Expand All @@ -33,10 +32,10 @@ bool ImageReference::hasImage() const
QPixmap ImageReference::create() const
{
QPixmap pixmap;
if (source.isLocalFile())
pixmap = ImageCache::loadPixmap(source.toLocalFile());
else if (source.scheme() == QLatin1String("qrc"))
pixmap = ImageCache::loadPixmap(QLatin1Char(':') + source.path());

const QString fileName = Tiled::urlToLocalFileOrQrc(source);
if (!fileName.isEmpty())
pixmap = ImageCache::loadPixmap(fileName);
else if (!data.isEmpty())
pixmap = QPixmap::fromImage(QImage::fromData(data, format));

Expand Down
7 changes: 3 additions & 4 deletions src/libtiled/mapreader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,8 @@ std::unique_ptr<Map> MapReaderPrivate::readMap()
mMap.reset();
} else {
// Try to load the tileset images for embedded tilesets
auto tilesets = mMap->tilesets();
for (SharedTileset &tileset : tilesets) {
if (!tileset->isCollection() && tileset->fileName().isEmpty())
for (const SharedTileset &tileset : mMap->tilesets()) {
if (tileset->fileName().isEmpty())
tileset->loadImage();
}

Expand Down Expand Up @@ -1501,7 +1500,7 @@ std::unique_ptr<Map> MapReader::readMap(const QString &fileName)
SharedTileset MapReader::readTileset(QIODevice *device, const QString &path)
{
SharedTileset tileset = d->readTileset(device, path);
if (tileset && !tileset->isCollection())
if (tileset)
tileset->loadImage();

return tileset;
Expand Down
29 changes: 23 additions & 6 deletions src/libtiled/mapwriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,17 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
writeProperties(w, tileset.properties());

// Write the image element
const QUrl &imageSource = tileset.imageSource();
if (!imageSource.isEmpty()) {
const bool isCollection = tileset.isCollection();
if (!isCollection) {
w.writeStartElement(QStringLiteral("image"));
QString source = toFileReference(imageSource, mUseAbsolutePaths ? QString()
: mDir.path());
w.writeAttribute(QStringLiteral("source"), source);

const QUrl &imageSource = tileset.imageSource();
if (!imageSource.isEmpty()) {
// Write a reference to an external tileset image
QString source = toFileReference(imageSource, mUseAbsolutePaths ? QString()
: mDir.path());
w.writeAttribute(QStringLiteral("source"), source);
}

const QColor transColor = tileset.transparentColor();
if (transColor.isValid())
Expand All @@ -422,10 +427,22 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
w.writeAttribute(QStringLiteral("height"),
QString::number(tileset.imageHeight()));

if (imageSource.isEmpty()) {
// Write an embedded image
w.writeAttribute(QStringLiteral("format"), QLatin1String("png"));

w.writeStartElement(QStringLiteral("data"));
w.writeAttribute(QStringLiteral("encoding"), QLatin1String("base64"));

QBuffer buffer;
tileset.image().save(&buffer, "png");
w.writeCharacters(QString::fromLatin1(buffer.data().toBase64()));
w.writeEndElement(); // </data>
}

w.writeEndElement();
}

const bool isCollection = tileset.isCollection();
const bool includeAllTiles = isCollection || tileset.anyTileOutOfOrder();

for (const Tile *tile : tileset.tiles()) {
Expand Down
27 changes: 13 additions & 14 deletions src/libtiled/tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,31 +228,29 @@ bool Tileset::loadFromImage(const QString &fileName)
}

/**
* Tries to load the image this tileset is referring to.
* Tries to load the image this tileset is referring to, if any.
*
* @return <code>true</code> if loading was successful, otherwise
* returns <code>false</code>
*/
bool Tileset::loadImage()
{
if (mTileWidth <= 0 || mTileHeight <= 0) {
mImageReference.status = LoadingError;
return false;
}

mImage = ImageCache::loadPixmap(Tiled::urlToLocalFileOrQrc(mImageReference.source));
if (mImage.isNull()) {
mImageReference.status = LoadingError;
return false;
if (mImageReference.hasImage()) {
mImage = mImageReference.create();
if (mImage.isNull()) {
mImageReference.status = LoadingError;
return false;
}
}

initializeTilesetTiles();

return true;
return initializeTilesetTiles();
}

void Tileset::initializeTilesetTiles()
bool Tileset::initializeTilesetTiles()
{
if (mImage.isNull() || mTileWidth <= 0 || mTileHeight <= 0)
return false;

if (mImageReference.transparentColor.isValid())
mImage.setMask(mImage.createMaskFromColor(mImageReference.transparentColor));

Expand Down Expand Up @@ -294,6 +292,7 @@ void Tileset::initializeTilesetTiles()
mImageReference.size = mImage.size();
mColumnCount = columnCountForWidth(mImageReference.size.width());
mImageReference.status = LoadingReady;
return true;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/libtiled/tileset.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
bool loadFromImage(const QImage &image, const QString &source);
bool loadFromImage(const QString &fileName);
bool loadImage();
bool initializeTilesetTiles();

SharedTileset findSimilarTileset(const QVector<SharedTileset> &tilesets) const;

Expand Down Expand Up @@ -293,8 +294,6 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
static FillMode fillModeFromString(const QString &);

private:
void initializeTilesetTiles();

void maybeUpdateTileSize(QSize oldSize, QSize newSize);
void updateTileSize();

Expand Down Expand Up @@ -642,11 +641,12 @@ inline const QPixmap &Tileset::image() const

/**
* Returns whether this tileset is a collection of images. In this case, the
* tileset itself has no image source.
* tileset itself has no image source and the tileset image is also not
* embedded.
*/
inline bool Tileset::isCollection() const
{
return imageSource().isEmpty();
return imageSource().isEmpty() && image().isNull();
}

inline const QList<WangSet*> &Tileset::wangSets() const
Expand Down
2 changes: 1 addition & 1 deletion src/libtiled/tilesetmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ void TilesetManager::reloadImages(Tileset *tileset)
}
}
emit tilesetImagesChanged(tileset);
} else {
} else if (tileset->imageSource().isLocalFile()) {
ImageCache::remove(tileset->imageSource().toLocalFile());
if (tileset->loadImage())
emit tilesetImagesChanged(tileset);
Expand Down
7 changes: 3 additions & 4 deletions src/libtiled/varianttomapconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,8 @@ std::unique_ptr<Map> VariantToMapConverter::toMap(const QVariant &variant,
}

// Try to load the tileset images
auto tilesets = map->tilesets();
for (SharedTileset &tileset : tilesets) {
if (!tileset->imageSource().isEmpty() && tileset->fileName().isEmpty())
for (const SharedTileset &tileset : map->tilesets()) {
if (tileset->fileName().isEmpty())
tileset->loadImage();
}

Expand All @@ -145,7 +144,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant,
mReadingExternalTileset = true;

SharedTileset tileset = toTileset(variant);
if (tileset && !tileset->imageSource().isEmpty())
if (tileset)
tileset->loadImage();

mReadingExternalTileset = false;
Expand Down
46 changes: 38 additions & 8 deletions src/tiled/editabletileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,7 @@ void EditableTileset::setImage(const QString &imageFilePath)
push(new ChangeTilesetParameters(doc, parameters));
} else if (!checkReadOnly()) {
tileset()->setImageSource(imageFilePath);

if (!tileSize().isEmpty() && !image().isEmpty())
tileset()->loadImage();
tileset()->loadImage();
}
}

Expand All @@ -268,9 +266,43 @@ void EditableTileset::setTileSize(QSize size)
push(new ChangeTilesetParameters(doc, parameters));
} else if (!checkReadOnly()) {
tileset()->setTileSize(size);
tileset()->initializeTilesetTiles();
}
}

void EditableTileset::setTileSpacing(int tileSpacing)
{
if (isCollection() && tileCount() > 0) {
ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Can't set tile spacing on an image collection tileset"));
return;
}

if (auto doc = tilesetDocument()) {
TilesetParameters parameters(*tileset());
parameters.tileSpacing = tileSpacing;

if (!tileSize().isEmpty() && !image().isEmpty())
tileset()->loadImage();
push(new ChangeTilesetParameters(doc, parameters));
} else if (!checkReadOnly()) {
tileset()->setTileSpacing(tileSpacing);
tileset()->initializeTilesetTiles();
}
}

void EditableTileset::setMargin(int margin)
{
if (isCollection() && tileCount() > 0) {
ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Can't set margin on an image collection tileset"));
return;
}

if (auto doc = tilesetDocument()) {
TilesetParameters parameters(*tileset());
parameters.margin = margin;

push(new ChangeTilesetParameters(doc, parameters));
} else if (!checkReadOnly()) {
tileset()->setMargin(margin);
tileset()->initializeTilesetTiles();
}
}

Expand Down Expand Up @@ -336,9 +368,7 @@ void EditableTileset::setTransparentColor(const QColor &color)
push(new ChangeTilesetParameters(doc, parameters));
} else if (!checkReadOnly()) {
tileset()->setTransparentColor(color);

if (!tileSize().isEmpty() && !image().isEmpty())
tileset()->loadImage();
tileset()->initializeTilesetTiles();
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/tiled/editabletileset.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ class EditableTileset : public EditableAsset
Q_PROPERTY(int imageWidth READ imageWidth)
Q_PROPERTY(int imageHeight READ imageHeight)
Q_PROPERTY(QSize imageSize READ imageSize)
Q_PROPERTY(int tileSpacing READ tileSpacing)
Q_PROPERTY(int margin READ margin)
Q_PROPERTY(int tileSpacing READ tileSpacing WRITE setTileSpacing)
Q_PROPERTY(int margin READ margin WRITE setMargin)
Q_PROPERTY(Alignment objectAlignment READ objectAlignment WRITE setObjectAlignment)
Q_PROPERTY(TileRenderSize tileRenderSize READ tileRenderSize WRITE setTileRenderSize)
Q_PROPERTY(FillMode fillMode READ fillMode WRITE setFillMode)
Expand Down Expand Up @@ -157,6 +157,8 @@ public slots:
void setTileHeight(int height);
void setTileSize(QSize size);
void setTileSize(int width, int height);
void setTileSpacing(int tileSpacing);
void setMargin(int margin);
void setColumnCount(int columnCount);
void setObjectAlignment(Alignment objectAlignment);
void setTileRenderSize(TileRenderSize tileRenderSize);
Expand Down

0 comments on commit 264e1ac

Please sign in to comment.