Skip to content

Commit a250178

Browse files
committed
Add SVGSkin class
1 parent 3db89da commit a250178

File tree

16 files changed

+225
-0
lines changed

16 files changed

+225
-0
lines changed

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ qt_add_qml_module(scratchcpp-render
3737
skin.h
3838
bitmapskin.cpp
3939
bitmapskin.h
40+
svgskin.cpp
41+
svgskin.h
4042
renderedtarget.cpp
4143
renderedtarget.h
4244
targetpainter.cpp

src/svgskin.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
3+
#include <scratchcpp/costume.h>
4+
5+
#include "svgskin.h"
6+
7+
using namespace scratchcpprender;
8+
9+
static const int MAX_TEXTURE_DIMENSION = 2048;
10+
static const int INDEX_OFFSET = 8;
11+
12+
SVGSkin::SVGSkin(libscratchcpp::Costume *costume) :
13+
Skin(costume)
14+
{
15+
if (!costume)
16+
return;
17+
18+
// Load SVG data
19+
m_svgRen.load(QByteArray(static_cast<const char *>(costume->data()), costume->dataSize()));
20+
21+
// Calculate maximum index (larger images will only be scaled up)
22+
const QRectF viewBox = m_svgRen.viewBox();
23+
24+
if (viewBox.width() == 0 || viewBox.height() == 0)
25+
return;
26+
27+
const int i1 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.width()) + INDEX_OFFSET;
28+
const int i2 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.height()) + INDEX_OFFSET;
29+
m_maxIndex = std::min(i1, i2);
30+
31+
// Create all possible textures (the 1.0 scale is stored at INDEX_OFFSET)
32+
// TODO: Is this necessary?
33+
for (int i = 0; i <= m_maxIndex; i++)
34+
createScaledTexture(i);
35+
}
36+
37+
SVGSkin::~SVGSkin()
38+
{
39+
for (const auto &[index, texture] : m_textures)
40+
m_textureObjects[texture].release();
41+
}
42+
43+
Texture SVGSkin::getTexture(double scale) const
44+
{
45+
// https://github.com/scratchfoundation/scratch-render/blob/423bb700c36b8c1c0baae1e2413878a4f778849a/src/SVGSkin.js#L158-L176
46+
int mipLevel = std::max(std::ceil(std::log2(scale)) + INDEX_OFFSET, 0.0);
47+
48+
// Limit to maximum index
49+
mipLevel = std::min(mipLevel, m_maxIndex);
50+
51+
auto it = m_textures.find(mipLevel);
52+
53+
if (it == m_textures.cend())
54+
return const_cast<SVGSkin *>(this)->createScaledTexture(mipLevel); // TODO: Remove that awful const_cast ;)
55+
else
56+
return m_textureObjects.at(it->second);
57+
}
58+
59+
double SVGSkin::getTextureScale(const Texture &texture) const
60+
{
61+
auto it = m_textureIndexes.find(texture.handle());
62+
63+
if (it != m_textureIndexes.cend())
64+
return std::pow(2, it->second - INDEX_OFFSET);
65+
66+
return 1;
67+
}
68+
69+
void SVGSkin::paint(QPainter *painter)
70+
{
71+
const QPaintDevice *device = painter->device();
72+
m_svgRen.render(painter, QRectF(0, 0, device->width(), device->height()));
73+
}
74+
75+
Texture SVGSkin::createScaledTexture(int index)
76+
{
77+
Q_ASSERT(m_textures.find(index) == m_textures.cend());
78+
auto it = m_textures.find(index);
79+
80+
if (it != m_textures.cend())
81+
return m_textureObjects[it->second];
82+
83+
const double scale = std::pow(2, index - INDEX_OFFSET);
84+
const QRect viewBox = m_svgRen.viewBox();
85+
const double width = viewBox.width() * scale;
86+
const double height = viewBox.height() * scale;
87+
88+
if (width > MAX_TEXTURE_DIMENSION || height > MAX_TEXTURE_DIMENSION) {
89+
Q_ASSERT(false); // this shouldn't happen because indexes are limited to the max index
90+
return Texture();
91+
}
92+
93+
const Texture texture = createAndPaintTexture(viewBox.width() * scale, viewBox.height() * scale, true);
94+
95+
if (texture.isValid()) {
96+
m_textures[index] = texture.handle();
97+
m_textureIndexes[texture.handle()] = index;
98+
m_textureObjects[texture.handle()] = texture;
99+
}
100+
101+
return texture;
102+
}

src/svgskin.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
3+
#pragma once
4+
5+
#include <QSvgRenderer>
6+
7+
#include "skin.h"
8+
#include "texture.h"
9+
10+
namespace scratchcpprender
11+
{
12+
13+
class SVGSkin : public Skin
14+
{
15+
public:
16+
SVGSkin(libscratchcpp::Costume *costume);
17+
~SVGSkin();
18+
19+
Texture getTexture(double scale) const override;
20+
double getTextureScale(const Texture &texture) const override;
21+
22+
protected:
23+
void paint(QPainter *painter) override;
24+
25+
private:
26+
Texture createScaledTexture(int index);
27+
28+
std::unordered_map<int, GLuint> m_textures;
29+
std::unordered_map<GLuint, int> m_textureIndexes; // reverse map of m_textures
30+
std::unordered_map<GLuint, Texture> m_textureObjects;
31+
QSvgRenderer m_svgRen;
32+
int m_maxIndex = 0;
33+
};
34+
35+
} // namespace scratchcpprender

test/skins/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# bitmapskin
12
add_executable(
23
bitmapskin_test
34
bitmapskin_test.cpp
@@ -12,3 +13,19 @@ target_link_libraries(
1213

1314
add_test(bitmapskin_test)
1415
gtest_discover_tests(bitmapskin_test)
16+
17+
# svgskin
18+
add_executable(
19+
svgskin_test
20+
svgskin_test.cpp
21+
)
22+
23+
target_link_libraries(
24+
svgskin_test
25+
GTest::gtest_main
26+
scratchcpp-render
27+
${QT_LIBS}
28+
)
29+
30+
add_test(svgskin_test)
31+
gtest_discover_tests(svgskin_test)

test/skins/svgskin_test.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#include <scratchcpp/costume.h>
2+
#include <svgskin.h>
3+
4+
#include "../common.h"
5+
6+
using namespace scratchcpprender;
7+
using namespace libscratchcpp;
8+
9+
class SVGSkinTest : public testing::Test
10+
{
11+
public:
12+
void SetUp() override
13+
{
14+
m_context.create();
15+
ASSERT_TRUE(m_context.isValid());
16+
17+
m_surface.setFormat(m_context.format());
18+
m_surface.create();
19+
Q_ASSERT(m_surface.isValid());
20+
m_context.makeCurrent(&m_surface);
21+
22+
Costume costume("", "", "");
23+
std::string costumeData = readFileStr("image.svg");
24+
costume.setData(costumeData.size(), costumeData.data());
25+
m_skin = std::make_unique<SVGSkin>(&costume);
26+
}
27+
28+
void TearDown() override
29+
{
30+
ASSERT_EQ(m_context.surface(), &m_surface);
31+
m_context.doneCurrent();
32+
}
33+
34+
QOpenGLContext m_context;
35+
QOffscreenSurface m_surface;
36+
std::unique_ptr<Skin> m_skin;
37+
};
38+
39+
TEST_F(SVGSkinTest, Textures)
40+
{
41+
static const int INDEX_OFFSET = 8;
42+
43+
for (int i = 0; i <= 18; i++) {
44+
double scale = std::pow(2, i - INDEX_OFFSET);
45+
Texture texture = m_skin->getTexture(scale);
46+
int dimension = static_cast<int>(13 * scale);
47+
ASSERT_TRUE(texture.isValid() || dimension == 0);
48+
49+
if (!texture.isValid())
50+
continue;
51+
52+
if (i > 15) {
53+
ASSERT_EQ(texture.width(), 1664);
54+
ASSERT_EQ(texture.height(), 1664);
55+
ASSERT_EQ(m_skin->getTextureScale(texture), 128);
56+
} else {
57+
ASSERT_EQ(texture.width(), dimension);
58+
ASSERT_EQ(texture.height(), dimension);
59+
ASSERT_EQ(m_skin->getTextureScale(texture), scale);
60+
}
61+
62+
QBuffer buffer;
63+
texture.toImage().save(&buffer, "png");
64+
QFile ref("svg_texture_results/" + QString::number(std::min(i, 15)) + ".png");
65+
ref.open(QFile::ReadOnly);
66+
buffer.open(QBuffer::ReadOnly);
67+
ASSERT_EQ(buffer.readAll(), ref.readAll());
68+
}
69+
}

test/svg_texture_results/10.png

431 Bytes
Loading

test/svg_texture_results/11.png

805 Bytes
Loading

test/svg_texture_results/12.png

1.46 KB
Loading

test/svg_texture_results/13.png

2.94 KB
Loading

test/svg_texture_results/14.png

6.99 KB
Loading

0 commit comments

Comments
 (0)