diff --git a/include/libtrx/engine/image.h b/include/libtrx/engine/image.h new file mode 100644 index 0000000..194ebe7 --- /dev/null +++ b/include/libtrx/engine/image.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} IMAGE_PIXEL; + +typedef struct { + int32_t width; + int32_t height; + IMAGE_PIXEL *data; +} IMAGE; + +IMAGE *Image_Create(int width, int height); +IMAGE *Image_CreateFromFile(const char *path); +void Image_Free(IMAGE *image); + +bool Image_SaveToFile(const IMAGE *image, const char *path); + +IMAGE *Image_ScaleFit( + const IMAGE *source_image, size_t target_width, size_t target_height); + +IMAGE *Image_ScaleCover( + const IMAGE *source_image, size_t target_width, size_t target_height); + +IMAGE *Image_ScaleStretch( + const IMAGE *source_image, size_t target_width, size_t target_height); + +IMAGE *Image_ScaleSmart( + const IMAGE *source_image, size_t target_width, size_t target_height); diff --git a/meson.build b/meson.build index c67a581..eb102a1 100644 --- a/meson.build +++ b/meson.build @@ -49,6 +49,7 @@ sources = [ 'src/engine/audio.c', 'src/engine/audio_sample.c', 'src/engine/audio_stream.c', + 'src/engine/image.c', 'src/filesystem.c', 'src/json/bson_parse.c', 'src/json/bson_write.c', diff --git a/src/engine/image.c b/src/engine/image.c new file mode 100644 index 0000000..6069b18 --- /dev/null +++ b/src/engine/image.c @@ -0,0 +1,549 @@ +#include "engine/image.h" + +#include "filesystem.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +IMAGE *Image_Create(const int width, const int height) +{ + IMAGE *image = Memory_Alloc(sizeof(IMAGE)); + image->width = width; + image->height = height; + image->data = Memory_Alloc(width * height * sizeof(IMAGE_PIXEL)); + return image; +} + +IMAGE *Image_CreateFromFile(const char *const path) +{ + assert(path); + + int error_code; + AVFormatContext *format_ctx = NULL; + const AVCodec *codec = NULL; + AVCodecContext *codec_ctx = NULL; + AVFrame *frame = NULL; + AVPacket *packet = NULL; + struct SwsContext *sws_ctx = NULL; + uint8_t *dst_data[4] = { 0 }; + int dst_linesize[4] = { 0 }; + IMAGE *target_image = NULL; + + char *full_path = File_GetFullPath(path); + error_code = avformat_open_input(&format_ctx, full_path, NULL, NULL); + Memory_FreePointer(&full_path); + if (error_code != 0) { + goto cleanup; + } + + error_code = avformat_find_stream_info(format_ctx, NULL); + if (error_code < 0) { + goto cleanup; + } + + AVStream *video_stream = NULL; + for (unsigned int i = 0; i < format_ctx->nb_streams; i++) { + AVStream *current_stream = format_ctx->streams[i]; + if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + video_stream = current_stream; + break; + } + } + if (!video_stream) { + error_code = AVERROR_STREAM_NOT_FOUND; + goto cleanup; + } + + codec = avcodec_find_decoder(video_stream->codecpar->codec_id); + if (!codec) { + error_code = AVERROR_DEMUXER_NOT_FOUND; + goto cleanup; + } + + codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + error_code = AVERROR(ENOMEM); + goto cleanup; + } + + error_code = + avcodec_parameters_to_context(codec_ctx, video_stream->codecpar); + if (error_code) { + goto cleanup; + } + + error_code = avcodec_open2(codec_ctx, codec, NULL); + if (error_code < 0) { + goto cleanup; + } + + packet = av_packet_alloc(); + av_new_packet(packet, 0); + error_code = av_read_frame(format_ctx, packet); + if (error_code < 0) { + goto cleanup; + } + + error_code = avcodec_send_packet(codec_ctx, packet); + if (error_code < 0) { + goto cleanup; + } + + frame = av_frame_alloc(); + if (!frame) { + error_code = AVERROR(ENOMEM); + goto cleanup; + } + + error_code = avcodec_receive_frame(codec_ctx, frame); + if (error_code < 0) { + goto cleanup; + } + + target_image = Image_Create(frame->width, frame->height); + + sws_ctx = sws_getContext( + codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, + target_image->width, target_image->height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, NULL, NULL, NULL); + + if (!sws_ctx) { + LOG_ERROR("Failed to get SWS context"); + error_code = AVERROR_EXTERNAL; + goto cleanup; + } + + error_code = av_image_alloc( + dst_data, dst_linesize, target_image->width, target_image->height, + AV_PIX_FMT_RGB24, 1); + if (error_code < 0) { + goto cleanup; + } + + sws_scale( + sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, + frame->height, dst_data, dst_linesize); + + av_image_copy_to_buffer( + (uint8_t *)target_image->data, + target_image->width * target_image->height * sizeof(IMAGE_PIXEL), + (const uint8_t *const *)dst_data, dst_linesize, AV_PIX_FMT_RGB24, + target_image->width, target_image->height, 1); + + error_code = 0; + goto success; + +cleanup: + if (target_image) { + Image_Free(target_image); + target_image = NULL; + } + + if (error_code) { + LOG_ERROR( + "Error while opening image %s: %s", path, av_err2str(error_code)); + } + +success: + av_freep(&dst_data[0]); + + if (sws_ctx) { + sws_freeContext(sws_ctx); + } + + if (packet) { + av_packet_free(&packet); + } + + if (frame) { + av_frame_free(&frame); + } + + if (codec_ctx) { + avcodec_close(codec_ctx); + av_free(codec_ctx); + codec_ctx = NULL; + } + + if (format_ctx) { + avformat_close_input(&format_ctx); + } + + return target_image; +} + +bool Image_SaveToFile(const IMAGE *const image, const char *const path) +{ + assert(image); + assert(path); + + bool result = false; + + int error_code = 0; + const AVCodec *codec = NULL; + AVCodecContext *codec_ctx = NULL; + AVFrame *frame = NULL; + AVPacket *packet = NULL; + struct SwsContext *sws_ctx = NULL; + MYFILE *fp = NULL; + + enum AVPixelFormat source_pix_fmt = AV_PIX_FMT_RGB24; + enum AVPixelFormat target_pix_fmt; + enum AVCodecID codec_id; + + if (strstr(path, ".jpg")) { + target_pix_fmt = AV_PIX_FMT_YUVJ420P; + codec_id = AV_CODEC_ID_MJPEG; + } else if (strstr(path, ".png")) { + target_pix_fmt = AV_PIX_FMT_RGB24; + codec_id = AV_CODEC_ID_PNG; + } else { + LOG_ERROR("Cannot determine image format based on path '%s'", path); + goto cleanup; + } + + fp = File_Open(path, FILE_OPEN_WRITE); + if (!fp) { + LOG_ERROR("Cannot create image file: %s", path); + goto cleanup; + } + + codec = avcodec_find_encoder(codec_id); + if (!codec) { + error_code = AVERROR_MUXER_NOT_FOUND; + goto cleanup; + } + + codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + error_code = AVERROR(ENOMEM); + goto cleanup; + } + + codec_ctx->bit_rate = 400000; + codec_ctx->width = image->width; + codec_ctx->height = image->height; + codec_ctx->time_base = (AVRational) { 1, 25 }; + codec_ctx->pix_fmt = target_pix_fmt; + + if (codec_id == AV_CODEC_ID_MJPEG) { + // 9 JPEG quality + codec_ctx->flags |= AV_CODEC_FLAG_QSCALE; + codec_ctx->global_quality = FF_QP2LAMBDA * 9; + } + + error_code = avcodec_open2(codec_ctx, codec, NULL); + if (error_code < 0) { + goto cleanup; + } + + frame = av_frame_alloc(); + if (!frame) { + error_code = AVERROR(ENOMEM); + goto cleanup; + } + frame->format = codec_ctx->pix_fmt; + frame->width = codec_ctx->width; + frame->height = codec_ctx->height; + frame->pts = 0; + + error_code = av_image_alloc( + frame->data, frame->linesize, codec_ctx->width, codec_ctx->height, + codec_ctx->pix_fmt, 32); + if (error_code < 0) { + goto cleanup; + } + + packet = av_packet_alloc(); + av_new_packet(packet, 0); + + sws_ctx = sws_getContext( + image->width, image->height, source_pix_fmt, frame->width, + frame->height, target_pix_fmt, SWS_BILINEAR, NULL, NULL, NULL); + + if (!sws_ctx) { + LOG_ERROR("Failed to get SWS context"); + error_code = AVERROR_EXTERNAL; + goto cleanup; + } + + uint8_t *src_planes[4]; + int src_linesize[4]; + av_image_fill_arrays( + src_planes, src_linesize, (const uint8_t *)image->data, source_pix_fmt, + image->width, image->height, 1); + + sws_scale( + sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, + image->height, frame->data, frame->linesize); + + error_code = avcodec_send_frame(codec_ctx, frame); + if (error_code < 0) { + goto cleanup; + } + + while (error_code >= 0) { + error_code = avcodec_receive_packet(codec_ctx, packet); + if (error_code == AVERROR(EAGAIN) || error_code == AVERROR_EOF) { + error_code = 0; + break; + } + if (error_code < 0) { + goto cleanup; + } + + File_WriteData(fp, packet->data, packet->size); + av_packet_unref(packet); + } + +cleanup: + if (error_code) { + LOG_ERROR( + "Error while saving image %s: %s", path, av_err2str(error_code)); + } else { + result = true; + } + + if (fp) { + File_Close(fp); + fp = NULL; + } + + if (sws_ctx) { + sws_freeContext(sws_ctx); + } + + if (packet) { + av_packet_free(&packet); + } + + if (codec) { + avcodec_close(codec_ctx); + av_free(codec_ctx); + codec_ctx = NULL; + } + + if (frame) { + av_freep(&frame->data[0]); + av_frame_free(&frame); + } + + return result; +} + +IMAGE *Image_ScaleLetterbox( + const IMAGE *const source_image, size_t target_width, size_t target_height) +{ + assert(source_image); + assert(source_image->data); + + IMAGE *target_image = NULL; + int source_width = source_image->width; + int source_height = source_image->height; + + const float source_ratio = source_width / (float)source_height; + const float target_ratio = target_width / (float)target_height; + { + int new_width = source_ratio < target_ratio + ? target_height * source_ratio + : target_width; + int new_height = source_ratio < target_ratio + ? target_height + : target_width / source_ratio; + target_width = new_width; + target_height = new_height; + } + + struct SwsContext *sws_ctx = sws_getContext( + source_width, source_height, AV_PIX_FMT_RGB24, target_width, + target_height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); + + if (!sws_ctx) { + LOG_ERROR("Failed to get SWS context"); + goto cleanup; + } + + target_image = Image_Create(target_width, target_height); + + uint8_t *src_planes[4]; + uint8_t *dst_planes[4]; + int src_linesize[4]; + int dst_linesize[4]; + + av_image_fill_arrays( + src_planes, src_linesize, (const uint8_t *)source_image->data, + AV_PIX_FMT_RGB24, source_width, source_height, 1); + + av_image_fill_arrays( + dst_planes, dst_linesize, (const uint8_t *)target_image->data, + AV_PIX_FMT_RGB24, target_image->width, target_image->height, 1); + + sws_scale( + sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, + source_height, (uint8_t *const *)dst_planes, dst_linesize); + +cleanup: + if (sws_ctx) { + sws_freeContext(sws_ctx); + } + + return target_image; +} + +IMAGE *Image_ScaleCrop( + const IMAGE *const source_image, const size_t target_width, + const size_t target_height) +{ + assert(source_image); + assert(source_image->data); + + IMAGE *target_image = NULL; + int source_width = source_image->width; + int source_height = source_image->height; + + const float source_ratio = source_width / (float)source_height; + const float target_ratio = target_width / (float)target_height; + + int crop_width = source_ratio < target_ratio ? source_width + : source_height * target_ratio; + int crop_height = source_ratio < target_ratio ? source_width / target_ratio + : source_height; + + struct SwsContext *sws_ctx = sws_getContext( + crop_width, crop_height, AV_PIX_FMT_RGB24, target_width, target_height, + AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); + + if (!sws_ctx) { + LOG_ERROR("Failed to get SWS context"); + goto cleanup; + } + + target_image = Image_Create(target_width, target_height); + + uint8_t *src_planes[4]; + uint8_t *dst_planes[4]; + int src_linesize[4]; + int dst_linesize[4]; + + av_image_fill_arrays( + src_planes, src_linesize, (const uint8_t *)source_image->data, + AV_PIX_FMT_RGB24, source_width, source_height, 1); + + src_planes[0] += ((source_height - crop_height) / 2) * src_linesize[0]; + src_planes[0] += ((source_width - crop_width) / 2) * sizeof(IMAGE_PIXEL); + + av_image_fill_arrays( + dst_planes, dst_linesize, (const uint8_t *)target_image->data, + AV_PIX_FMT_RGB24, target_image->width, target_image->height, 1); + + sws_scale( + sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, + crop_height, (uint8_t *const *)dst_planes, dst_linesize); + +cleanup: + if (sws_ctx) { + sws_freeContext(sws_ctx); + } + + return target_image; +} + +IMAGE *Image_ScaleStretch( + const IMAGE *const source_image, const size_t target_width, + const size_t target_height) +{ + assert(source_image); + assert(source_image->data); + + IMAGE *target_image = NULL; + int source_width = source_image->width; + int source_height = source_image->height; + + struct SwsContext *sws_ctx = sws_getContext( + source_width, source_height, AV_PIX_FMT_RGB24, target_width, + target_height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); + + if (!sws_ctx) { + LOG_ERROR("Failed to get SWS context"); + goto cleanup; + } + + target_image = Image_Create(target_width, target_height); + + uint8_t *src_planes[4]; + uint8_t *dst_planes[4]; + int src_linesize[4]; + int dst_linesize[4]; + + av_image_fill_arrays( + src_planes, src_linesize, (const uint8_t *)source_image->data, + AV_PIX_FMT_RGB24, source_width, source_height, 1); + + av_image_fill_arrays( + dst_planes, dst_linesize, (const uint8_t *)target_image->data, + AV_PIX_FMT_RGB24, target_image->width, target_image->height, 1); + + sws_scale( + sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0, + source_height, (uint8_t *const *)dst_planes, dst_linesize); + +cleanup: + if (sws_ctx) { + sws_freeContext(sws_ctx); + } + + return target_image; +} + +IMAGE *Image_ScaleSmart( + const IMAGE *const source_image, const size_t target_width, + const size_t target_height) +{ + assert(source_image); + const float source_ratio = + source_image->width / (float)source_image->height; + const float target_ratio = target_width / (float)target_height; + + // if the difference between aspect ratios is under 10%, just stretch it + const float ar_diff = + (source_ratio > target_ratio ? source_ratio / target_ratio + : target_ratio / source_ratio) + - 1.0f; + if (ar_diff <= 0.1f) { + return Image_ScaleStretch(source_image, target_width, target_height); + } + + // if the viewport is too wide, center the image + if (source_ratio <= target_ratio) { + return Image_ScaleLetterbox(source_image, target_width, target_height); + } + + // if the image is too wide, crop the image + return Image_ScaleCrop(source_image, target_width, target_height); +} + +void Image_Free(IMAGE *image) +{ + if (image) { + Memory_FreePointer(&image->data); + } + Memory_FreePointer(&image); +}