Skip to content

Commit

Permalink
Merge pull request #5 from TDT4102/thaugdahl/image-additions
Browse files Browse the repository at this point in the history
Thaugdahl/image additions
  • Loading branch information
joakibhu authored Jul 13, 2026
2 parents b877adb + 40a807b commit 63bb565
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 30 deletions.
77 changes: 73 additions & 4 deletions dependencies/subprojects/animationwindow/include/Image.h
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
#pragma once

#include <string>
#include <vector>
#include "Point.h"
#include "Color.h"
#include <filesystem>
#include "SDL_render.h"

namespace TDT4102 {

struct ImageCache;

enum class FlipImage {
NONE, HORIZONTAL, VERTICAL
};

struct Image {
// Allow the texture to be read out and drawn despite being private
friend class AnimationWindow;
friend class ImageCache;

public:
explicit Image();
explicit Image(std::filesystem::path pathToImageFile);

Image &operator=(Image &&other) {
this->~Image();

state = other.state;
texture = other.texture;
surface = other.surface;

other.reset();

return *this;
}

Image(Image &&other)
: texture{other.texture}, surface{other.surface}, path{std::move(other.path)}, state{other.state}
{
other.reset();
}

~Image();

/// Creates an image instance from a vector of colors.
/// \see TDT4102::Image::bytes
static Image fromPixels(const std::vector<TDT4102::Color> &bytes,
int imageWidth, int imageHeight);

static std::vector<TDT4102::Color> loadPixels(std::filesystem::path pathToImageFile);

int width = 0;
int height = 0;

std::vector<TDT4102::Color> getPixels();
void setPixels(const std::vector<TDT4102::Color> &, int width_, int height_);

private:

enum class ImageState {
Loaded,
FromFile,
FromBuffer,
Invalidated
};

SDL_Texture *texture = nullptr;
std::filesystem::path src = "non existent file";

/// Trade-off. Allows image manipulation at the cost of memory.
SDL_Surface *surface = nullptr;

std::filesystem::path path{"nonexistent"};

/// The state of the image
ImageState state = ImageState::Invalidated;

void load(SDL_Renderer* renderer);
void draw(SDL_Renderer* renderer, TDT4102::Point location, int imageWidth = 0, int imageHeight = 0, double angleDegrees = 0, TDT4102::Point rotationOrigin = {0, 0});
void drawRegion(SDL_Renderer* renderer, TDT4102::Point location, int imageWidth = 0, int imageHeight = 0, TDT4102::Point sourceTopLeftPoint = {0, 0}, int sourceWidth = 0, int sourceHeight = 0, SDL_RendererFlip flip = SDL_FLIP_NONE);

void draw(SDL_Renderer* renderer, TDT4102::Point location,
int imageWidth = 0, int imageHeight = 0,
double angleDegrees = 0, TDT4102::Point rotationOrigin = {0, 0});

/// Does conditional texture cleanup.
/// Unsets the associated texture pointer
void cleanupTexture();

/// Conditionally cleans up the SDL_Surface
/// Unsets the associated SDL_Surface pointer
void cleanupSurface();

/// Resets the Image. For use with move-operators
void reset();

static ImageCache cache;

void drawRegion(SDL_Renderer* renderer, TDT4102::Point location,
int imageWidth = 0, int imageHeight = 0,
TDT4102::Point sourceTopLeftPoint = {0, 0},
int sourceWidth = 0, int sourceHeight = 0, SDL_RendererFlip flip = SDL_FLIP_NONE);
};
}
205 changes: 196 additions & 9 deletions dependencies/subprojects/animationwindow/src/Image.cpp
Original file line number Diff line number Diff line change
@@ -1,27 +1,151 @@
#include "Image.h"
#include "SDL_surface.h"
#include <SDL_image.h>
#include <unordered_set>
#include <unordered_map>
#include <cstring>

namespace TDT4102 {
/// A caching mechanism to avoid processing images loaded from files
struct ImageCache {
/// Two-way lookup
std::unordered_map<std::filesystem::path, std::pair<SDL_Surface *, SDL_Texture *>> pathToTexture;
std::unordered_set<SDL_Texture *> textureMembershipSet;
std::unordered_set<SDL_Surface *> surfaceMembershipSet;

/// Loads an SDL_Texture from the file path and caches it
SDL_Texture *insert(SDL_Renderer *renderer, SDL_Surface *surface, const std::filesystem::path &filePath);

SDL_Texture *getTexture(const std::filesystem::path &path) const;
SDL_Surface *getSurface(const std::filesystem::path &path) const;

bool isTextureCached(SDL_Texture *texture) const;
bool isSurfaceCached(SDL_Surface *texture) const;

~ImageCache();

private:
ImageCache() = default;
friend class Image;
};
}

constexpr uint32_t GetPixelFormat() {
return SDL_PIXELFORMAT_ARGB8888;
}

TDT4102::Image::Image() {}

TDT4102::Image::Image(const std::filesystem::path pathToImageFile) {
src = pathToImageFile;
path = pathToImageFile;

SDL_Surface *cachedSurface = cache.getSurface(pathToImageFile);

if ( cachedSurface == nullptr ) {

if ( ! std::filesystem::exists(path) ) {
throw std::runtime_error("The image file located at " + path.string() + "\ncould not be found");
}

surface = IMG_Load(pathToImageFile.c_str());
surface = SDL_ConvertSurfaceFormat(surface, GetPixelFormat(), 0);
width = surface->w;
height = surface->h;
} else {
surface = cachedSurface;
width = surface->w;
height = surface->h;
}

state = ImageState::FromFile;
}

void TDT4102::Image::load(SDL_Renderer* renderer) {
if(!std::filesystem::exists(src)) {
throw std::runtime_error("The image file located at: " + src.string() + "\ncould not be found.");
// We need a reference to SDL_Renderer to perform the loading operation
// This requires loading it once it gets used
if ( state == ImageState::FromFile ) {
texture = cache.insert(renderer, surface, path);
state = texture ? ImageState::Loaded : ImageState::Invalidated;
} else if ( state == ImageState::FromBuffer ) {
texture = SDL_CreateTextureFromSurface(renderer, surface);
state = texture ? ImageState::Loaded : ImageState::Invalidated;
}
}

TDT4102::Image TDT4102::Image::fromPixels(const std::vector<TDT4102::Color> &pixels, int imageWidth, int imageHeight)
{
TDT4102::Image result = TDT4102::Image{};
result.setPixels(pixels, imageWidth, imageHeight);
result.state = ImageState::FromBuffer;
return result;
}

std::vector<TDT4102::Color> TDT4102::Image::getPixels() {

std::vector<TDT4102::Color> result;
result.resize(width * height);

// Convert the pixel format before copying
if ( surface->format->format != GetPixelFormat()) {
SDL_Surface *oldSurface = surface;
SDL_Surface *newSurface = SDL_ConvertSurfaceFormat(oldSurface, GetPixelFormat(), 0);

if ( newSurface != nullptr ) {
SDL_FreeSurface(oldSurface);
surface = newSurface;
}
}

// Early return if we're left with no surface to pull pixels from
if ( surface == nullptr ) {
return result;
}

// Respect pitch
unsigned char *pixels = static_cast<unsigned char *>(surface->pixels);
int pitch = surface->pitch;

for ( int y = 0; y < height; y++ ) {
std::memcpy(&result[y*width], &pixels[y*pitch], sizeof(TDT4102::Color)*width);
}
return result;
}

void TDT4102::Image::setPixels(const std::vector<TDT4102::Color> &pixels, int width_, int height_) {

width = width_;
height = height_;

cleanupTexture();
cleanupSurface();

surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, GetPixelFormat());

if ( surface == nullptr ) {
throw std::runtime_error{"Failed to create SDL Surface when setting pixels"};
state = ImageState::Invalidated;
return;
}
texture = IMG_LoadTexture(renderer, src.string().c_str());

if(width == 0 && height == 0) {
SDL_QueryTexture(texture, nullptr, nullptr, &width, &height);
unsigned char *pixelData = static_cast<unsigned char *>(surface->pixels);
int pitch = surface->pitch;

for ( int y = 0; y < height; y++ ) {
std::memcpy(
&pixelData[y*pitch],
&pixels[y*width],
width * sizeof(TDT4102::Color)
);
}

// Invalidate the image. Reload texture
state = ImageState::FromBuffer;
}

void TDT4102::Image::draw(SDL_Renderer *renderer, TDT4102::Point location, int imageWidth, int imageHeight, double angleDegrees, TDT4102::Point rotationOrigin) {
// We need a reference to SDL_Renderer to perform the loading operation
// This requires loading it once it gets used
if(texture == nullptr) {
if ( texture == nullptr ) {
load(renderer);
}

Expand Down Expand Up @@ -61,8 +185,71 @@ void TDT4102::Image::drawRegion(SDL_Renderer* renderer, TDT4102::Point location,


TDT4102::Image::~Image() {
if (texture != nullptr) {
cleanupTexture();
cleanupSurface();
}

void TDT4102::Image::cleanupTexture() {
if (texture != nullptr && ! cache.isTextureCached(texture)) {
SDL_DestroyTexture(texture);
texture = nullptr;
}
texture = nullptr;
}

void TDT4102::Image::cleanupSurface() {
if ( surface != nullptr && !cache.isSurfaceCached(surface)) {
SDL_FreeSurface(surface);
}
surface = nullptr;
}

void TDT4102::Image::reset() {
state = ImageState::Invalidated;
texture = nullptr;
surface = nullptr;
}

TDT4102::ImageCache TDT4102::Image::cache = TDT4102::ImageCache{};

SDL_Texture *TDT4102::ImageCache::insert(SDL_Renderer *renderer, SDL_Surface *surface, const std::filesystem::path &filePath) {
// TODO: Normalize cache key
if ( pathToTexture.contains(filePath) ) {
return pathToTexture[filePath].second;
} else {
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
pathToTexture.insert(std::make_pair(filePath, std::make_pair(surface, texture)));
textureMembershipSet.insert(texture);
surfaceMembershipSet.insert(surface);
return texture;
}
}

bool TDT4102::ImageCache::isTextureCached(SDL_Texture *texture) const {
return textureMembershipSet.contains(texture);
}

bool TDT4102::ImageCache::isSurfaceCached(SDL_Surface *surface) const {
return surfaceMembershipSet.contains(surface);
}


SDL_Surface *TDT4102::ImageCache::getSurface(const std::filesystem::path &path) const {
if ( pathToTexture.contains(path) ) {
return pathToTexture.at(path).first;
}
return nullptr;
}

SDL_Texture *TDT4102::ImageCache::getTexture(const std::filesystem::path &path) const {
if ( pathToTexture.contains(path) ) {
return pathToTexture.at(path).second;
}
return nullptr;
}

TDT4102::ImageCache::~ImageCache() {
for ( auto &[key, value] : pathToTexture ) {
SDL_FreeSurface(value.first);
SDL_DestroyTexture(value.second);
}
}
21 changes: 4 additions & 17 deletions dependencies/testproject_featuredemo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,9 @@ int main(int argc, char* argv[]) {
int x = 10;
int y = 10;

// TDT4102::Image image("res/unknown.jpg");
// image.width = 100;
// image.height = 100;
// // TDT4102::Image image("res/unknown.jpg");
// // image.width = 100;
// // image.height = 100;
TDT4102::Image image("res/unknown.jpg");
image.width = 100;
image.height = 100;

std::vector<std::string> colors{"red", "gold", "green", "rebecca_purple", "blue"};

Expand Down Expand Up @@ -160,17 +157,7 @@ int main(int argc, char* argv[]) {
// window.draw_text({mousePosition.x, mousePosition.y + 100}, std::to_string(mousePosition.x) + ", " + std::to_string(mousePosition.y));
// window.draw_text({mousePosition.x, mousePosition.y + 200}, std::to_string(mousePosition.x) + ", " + std::to_string(mousePosition.y), TDT4102::Color::brown, 70, TDT4102::Font::courier_bold_italic);
// window.draw_arc({500, 500}, mousePosition.x, mousePosition.y, 190, 350, TDT4102::Color::orange);
// window.draw_image(mousePosition, image);
// if (rb.isSelected()) {
// std::cout << "Checked\n";
// } else {
// std::cout << "Not checked\n";
// }

// window.draw_text({mousePosition.x, mousePosition.y + 100}, std::to_string(mousePosition.x) + ", " + std::to_string(mousePosition.y));
// window.draw_text({mousePosition.x, mousePosition.y + 200}, std::to_string(mousePosition.x) + ", " + std::to_string(mousePosition.y), TDT4102::Color::brown, 70, TDT4102::Font::courier_bold_italic);
// window.draw_arc({500, 500}, mousePosition.x, mousePosition.y, 190, 350, TDT4102::Color::orange);
// window.draw_image(mousePosition, image);
window.draw_image(mousePosition, image);
// if (rb.isSelected()) {
// std::cout << "Checked\n";
// } else {
Expand Down

0 comments on commit 63bb565

Please sign in to comment.