diff --git a/Sources/ObjectivelyMVC/Image.c b/Sources/ObjectivelyMVC/Image.c index c348dade..e6923896 100644 --- a/Sources/ObjectivelyMVC/Image.c +++ b/Sources/ObjectivelyMVC/Image.c @@ -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); @@ -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; } @@ -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 @@ -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) { @@ -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 @@ -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; } diff --git a/Sources/ObjectivelyMVC/Image.h b/Sources/ObjectivelyMVC/Image.h index 01015d02..aa1fe40c 100644 --- a/Sources/ObjectivelyMVC/Image.h +++ b/Sources/ObjectivelyMVC/Image.h @@ -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 { @@ -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. */ @@ -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) @@ -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. diff --git a/Sources/ObjectivelyMVC/Text.c b/Sources/ObjectivelyMVC/Text.c index 32ce9c45..543727c8 100644 --- a/Sources/ObjectivelyMVC/Text.c +++ b/Sources/ObjectivelyMVC/Text.c @@ -357,6 +357,7 @@ static void applyTransform(Text *self) { } wordStart = SDL_isspace((unsigned char) *c) != 0; + } } /** diff --git a/Tests/ObjectivelyMVC/.gitignore b/Tests/ObjectivelyMVC/.gitignore index 078c2227..bc0e68c8 100644 --- a/Tests/ObjectivelyMVC/.gitignore +++ b/Tests/ObjectivelyMVC/.gitignore @@ -2,6 +2,7 @@ *.trs Constraint Font+Bitmap +Image ImageAtlas Selector Style diff --git a/Tests/ObjectivelyMVC/Image.c b/Tests/ObjectivelyMVC/Image.c new file mode 100644 index 00000000..5a36627e --- /dev/null +++ b/Tests/ObjectivelyMVC/Image.c @@ -0,0 +1,122 @@ +/* + * ObjectivelyMVC: Object oriented MVC framework for SDL3 and C. + * Copyright (C) 2014 Jay Dolan + * 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 +#include + +#include "ObjectivelyMVC.h" + +static const char *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; +} diff --git a/Tests/ObjectivelyMVC/Makefile.am b/Tests/ObjectivelyMVC/Makefile.am index 670734e7..64f5b349 100644 --- a/Tests/ObjectivelyMVC/Makefile.am +++ b/Tests/ObjectivelyMVC/Makefile.am @@ -7,6 +7,7 @@ DEFAULT_INCLUDES = \ TESTS = \ Font+Bitmap \ + Image \ ImageAtlas \ Selector \ Style \