Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions Sources/ObjectivelyMVC/Image.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,44 @@ static Image *imageWithSurface(SDL_Surface *surface) {
return $(alloc(Image), initWithSurface, surface);
}

/**
* @fn Image *Image::imageWithSvg(const uint8_t *bytes, size_t length, float scale)
* @memberof Image
*/
static Image *imageWithSvg(const uint8_t *bytes, size_t length, float scale) {
return $(alloc(Image), initWithSvg, bytes, length, scale);
}

/**
* @return True if `bytes` are an SVG document, by declared type or by sniffing.
*/
static bool isSVG(const Image *self, const uint8_t *bytes, size_t length) {

if (self->type && SDL_strcasecmp(self->type, "svg") == 0) {
return true;
}

bool svg = false;

SDL_IOStream *stream = SDL_IOFromConstMem(bytes, (int) length);
if (stream) {
svg = IMG_isSVG(stream);
SDL_CloseIO(stream);
}

return svg;
}

/**
* @fn Image *Image::initWithBytes(Image *self, const uint8_t *bytes, size_t length)
* @memberof Image
*/
static Image *initWithBytes(Image *self, const uint8_t *bytes, size_t length) {

if (isSVG(self, bytes, length)) {
return $(self, initWithSvg, bytes, length, 1.f);
}

SDL_IOStream *stream = SDL_IOFromConstMem(bytes, (int) length);
if (stream) {
SDL_Surface *surface = IMG_LoadTyped_IO(stream, 0, self->type);
Comment thread
jdolan marked this conversation as resolved.
Expand All @@ -96,11 +128,11 @@ static Image *initWithBytes(Image *self, const uint8_t *bytes, size_t length) {
} else {
self = release(self);
}
SDL_CloseIO(stream);
} else {
self = release(self);
}

SDL_CloseIO(stream);
return self;
}

Expand Down Expand Up @@ -150,6 +182,58 @@ static Image *initWithResourceName(Image *self, const char *name) {
return self;
}

/**
* @brief Rasterizes `bytes` as SVG at `size` pixels, or at its intrinsic size when `size` is zero.
*/
static SDL_Surface *rasterizeSVG(const uint8_t *bytes, size_t length, SDL_Size size) {

SDL_Surface *surface = NULL;

SDL_IOStream *stream = SDL_IOFromConstMem(bytes, (int) length);
if (stream) {
surface = IMG_LoadSizedSVG_IO(stream, size.w, size.h);
SDL_CloseIO(stream);
}

return surface;
}

/**
* @fn Image *Image::initWithSvg(Image *self, const uint8_t *bytes, size_t length, float scale)
* @memberof Image
*/
static Image *initWithSvg(Image *self, const uint8_t *bytes, size_t length, float scale) {

scale = scale > 0.f ?: 1.f;

SDL_Surface *surface = rasterizeSVG(bytes, length, MakeSize(0, 0));

if (surface && scale != 1.f) {
const SDL_Size size = MakeSize(
(int) SDL_roundf(surface->w * scale),
(int) SDL_roundf(surface->h * scale)
);

SDL_DestroySurface(surface);
surface = rasterizeSVG(bytes, length, size);
}

if (surface) {
self = $(self, initWithSurface, surface);
SDL_DestroySurface(surface);

if (self) {
self->type = "svg";
self->scale = scale;
}
} else {
MVC_LogWarn("%s\n", SDL_GetError());
self = release(self);
}

return self;
}

/**
* @fn Image *Image::initWithSurface(Image *self, SDL_Surface *surface)
* @memberof Image
Expand All @@ -158,6 +242,7 @@ static Image *initWithSurface(Image *self, SDL_Surface *surface) {

self = (Image *) super(Object, self, init);
if (self) {
self->scale = 1.f;

if (surface) {
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
Expand All @@ -179,7 +264,10 @@ static Image *initWithSurface(Image *self, SDL_Surface *surface) {
* @memberof Image
*/
static SDL_Size size(const Image *self) {
return MakeSize(self->surface->w, self->surface->h);
return MakeSize(
(int) SDL_roundf(self->surface->w / self->scale),
(int) SDL_roundf(self->surface->h / self->scale)
);
}

#pragma mark - Class lifecycle
Expand All @@ -196,11 +284,13 @@ static void initialize(Class *clazz) {
((ImageInterface *) clazz->interface)->imageWithResource = imageWithResource;
((ImageInterface *) clazz->interface)->imageWithResourceName = imageWithResourceName;
((ImageInterface *) clazz->interface)->imageWithSurface = imageWithSurface;
((ImageInterface *) clazz->interface)->imageWithSvg = imageWithSvg;
((ImageInterface *) clazz->interface)->initWithBytes = initWithBytes;
((ImageInterface *) clazz->interface)->initWithData = initWithData;
((ImageInterface *) clazz->interface)->initWithResource = initWithResource;
((ImageInterface *) clazz->interface)->initWithResourceName = initWithResourceName;
((ImageInterface *) clazz->interface)->initWithSurface = initWithSurface;
((ImageInterface *) clazz->interface)->initWithSvg = initWithSvg;
((ImageInterface *) clazz->interface)->size = size;
}

Expand Down
33 changes: 33 additions & 0 deletions Sources/ObjectivelyMVC/Image.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ typedef struct ImageInterface ImageInterface;

/**
* @brief Image loading.
* @details Raster formats load at their native size. SVG, recognized by type or by sniffing,
* rasterizes at its intrinsic size times a `scale`, so that a vector asset drawn into a frame
* of its intrinsic size stays sharp at any pixel density; Image::size reports points.
* @extends Object
*/
struct Image {
Expand All @@ -48,6 +51,11 @@ struct Image {
*/
ImageInterface *interface[0];

/**
* @brief Pixels of `surface` per point: `1` for raster images, the requested scale for SVG.
*/
float scale;

/**
* @brief The backing surface.
*/
Expand Down Expand Up @@ -110,6 +118,18 @@ struct ImageInterface {
*/
Image *(*imageWithResourceName)(const char *name);

/**
* @static
* @fn Image *Image::imageWithSvg(const uint8_t *bytes, size_t length, float scale)
* @brief Instantiates an Image by rasterizing the specified SVG.
* @param bytes The SVG document.
* @param length The length of `bytes`.
* @param scale Pixels per point, e.g. the window's pixel density.
* @return The new Image, or `NULL` on error.
* @memberof Image
*/
Image *(*imageWithSvg)(const uint8_t *bytes, size_t length, float scale);

/**
* @static
* @fn Image *Image::imageWithSurface(SDL_Surface *surface)
Expand Down Expand Up @@ -161,6 +181,19 @@ struct ImageInterface {
*/
Image *(*initWithResourceName)(Image *self, const char *name);

/**
* @fn Image *Image::initWithSvg(Image *self, const uint8_t *bytes, size_t length, float scale)
* @brief Initializes this Image by rasterizing the specified SVG at its intrinsic size times
* `scale`.
* @param self The Image.
* @param bytes The SVG document.
* @param length The length of `bytes`.
* @param scale Pixels per point, e.g. the window's pixel density.
* @return The initialized Image, or `NULL` on error.
* @memberof Image
*/
Image *(*initWithSvg)(Image *self, const uint8_t *bytes, size_t length, float scale);

/**
* @fn Image *Image::initWithSurface(Image *self, SDL_Surface *surface)
* @brief Initializes this Image with the given surface.
Expand Down
1 change: 1 addition & 0 deletions Sources/ObjectivelyMVC/Text.c
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ static void applyTransform(Text *self) {
}

wordStart = SDL_isspace((unsigned char) *c) != 0;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions Tests/ObjectivelyMVC/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.trs
Constraint
Font+Bitmap
Image
ImageAtlas
Selector
Style
Expand Down
122 changes: 122 additions & 0 deletions Tests/ObjectivelyMVC/Image.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* ObjectivelyMVC: Object oriented MVC framework for SDL3 and C.
* Copyright (C) 2014 Jay Dolan <jay@jaydolan.com>
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*/

#include <check.h>
#include <string.h>

#include "ObjectivelyMVC.h"

static const char *svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"16\" viewBox=\"0 0 32 16\">"
"<rect width=\"32\" height=\"16\" fill=\"#ff0000\"/></svg>";

static void assertCenterIsRed(const Image *image) {

SDL_Surface *surface = image->surface;

Uint8 r, g, b, a;
ck_assert(SDL_ReadSurfacePixel(surface, surface->w / 2, surface->h / 2, &r, &g, &b, &a));
ck_assert_int_eq(255, r);
ck_assert_int_eq(0, g);
ck_assert_int_eq(0, b);
ck_assert_int_eq(255, a);
}

START_TEST(svgLoadsAtIntrinsicSize) {

Image *image = $$(Image, imageWithBytes, (const uint8_t *) svg, strlen(svg));
ck_assert_ptr_nonnull(image);

ck_assert_str_eq("svg", image->type);
ck_assert_float_eq(1.f, image->scale);
ck_assert_int_eq(32, image->surface->w);
ck_assert_int_eq(16, image->surface->h);

const SDL_Size size = $(image, size);
ck_assert_int_eq(32, size.w);
ck_assert_int_eq(16, size.h);

assertCenterIsRed(image);

release(image);

} END_TEST

START_TEST(svgRasterizesAtScale) {

Image *image = $$(Image, imageWithSVG, (const uint8_t *) svg, strlen(svg), 2.f);
ck_assert_ptr_nonnull(image);

ck_assert_float_eq(2.f, image->scale);
ck_assert_int_eq(64, image->surface->w);
ck_assert_int_eq(32, image->surface->h);

// Points, not pixels, so a View sized to the image is the intrinsic size
const SDL_Size size = $(image, size);
ck_assert_int_eq(32, size.w);
ck_assert_int_eq(16, size.h);

assertCenterIsRed(image);

release(image);

} END_TEST

START_TEST(rasterKeepsUnitScale) {

SDL_Surface *surface = SDL_CreateSurface(8, 4, SDL_PIXELFORMAT_RGBA32);
Image *image = $$(Image, imageWithSurface, surface);
SDL_DestroySurface(surface);

ck_assert_float_eq(1.f, image->scale);

const SDL_Size size = $(image, size);
ck_assert_int_eq(8, size.w);
ck_assert_int_eq(4, size.h);

release(image);

} END_TEST

START_TEST(garbageFails) {

const char *garbage = "not an image";
Image *image = $$(Image, imageWithSVG, (const uint8_t *) garbage, strlen(garbage), 1.f);
ck_assert_ptr_null(image);

} END_TEST

int main(int argc, char **argv) {

TCase *tcase = tcase_create("Image");
tcase_add_test(tcase, svgLoadsAtIntrinsicSize);
tcase_add_test(tcase, svgRasterizesAtScale);
tcase_add_test(tcase, rasterKeepsUnitScale);
tcase_add_test(tcase, garbageFails);

Suite *suite = suite_create("Image");
suite_add_tcase(suite, tcase);

SRunner *runner = srunner_create(suite);

srunner_run_all(runner, CK_VERBOSE);
int failed = srunner_ntests_failed(runner);

srunner_free(runner);

return failed;
}
1 change: 1 addition & 0 deletions Tests/ObjectivelyMVC/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ DEFAULT_INCLUDES = \

TESTS = \
Font+Bitmap \
Image \
ImageAtlas \
Selector \
Style \
Expand Down
Loading