mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-17 23:01:24 +02:00
196 lines
6.6 KiB
C++
196 lines
6.6 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "ui/image_codec.h"
|
|
|
|
#include <cstring>
|
|
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavutil/imgutils.h>
|
|
#include <libavutil/opt.h>
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
|
|
#include "util/log.h"
|
|
|
|
namespace ui {
|
|
|
|
namespace {
|
|
|
|
AVCodecID codec_for(ImageFormat format) {
|
|
switch (format) {
|
|
case ImageFormat::Jpeg: return AV_CODEC_ID_MJPEG;
|
|
case ImageFormat::Png: return AV_CODEC_ID_PNG;
|
|
case ImageFormat::WebP: return AV_CODEC_ID_WEBP;
|
|
default: return AV_CODEC_ID_NONE;
|
|
}
|
|
}
|
|
|
|
// "J" formats (JPEG, full range): swscale considers them obsolete and
|
|
// reports it in the logs; we pass the standard format + full range.
|
|
AVPixelFormat normalize_jpeg_format(AVPixelFormat fmt, bool* full_range) {
|
|
switch (fmt) {
|
|
case AV_PIX_FMT_YUVJ420P: *full_range = true; return AV_PIX_FMT_YUV420P;
|
|
case AV_PIX_FMT_YUVJ422P: *full_range = true; return AV_PIX_FMT_YUV422P;
|
|
case AV_PIX_FMT_YUVJ444P: *full_range = true; return AV_PIX_FMT_YUV444P;
|
|
case AV_PIX_FMT_YUVJ440P: *full_range = true; return AV_PIX_FMT_YUV440P;
|
|
case AV_PIX_FMT_YUVJ411P: *full_range = true; return AV_PIX_FMT_YUV411P;
|
|
default: return fmt;
|
|
}
|
|
}
|
|
|
|
bool decode_frame(AVCodecID id, const std::string& bytes, AVFrame* frame) {
|
|
const AVCodec* codec = avcodec_find_decoder(id);
|
|
if (!codec) {
|
|
return false;
|
|
}
|
|
AVCodecContext* ctx = avcodec_alloc_context3(codec);
|
|
AVPacket* pkt = av_packet_alloc();
|
|
// The decoder may read past the end: buffer padded with zeros.
|
|
std::vector<uint8_t> buffer(bytes.size() + AV_INPUT_BUFFER_PADDING_SIZE, 0);
|
|
std::memcpy(buffer.data(), bytes.data(), bytes.size());
|
|
bool ok = false;
|
|
if (ctx && pkt) {
|
|
ctx->thread_count = 1;
|
|
if (avcodec_open2(ctx, codec, nullptr) == 0) {
|
|
pkt->data = buffer.data();
|
|
pkt->size = static_cast<int>(bytes.size());
|
|
if (avcodec_send_packet(ctx, pkt) == 0) {
|
|
int rc = avcodec_receive_frame(ctx, frame);
|
|
if (rc == AVERROR(EAGAIN)) {
|
|
avcodec_send_packet(ctx, nullptr); // vidange
|
|
rc = avcodec_receive_frame(ctx, frame);
|
|
}
|
|
ok = rc == 0 && frame->width > 0 && frame->height > 0;
|
|
}
|
|
}
|
|
}
|
|
av_packet_free(&pkt);
|
|
avcodec_free_context(&ctx);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ImageFormat sniff_image_format(const std::string& b) {
|
|
const auto at = [&b](size_t i) { return static_cast<unsigned char>(b[i]); };
|
|
if (b.size() >= 3 && at(0) == 0xFF && at(1) == 0xD8 && at(2) == 0xFF) {
|
|
return ImageFormat::Jpeg;
|
|
}
|
|
if (b.size() >= 8 && std::memcmp(b.data(), "\x89PNG\r\n\x1a\n", 8) == 0) {
|
|
return ImageFormat::Png;
|
|
}
|
|
if (b.size() >= 12 && std::memcmp(b.data(), "RIFF", 4) == 0 &&
|
|
std::memcmp(b.data() + 8, "WEBP", 4) == 0) {
|
|
return ImageFormat::WebP;
|
|
}
|
|
return ImageFormat::Unknown;
|
|
}
|
|
|
|
void fit_within(int w, int h, int max_w, int max_h, int* out_w, int* out_h) {
|
|
int ow = w;
|
|
int oh = h;
|
|
if (max_w > 0 && ow > max_w) {
|
|
oh = static_cast<int>((static_cast<int64_t>(oh) * max_w + ow / 2) / ow);
|
|
ow = max_w;
|
|
}
|
|
if (max_h > 0 && oh > max_h) {
|
|
ow = static_cast<int>((static_cast<int64_t>(ow) * max_h + oh / 2) / oh);
|
|
oh = max_h;
|
|
}
|
|
*out_w = ow > 0 ? ow : 1;
|
|
*out_h = oh > 0 ? oh : 1;
|
|
}
|
|
|
|
bool decode_image(const std::string& bytes, int max_w, int max_h, Image* out) {
|
|
const AVCodecID id = codec_for(sniff_image_format(bytes));
|
|
if (id == AV_CODEC_ID_NONE) {
|
|
LOG_DEBUG("Image : format non reconnu (%zu octets)", bytes.size());
|
|
return false;
|
|
}
|
|
AVFrame* frame = av_frame_alloc();
|
|
if (!frame || !decode_frame(id, bytes, frame)) {
|
|
LOG_DEBUG("Image: cannot decode (%zu bytes)", bytes.size());
|
|
av_frame_free(&frame);
|
|
return false;
|
|
}
|
|
int dw = 0;
|
|
int dh = 0;
|
|
fit_within(frame->width, frame->height, max_w, max_h, &dw, &dh);
|
|
bool full_range = frame->color_range == AVCOL_RANGE_JPEG;
|
|
const AVPixelFormat src_fmt =
|
|
normalize_jpeg_format(static_cast<AVPixelFormat>(frame->format), &full_range);
|
|
// Downscaling: SWS_AREA (average, no aliasing); otherwise bicubic.
|
|
const int flags = (dw < frame->width) ? SWS_AREA : SWS_BICUBIC;
|
|
SwsContext* sws = sws_getContext(frame->width, frame->height, src_fmt, dw, dh,
|
|
AV_PIX_FMT_RGBA, flags, nullptr, nullptr, nullptr);
|
|
bool ok = false;
|
|
if (sws) {
|
|
int* inv_table = nullptr;
|
|
int* table = nullptr;
|
|
int src_range = 0, dst_range = 0, brightness = 0, contrast = 0, saturation = 0;
|
|
if (sws_getColorspaceDetails(sws, &inv_table, &src_range, &table, &dst_range,
|
|
&brightness, &contrast, &saturation) >= 0) {
|
|
sws_setColorspaceDetails(sws, inv_table, full_range ? 1 : 0, table, 1, brightness,
|
|
contrast, saturation);
|
|
}
|
|
out->w = dw;
|
|
out->h = dh;
|
|
out->rgba.assign(static_cast<size_t>(dw) * dh * 4, 0);
|
|
uint8_t* dst[1] = {out->rgba.data()};
|
|
const int dst_stride[1] = {dw * 4};
|
|
ok = sws_scale(sws, frame->data, frame->linesize, 0, frame->height, dst, dst_stride) ==
|
|
dh;
|
|
sws_freeContext(sws);
|
|
}
|
|
av_frame_free(&frame);
|
|
return ok;
|
|
}
|
|
|
|
bool encode_png(const Image& image, std::string* out) {
|
|
if (image.w <= 0 || image.h <= 0 ||
|
|
image.rgba.size() < static_cast<size_t>(image.w) * image.h * 4) {
|
|
return false;
|
|
}
|
|
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_PNG);
|
|
if (!codec) {
|
|
return false;
|
|
}
|
|
AVCodecContext* ctx = avcodec_alloc_context3(codec);
|
|
AVFrame* frame = av_frame_alloc();
|
|
AVPacket* pkt = av_packet_alloc();
|
|
bool ok = false;
|
|
if (ctx && frame && pkt) {
|
|
ctx->width = image.w;
|
|
ctx->height = image.h;
|
|
ctx->pix_fmt = AV_PIX_FMT_RGB24;
|
|
ctx->time_base = AVRational{1, 25};
|
|
ctx->compression_level = 9;
|
|
av_opt_set(ctx->priv_data, "pred", "mixed", 0); // meilleure compression
|
|
frame->format = AV_PIX_FMT_RGB24;
|
|
frame->width = image.w;
|
|
frame->height = image.h;
|
|
if (avcodec_open2(ctx, codec, nullptr) == 0 && av_frame_get_buffer(frame, 0) == 0) {
|
|
for (int y = 0; y < image.h; ++y) {
|
|
const uint8_t* src = &image.rgba[static_cast<size_t>(y) * image.w * 4];
|
|
uint8_t* dst = frame->data[0] + static_cast<size_t>(y) * frame->linesize[0];
|
|
for (int x = 0; x < image.w; ++x) {
|
|
dst[x * 3 + 0] = src[x * 4 + 0];
|
|
dst[x * 3 + 1] = src[x * 4 + 1];
|
|
dst[x * 3 + 2] = src[x * 4 + 2];
|
|
}
|
|
}
|
|
if (avcodec_send_frame(ctx, frame) == 0 && avcodec_receive_packet(ctx, pkt) == 0) {
|
|
out->assign(reinterpret_cast<const char*>(pkt->data), static_cast<size_t>(pkt->size));
|
|
ok = true;
|
|
}
|
|
}
|
|
}
|
|
av_packet_free(&pkt);
|
|
av_frame_free(&frame);
|
|
avcodec_free_context(&ctx);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace ui
|