Skip to content

Commit d51e5f8

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

File tree

16 files changed

+227
-0
lines changed

16 files changed

+227
-0
lines changed

src/CMakeLists.txt

+2
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

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

src/svgskin.h

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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, bool antialiasing = true);
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+
bool m_antialiasing = false;
34+
};
35+
36+
} // namespace scratchcpprender

test/skins/CMakeLists.txt

+17
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

+69
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, false);
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

245 Bytes
Loading

test/svg_texture_results/11.png

454 Bytes
Loading

test/svg_texture_results/12.png

873 Bytes
Loading

test/svg_texture_results/13.png

1.93 KB
Loading

test/svg_texture_results/14.png

5.09 KB
Loading

test/svg_texture_results/15.png

15.6 KB
Loading

test/svg_texture_results/5.png

91 Bytes
Loading

test/svg_texture_results/6.png

108 Bytes
Loading

test/svg_texture_results/7.png

121 Bytes
Loading

test/svg_texture_results/8.png

137 Bytes
Loading

test/svg_texture_results/9.png

167 Bytes
Loading

0 commit comments

Comments
 (0)