fix(media-proxy): tone map HDR video instead of refusing it (#2606)

This commit is contained in:
Hampus
2026-09-08 21:18:55 +02:00
committed by GitHub
parent cdecda7f78
commit 63e3be5750
8 changed files with 892 additions and 626 deletions
+4 -2
View File
@@ -9,8 +9,9 @@ fn is_distribution_lib_dir(path: &Path) -> bool {
path.starts_with("/usr/lib") || path.starts_with("/lib")
}
const NATIVE_SHIM_SOURCES: [&str; 16] = [
const NATIVE_SHIM_SOURCES: [&str; 17] = [
"src/webp_animation.c",
"src/native_shim/hdr_color.c",
"src/native_shim/av_input.c",
"src/native_shim/av_frame_rgba.c",
"src/native_shim/vips_image.c",
@@ -28,7 +29,8 @@ const NATIVE_SHIM_SOURCES: [&str; 16] = [
"src/native_shim/nsfw_webp.c",
];
const NATIVE_SHIM_HEADERS: [&str; 3] = [
const NATIVE_SHIM_HEADERS: [&str; 4] = [
"src/native_shim/hdr_color.h",
"src/vips_shim.h",
"src/webp_animation.h",
"src/native_shim/native_shim_internal.h",
+2 -1
View File
@@ -463,7 +463,8 @@ fn nsfw_frame_emit_failure_is_reported_instead_of_decoder_eof() {
assert_eq!(NativeStatus::Ok, sdr_status);
assert_eq!(3, sdr_frames.expect("sdr frames").len());
let rejected = matroska_video(&["-colorspace", "bt2020nc"]).expect("ffmpeg tags the matrix");
let rejected =
matroska_video(&["-vf", "setparams=color_trc=smpte428"]).expect("ffmpeg tags the transfer");
let (status, frames) = av_nsfw_extract(&rejected, &timestamps);
assert_eq!(NativeStatus::Unsupported, status);
assert_eq!(Err(NSFWFrameCopyError::InvalidOutput), frames);
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::super::{MediaError, extract_video_thumbnail};
use super::super::{
MediaError, VideoThumbnailOptions, extract_video_thumbnail,
extract_video_thumbnail_with_options,
};
use super::fixtures::{decode_rgba, metadata_value, test_media_limits};
use crate::{
image_quality::ResolvedImageQuality,
output_format::OutputFormat,
test_fixtures::{
ffmpeg_gen_media, ffmpeg_gen_mp4, ffmpeg_gen_rotated_mp4, ffmpeg_mirror_mp4, png_dimensions,
@@ -115,32 +119,171 @@ fn video_thumbnail_accepts_every_widened_sdr_transfer() {
}
#[test]
fn video_thumbnail_still_rejects_pq_bt2020_hdr() {
fn video_thumbnail_tone_maps_hdr_instead_of_refusing_it() {
let Some(hdr) = color_tagged_video("bt2020", "smpte2084", "bt2020nc") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
let thumb = extract_video_thumbnail(&hdr, OutputFormat::PNG, &test_media_limits())
.expect("pq bt2020 video should thumbnail");
assert_eq!(png_dimensions(&thumb.bytes), Some((320, 240)));
for (primaries, transfer, matrix) in [
("bt709", "smpte2084", "bt709"),
("bt2020", "arib-std-b67", "bt709"),
("bt2020", "arib-std-b67", "bt2020nc"),
("bt2020", "bt709", "bt709"),
("smpte432", "bt709", "bt709"),
] {
let video = color_tagged_video(primaries, transfer, matrix)
.unwrap_or_else(|| panic!("{primaries}/{transfer}/{matrix} fixture"));
let thumb = extract_video_thumbnail(&video, OutputFormat::PNG, &test_media_limits())
.unwrap_or_else(|error| {
panic!("{primaries}/{transfer}/{matrix} should thumbnail: {error:?}")
});
assert_eq!(
png_dimensions(&thumb.bytes),
Some((320, 240)),
"{primaries}/{transfer}/{matrix}"
);
}
}
#[test]
fn hdr_video_thumbnail_still_errors_on_an_expired_deadline() {
let Some(hdr) = color_tagged_video("bt2020", "smpte2084", "bt2020nc") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
assert!(matches!(
extract_video_thumbnail_with_options(
&hdr,
VideoThumbnailOptions {
format: OutputFormat::PNG,
width: None,
height: None,
quality: ResolvedImageQuality::High,
deadline_ms: Some(1),
},
&test_media_limits(),
),
Err(MediaError::RequestTimeout)
));
}
#[test]
fn video_thumbnail_still_rejects_colour_signals_it_cannot_model() {
let Some(cinema_transfer) = color_tagged_video("bt709", "smpte428", "bt709") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
assert_eq!(
Some(MediaError::MediaDecodeFailed),
extract_video_thumbnail(&hdr, OutputFormat::PNG, &test_media_limits()).err()
extract_video_thumbnail(&cinema_transfer, OutputFormat::PNG, &test_media_limits()).err()
);
let pq_only = color_tagged_video("bt709", "smpte2084", "bt709").expect("pq fixture");
let cinema_primaries =
color_tagged_video("smpte428", "bt709", "bt709").expect("smpte428 primaries fixture");
assert_eq!(
Some(MediaError::MediaDecodeFailed),
extract_video_thumbnail(&pq_only, OutputFormat::PNG, &test_media_limits()).err()
extract_video_thumbnail(&cinema_primaries, OutputFormat::PNG, &test_media_limits()).err()
);
}
let hlg = color_tagged_video("bt2020", "arib-std-b67", "bt709").expect("hlg fixture");
assert_eq!(
Some(MediaError::MediaDecodeFailed),
extract_video_thumbnail(&hlg, OutputFormat::PNG, &test_media_limits()).err()
fn flat_colour_tagged_video(
colour: &str,
primaries: &str,
transfer: &str,
matrix: &str,
) -> Option<Vec<u8>> {
let params =
format!("setparams=color_primaries={primaries}:color_trc={transfer}:colorspace={matrix}");
ffmpeg_gen_media(
"fixture.mkv",
&[
"-f",
"lavfi",
"-i",
&format!("color=c={colour}:size=320x240:rate=10:duration=1"),
"-vf",
&params,
"-pix_fmt",
"yuv420p",
"-c:v",
"mpeg4",
"-qscale:v",
"1",
"-f",
"matroska",
],
)
}
fn flat_colour_centre_pixel(
colour: &str,
primaries: &str,
transfer: &str,
matrix: &str,
) -> Option<[u8; 4]> {
let video = flat_colour_tagged_video(colour, primaries, transfer, matrix)?;
let thumb = extract_video_thumbnail(&video, OutputFormat::PNG, &test_media_limits())
.unwrap_or_else(|error| panic!("{primaries}/{transfer}/{matrix} thumbnails: {error:?}"));
let (width, height, rgba) = decode_rgba(&thumb.bytes);
assert_eq!((320, 240), (width, height));
let centre = ((height as usize / 2) * width as usize + width as usize / 2) * 4;
Some([
rgba[centre],
rgba[centre + 1],
rgba[centre + 2],
rgba[centre + 3],
])
}
#[test]
fn a_pq_video_frame_is_tone_mapped_rather_than_read_as_srgb() {
let Some(sdr) = flat_colour_centre_pixel("0xc0c0c0", "bt709", "bt709", "bt709") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
let hdr = flat_colour_centre_pixel("0xc0c0c0", "bt2020", "smpte2084", "bt2020nc")
.expect("pq fixture");
assert!(
hdr[0] > sdr[0] + 20,
"a PQ highlight read as plain sRGB would land on the sdr value, got {hdr:?} against {sdr:?}"
);
assert_eq!(255, hdr[3], "tone mapped output stays opaque");
}
let wide_primaries = color_tagged_video("bt2020", "bt709", "bt709").expect("bt2020 fixture");
assert_eq!(
Some(MediaError::MediaDecodeFailed),
extract_video_thumbnail(&wide_primaries, OutputFormat::PNG, &test_media_limits()).err()
#[test]
fn an_hlg_video_frame_is_tone_mapped_rather_than_read_as_srgb() {
let Some(sdr) = flat_colour_centre_pixel("0xc0c0c0", "bt709", "bt709", "bt709") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
let hdr = flat_colour_centre_pixel("0xc0c0c0", "bt2020", "arib-std-b67", "bt2020nc")
.expect("hlg fixture");
assert!(
hdr[0] > sdr[0] + 20,
"an HLG highlight read as plain sRGB would land on the sdr value, got {hdr:?} against {sdr:?}"
);
assert_eq!(255, hdr[3], "tone mapped output stays opaque");
}
#[test]
fn a_bt2020_sdr_video_frame_is_converted_into_the_bt709_gamut() {
let Some(wide) = flat_colour_centre_pixel("0x00ff00", "bt2020", "bt709", "bt709") else {
eprintln!("skipping: ffmpeg CLI not available");
return;
};
let narrow =
flat_colour_centre_pixel("0x00ff00", "bt709", "bt709", "bt709").expect("bt709 fixture");
let shift = (0..3)
.map(|channel| wide[channel].abs_diff(narrow[channel]))
.max()
.expect("three channels");
assert!(
shift > 8,
"bt2020 green {wide:?} should not match bt709 green {narrow:?}"
);
}
+149 -126
View File
@@ -2,46 +2,74 @@
#include "native_shim_internal.h"
enum ff_sdr_transfer {
FF_SDR_TRANSFER_SRGB = 0,
FF_SDR_TRANSFER_LINEAR = 1,
struct ff_frame_color_plan {
int transfer;
int gamut;
};
static uint8_t ff_linear_to_srgb_lut[256];
static pthread_once_t ff_transfer_lut_once = PTHREAD_ONCE_INIT;
static uint8_t ff_encode_srgb_byte(double linear) {
double srgb = linear <= 0.0031308
? 12.92 * linear
: 1.055 * pow(linear, 1.0 / 2.4) - 0.055;
long quantized = lround(srgb * 255.0);
if (quantized < 0) quantized = 0;
if (quantized > 255) quantized = 255;
return (uint8_t)quantized;
}
static void ff_initialize_transfer_luts(void) {
for (int index = 0; index < 256; index++) {
double encoded = (double)index / 255.0;
ff_linear_to_srgb_lut[index] = ff_encode_srgb_byte(encoded);
}
}
static const uint8_t *ff_transfer_to_srgb_lut(enum ff_sdr_transfer transfer) {
switch (transfer) {
case FF_SDR_TRANSFER_LINEAR:
return ff_linear_to_srgb_lut;
static int ff_frame_color_gamut(const AVFrame *frame, int *out_gamut) {
assert(frame != NULL);
assert(out_gamut != NULL);
switch (frame->color_primaries) {
case AVCOL_PRI_UNSPECIFIED:
case AVCOL_PRI_BT709:
case AVCOL_PRI_BT470M:
case AVCOL_PRI_BT470BG:
case AVCOL_PRI_SMPTE170M:
case AVCOL_PRI_SMPTE240M:
case AVCOL_PRI_FILM:
*out_gamut = FLUXER_HDR_GAMUT_SRGB;
return 0;
case AVCOL_PRI_BT2020:
*out_gamut = FLUXER_HDR_GAMUT_BT2020;
return 0;
case AVCOL_PRI_SMPTE432:
*out_gamut = FLUXER_HDR_GAMUT_DISPLAY_P3;
return 0;
default:
return NULL;
return -1;
}
}
static int ff_frame_sdr_transfer(
const AVFrame *frame,
enum ff_sdr_transfer *out_transfer
) {
static int ff_frame_color_transfer(const AVFrame *frame, int *out_transfer) {
assert(frame != NULL);
assert(out_transfer != NULL);
switch (frame->color_trc) {
case AVCOL_TRC_UNSPECIFIED:
case AVCOL_TRC_IEC61966_2_1:
case AVCOL_TRC_GAMMA22:
case AVCOL_TRC_GAMMA28:
*out_transfer = FLUXER_HDR_TRANSFER_SRGB;
return 0;
case AVCOL_TRC_BT709:
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_SMPTE240M:
case AVCOL_TRC_BT2020_10:
*out_transfer = FLUXER_HDR_TRANSFER_BT709;
return 0;
case AVCOL_TRC_BT2020_12:
*out_transfer = FLUXER_HDR_TRANSFER_BT2020_12;
return 0;
case AVCOL_TRC_LINEAR:
*out_transfer = FLUXER_HDR_TRANSFER_LINEAR;
return 0;
case AVCOL_TRC_SMPTE2084:
*out_transfer = FLUXER_HDR_TRANSFER_PQ;
return 0;
case AVCOL_TRC_ARIB_STD_B67:
*out_transfer = FLUXER_HDR_TRANSFER_HLG;
return 0;
default:
return -1;
}
}
static int ff_frame_color_plan(
const AVFrame *frame,
struct ff_frame_color_plan *out_plan
) {
assert(frame != NULL);
assert(out_plan != NULL);
switch (frame->colorspace) {
case AVCOL_SPC_RGB:
case AVCOL_SPC_BT709:
@@ -50,6 +78,7 @@ static int ff_frame_sdr_transfer(
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
case AVCOL_SPC_SMPTE240M:
case AVCOL_SPC_BT2020_NCL:
break;
default:
return -1;
@@ -59,75 +88,17 @@ static int ff_frame_sdr_transfer(
frame->color_range != AVCOL_RANGE_JPEG) {
return -1;
}
switch (frame->color_primaries) {
case AVCOL_PRI_UNSPECIFIED:
case AVCOL_PRI_BT709:
case AVCOL_PRI_BT470M:
case AVCOL_PRI_BT470BG:
case AVCOL_PRI_SMPTE170M:
case AVCOL_PRI_SMPTE240M:
case AVCOL_PRI_FILM:
break;
default:
return -1;
}
switch (frame->color_trc) {
case AVCOL_TRC_UNSPECIFIED:
case AVCOL_TRC_IEC61966_2_1:
case AVCOL_TRC_GAMMA22:
case AVCOL_TRC_GAMMA28:
*out_transfer = FF_SDR_TRANSFER_SRGB;
return 0;
case AVCOL_TRC_BT709:
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_SMPTE240M:
*out_transfer = FF_SDR_TRANSFER_SRGB;
return 0;
case AVCOL_TRC_LINEAR:
*out_transfer = FF_SDR_TRANSFER_LINEAR;
return 0;
default:
return -1;
}
if (ff_frame_color_gamut(frame, &out_plan->gamut) != 0) return -1;
return ff_frame_color_transfer(frame, &out_plan->transfer);
}
static int ff_convert_rgba_transfer_to_srgb(
enum ff_sdr_transfer transfer,
uint8_t *data,
int width,
int height,
long long deadline_monotonic_ms
static int ff_frame_plan_is_passthrough(
const struct ff_frame_color_plan *plan
) {
assert(data != NULL);
assert(width > 0);
assert(height > 0);
if (deadline_monotonic_ms < 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
if (pthread_once(
&ff_transfer_lut_once,
ff_initialize_transfer_luts) != 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
const uint8_t *lut = ff_transfer_to_srgb_lut(transfer);
if (lut == NULL) {
return fluxer_native_deadline_status(deadline_monotonic_ms);
}
size_t row_bytes = (size_t)width * 4u;
for (int row = 0; row < height; row++) {
if (row % FLUXER_VIDEO_DEADLINE_ROWS == 0) {
int status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
}
uint8_t *row_data = data + (size_t)row * row_bytes;
for (int column = 0; column < width; column++) {
uint8_t *pixel = row_data + (size_t)column * 4u;
pixel[0] = lut[pixel[0]];
pixel[1] = lut[pixel[1]];
pixel[2] = lut[pixel[2]];
}
}
return fluxer_native_deadline_status(deadline_monotonic_ms);
assert(plan != NULL);
return plan->gamut == FLUXER_HDR_GAMUT_SRGB &&
!fluxer_hdr_transfer_is_hdr(plan->transfer) &&
plan->transfer != FLUXER_HDR_TRANSFER_LINEAR;
}
typedef int (*ff_i420_to_abgr_fn)(
@@ -150,6 +121,8 @@ static int ff_swscale_colorspace(enum AVColorSpace colorspace) {
return SWS_CS_SMPTE170M;
case AVCOL_SPC_SMPTE240M:
return SWS_CS_SMPTE240M;
case AVCOL_SPC_BT2020_NCL:
return SWS_CS_BT2020;
default:
return SWS_CS_DEFAULT;
}
@@ -331,6 +304,7 @@ static int ff_swscale_frame_to_rgba(
int source_height,
int output_width,
int output_height,
int destination_pixel_bytes,
long long deadline_monotonic_ms,
uint8_t *dst
) {
@@ -339,6 +313,7 @@ static int ff_swscale_frame_to_rgba(
assert(source_height > 0);
assert(output_width > 0);
assert(output_height > 0);
assert(destination_pixel_bytes == 4 || destination_pixel_bytes == 8);
assert(dst != NULL);
const AVPixFmtDescriptor *descriptor = av_pix_fmt_desc_get(frame->format);
if (descriptor == NULL) return FLUXER_NATIVE_STATUS_UNSUPPORTED;
@@ -350,7 +325,7 @@ static int ff_swscale_frame_to_rgba(
if (source_height < 2) return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
uint8_t *dst_data[4] = { dst, NULL, NULL, NULL };
int dst_linesize[4] = { output_width * 4, 0, 0, 0 };
int dst_linesize[4] = { output_width * destination_pixel_bytes, 0, 0, 0 };
int output_rows = 0;
for (int source_row = 0; source_row < source_height;) {
status = fluxer_native_deadline_status(deadline_monotonic_ms);
@@ -395,6 +370,54 @@ static int ff_swscale_frame_to_rgba(
: FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
static int ff_convert_hdr_frame_to_rgba(
AVFrame *frame,
int source_width,
int source_height,
int output_width,
int output_height,
struct SwsContext **sws,
long long deadline_monotonic_ms,
uint8_t *dst,
size_t rgba_size,
const struct ff_frame_color_plan *plan
) {
assert(sws != NULL);
assert(dst != NULL);
assert(plan != NULL);
if (rgba_size > FLUXER_MAX_VIDEO_RGBA_BYTES / 2u) {
return FLUXER_NATIVE_STATUS_INVALID_DIMENSIONS;
}
uint8_t *staging = (uint8_t *)g_try_malloc(rgba_size * 2u);
if (staging == NULL) return FLUXER_NATIVE_STATUS_ALLOCATION_FAILED;
*sws = sws_getCachedContext(*sws,
source_width, source_height,
(enum AVPixelFormat)frame->format,
output_width, output_height,
AV_PIX_FMT_RGBA64LE,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
int status = FLUXER_NATIVE_STATUS_OK;
if (*sws == NULL) {
status = FLUXER_NATIVE_STATUS_CODEC_FAILURE;
} else if (ff_configure_swscale_color(*sws, frame) != 0) {
status = FLUXER_NATIVE_STATUS_UNSUPPORTED;
} else {
status = ff_swscale_frame_to_rgba(
*sws, frame, source_height, output_width, output_height, 8,
deadline_monotonic_ms, staging);
}
if (status == FLUXER_NATIVE_STATUS_OK) {
status = fluxer_hdr_tone_map_rgba16(
staging, (size_t)output_width * 8u,
dst, (size_t)output_width * 4u,
output_width, output_height, 16,
plan->gamut, plan->transfer, FLUXER_VIDEO_DEADLINE_ROWS,
deadline_monotonic_ms);
}
g_free(staging);
return status;
}
int fluxer_av_frame_convert_to_rgba(
AVFrame *frame,
int source_width,
@@ -411,43 +434,43 @@ int fluxer_av_frame_convert_to_rgba(
}
int status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
size_t rgba_size = 0;
if (ff_validate_rgba_geometry(source_width, source_height, NULL) != 0 ||
ff_validate_rgba_geometry(output_width, output_height, NULL) != 0) {
ff_validate_rgba_geometry(output_width, output_height, &rgba_size) != 0) {
return FLUXER_NATIVE_STATUS_INVALID_DIMENSIONS;
}
enum ff_sdr_transfer transfer = FF_SDR_TRANSFER_SRGB;
if (ff_frame_sdr_transfer(frame, &transfer) != 0) {
struct ff_frame_color_plan plan;
if (ff_frame_color_plan(frame, &plan) != 0) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if (fluxer_hdr_transfer_is_hdr(plan.transfer)) {
return ff_convert_hdr_frame_to_rgba(
frame, source_width, source_height, output_width, output_height,
sws, deadline_monotonic_ms, dst, rgba_size, &plan);
}
int libyuv_applied = 0;
status = ff_convert_i420_frame_to_rgba_libyuv(
frame, source_width, source_height, output_width, output_height,
deadline_monotonic_ms, dst, &libyuv_applied);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
if (libyuv_applied) {
if (transfer != FF_SDR_TRANSFER_SRGB) {
return ff_convert_rgba_transfer_to_srgb(
transfer, dst, output_width, output_height,
deadline_monotonic_ms);
if (!libyuv_applied) {
*sws = sws_getCachedContext(*sws,
source_width, source_height,
(enum AVPixelFormat)frame->format,
output_width, output_height,
AV_PIX_FMT_RGBA,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
if (*sws == NULL) return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
if (ff_configure_swscale_color(*sws, frame) != 0) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
return FLUXER_NATIVE_STATUS_OK;
status = ff_swscale_frame_to_rgba(
*sws, frame, source_height, output_width, output_height, 4,
deadline_monotonic_ms, dst);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
}
*sws = sws_getCachedContext(*sws,
source_width, source_height,
(enum AVPixelFormat)frame->format,
output_width, output_height, AV_PIX_FMT_RGBA,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
if (*sws == NULL) return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
if (ff_configure_swscale_color(*sws, frame) != 0) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
status = ff_swscale_frame_to_rgba(
*sws, frame, source_height, output_width, output_height,
deadline_monotonic_ms, dst);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
if (transfer != FF_SDR_TRANSFER_SRGB) {
return ff_convert_rgba_transfer_to_srgb(
transfer, dst, output_width, output_height, deadline_monotonic_ms);
}
return FLUXER_NATIVE_STATUS_OK;
if (ff_frame_plan_is_passthrough(&plan)) return FLUXER_NATIVE_STATUS_OK;
return fluxer_hdr_apply_sdr_gamut(
dst, output_width, output_height, plan.gamut, plan.transfer,
FLUXER_VIDEO_DEADLINE_ROWS, deadline_monotonic_ms);
}
@@ -0,0 +1,250 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#include "native_shim_internal.h"
float fluxer_pq_lut[FLUXER_HDR_PQ_LUT_SIZE];
float fluxer_hlg_lut[FLUXER_HDR_HLG_LUT_SIZE];
float fluxer_hlg_ootf_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
float fluxer_pq_tone_scale_lut[FLUXER_HDR_PQ_LUT_SIZE];
float fluxer_hlg_tone_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
uint8_t fluxer_srgb_lut[FLUXER_HDR_SRGB_LUT_SIZE];
float fluxer_pq_sdr_target_perceptual;
float fluxer_hlg_source_peak_perceptual;
float fluxer_hlg_sdr_target_perceptual;
static pthread_once_t fluxer_hdr_lut_once = PTHREAD_ONCE_INIT;
static void fluxer_init_hdr_luts(void) {
const double m1 = 0.1593017578125;
const double m2 = 78.84375;
const double c1 = 0.8359375;
const double c2 = 18.8515625;
const double c3 = 18.6875;
for (int index = 0; index < FLUXER_HDR_PQ_LUT_SIZE; index++) {
double encoded = (double)index / (FLUXER_HDR_PQ_LUT_SIZE - 1);
double encoded_power = pow(encoded, 1.0 / m2);
double numerator = encoded_power - c1;
if (numerator < 0.0) numerator = 0.0;
double denominator = c2 - c3 * encoded_power;
double luminance = denominator > 0.0
? pow(numerator / denominator, 1.0 / m1)
: 0.0;
if (luminance < 0.0) luminance = 0.0;
if (luminance > 1.0) luminance = 1.0;
fluxer_pq_lut[index] = (float)luminance;
}
const double a = 0.17883277;
const double b = 0.28466892;
const double c = 0.55991073;
for (int index = 0; index < FLUXER_HDR_HLG_LUT_SIZE; index++) {
double encoded = (double)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
double scene = encoded <= 0.5
? (encoded * encoded) / 3.0
: (exp((encoded - c) / a) + b) / 12.0;
if (scene < 0.0) scene = 0.0;
if (scene > 1.0) scene = 1.0;
fluxer_hlg_lut[index] = (float)scene;
double normalized = (double)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
fluxer_hlg_ootf_scale_lut[index] = normalized > 0.0
? (float)pow(normalized, 0.2)
: 0.0f;
}
fluxer_pq_sdr_target_perceptual =
fluxer_pq_oetf(FLUXER_PQ_SDR_TARGET_NORM);
fluxer_hlg_source_peak_perceptual =
fluxer_pq_oetf(FLUXER_HLG_REFERENCE_PEAK_NORM);
fluxer_hlg_sdr_target_perceptual =
fluxer_pq_sdr_target_perceptual /
fluxer_hlg_source_peak_perceptual;
for (int index = 0; index < FLUXER_HDR_PQ_LUT_SIZE; index++) {
fluxer_pq_tone_scale_lut[index] = fluxer_hdr_tone_scale(
fluxer_pq_lut[index], FLUXER_PQ_SDR_TARGET_NORM,
1.0f,
fluxer_pq_sdr_target_perceptual);
}
for (int index = 0; index < FLUXER_HDR_HLG_LUT_SIZE; index++) {
float maximum = (float)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
float absolute_maximum =
maximum * FLUXER_HLG_REFERENCE_PEAK_NORM;
fluxer_hlg_tone_scale_lut[index] =
FLUXER_HLG_REFERENCE_PEAK_NORM * fluxer_hdr_tone_scale(
absolute_maximum, FLUXER_PQ_SDR_TARGET_NORM,
fluxer_hlg_source_peak_perceptual,
fluxer_hlg_sdr_target_perceptual);
}
for (int index = 0; index < FLUXER_HDR_SRGB_LUT_SIZE; index++) {
float linear = (float)index / (FLUXER_HDR_SRGB_LUT_SIZE - 1);
fluxer_srgb_lut[index] = fluxer_quantize8(
fluxer_srgb_oetf(linear));
}
}
static inline uint16_t fluxer_hdr_read_le16(const uint8_t *value) {
return (uint16_t)((uint16_t)value[0] | ((uint16_t)value[1] << 8));
}
int fluxer_hdr_luts_ready(void) {
if (pthread_once(&fluxer_hdr_lut_once, fluxer_init_hdr_luts) != 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
return FLUXER_NATIVE_STATUS_OK;
}
int fluxer_hdr_transfer_is_hdr(int transfer) {
return transfer == FLUXER_HDR_TRANSFER_PQ ||
transfer == FLUXER_HDR_TRANSFER_HLG;
}
int fluxer_hdr_apply_sdr_gamut(
uint8_t *rgba,
int width,
int height,
int gamut,
int transfer,
int deadline_rows,
long long deadline_monotonic_ms
) {
if (rgba == NULL || width <= 0 || height <= 0 || deadline_rows <= 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
if (fluxer_hdr_transfer_is_hdr(transfer)) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if (transfer != FLUXER_HDR_TRANSFER_SRGB &&
transfer != FLUXER_HDR_TRANSFER_BT709 &&
transfer != FLUXER_HDR_TRANSFER_BT2020_12 &&
transfer != FLUXER_HDR_TRANSFER_LINEAR) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if (transfer == FLUXER_HDR_TRANSFER_SRGB &&
gamut == FLUXER_HDR_GAMUT_SRGB) {
return FLUXER_NATIVE_STATUS_OK;
}
int status = fluxer_hdr_luts_ready();
if (status != FLUXER_NATIVE_STATUS_OK) return status;
size_t row_bytes = (size_t)width * 4u;
for (int row = 0; row < height; row++) {
if (row % deadline_rows == 0) {
status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
}
uint8_t *row_data = rgba + (size_t)row * row_bytes;
for (int column = 0; column < width; column++) {
uint8_t *pixel = row_data + (size_t)column * 4u;
float red = (float)pixel[0] / 255.0f;
float green = (float)pixel[1] / 255.0f;
float blue = (float)pixel[2] / 255.0f;
if (transfer == FLUXER_HDR_TRANSFER_SRGB) {
red = fluxer_inverse_srgb(red);
green = fluxer_inverse_srgb(green);
blue = fluxer_inverse_srgb(blue);
} else if (transfer == FLUXER_HDR_TRANSFER_BT709) {
red = fluxer_inverse_bt709(red);
green = fluxer_inverse_bt709(green);
blue = fluxer_inverse_bt709(blue);
} else if (transfer == FLUXER_HDR_TRANSFER_BT2020_12) {
red = fluxer_inverse_bt2020_12(red);
green = fluxer_inverse_bt2020_12(green);
blue = fluxer_inverse_bt2020_12(blue);
}
float srgb_red = 0.0f;
float srgb_green = 0.0f;
float srgb_blue = 0.0f;
fluxer_hdr_convert_gamut_linear(
gamut, red, green, blue,
&srgb_red, &srgb_green, &srgb_blue);
pixel[0] = fluxer_quantize8(fluxer_srgb_oetf(srgb_red));
pixel[1] = fluxer_quantize8(fluxer_srgb_oetf(srgb_green));
pixel[2] = fluxer_quantize8(fluxer_srgb_oetf(srgb_blue));
}
}
return fluxer_native_deadline_status(deadline_monotonic_ms);
}
int fluxer_hdr_tone_map_rgba16(
const uint8_t *source,
size_t source_stride,
uint8_t *destination,
size_t destination_stride,
int width,
int height,
int bit_depth,
int gamut,
int transfer,
int deadline_rows,
long long deadline_monotonic_ms
) {
if (source == NULL || destination == NULL || width <= 0 || height <= 0 ||
deadline_rows <= 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
if (bit_depth != 10 && bit_depth != 12 && bit_depth != 16) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if (!fluxer_hdr_transfer_is_hdr(transfer)) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if ((size_t)width > SIZE_MAX / 8u) {
return FLUXER_NATIVE_STATUS_INVALID_DIMENSIONS;
}
if (source_stride < (size_t)width * 8u ||
destination_stride < (size_t)width * 4u) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
int status = fluxer_hdr_luts_ready();
if (status != FLUXER_NATIVE_STATUS_OK) return status;
int is_hlg = transfer == FLUXER_HDR_TRANSFER_HLG;
unsigned int mask = bit_depth == 16
? 0xffffu
: (unsigned int)((1u << bit_depth) - 1u);
const float *linear_lut = is_hlg ? fluxer_hlg_lut : fluxer_pq_lut;
const float *tone_scale_lut =
is_hlg ? fluxer_hlg_tone_scale_lut : fluxer_pq_tone_scale_lut;
for (int row = 0; row < height; row++) {
if (row % deadline_rows == 0) {
status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
}
const uint8_t *source_row = source + (size_t)row * source_stride;
uint8_t *destination_row =
destination + (size_t)row * destination_stride;
for (int column = 0; column < width; column++) {
const uint8_t *source_pixel = source_row + (size_t)column * 8u;
uint16_t red_code =
(uint16_t)(fluxer_hdr_read_le16(source_pixel) & mask);
uint16_t green_code =
(uint16_t)(fluxer_hdr_read_le16(source_pixel + 2) & mask);
uint16_t blue_code =
(uint16_t)(fluxer_hdr_read_le16(source_pixel + 4) & mask);
uint16_t red_index = fluxer_hdr_lut_index(red_code, bit_depth);
uint16_t green_index = fluxer_hdr_lut_index(green_code, bit_depth);
uint16_t blue_index = fluxer_hdr_lut_index(blue_code, bit_depth);
float red = linear_lut[red_index];
float green = linear_lut[green_index];
float blue = linear_lut[blue_index];
uint16_t maximum_index = red_index;
if (green_index > maximum_index) maximum_index = green_index;
if (blue_index > maximum_index) maximum_index = blue_index;
if (is_hlg) {
float luma = fluxer_hdr_linear_luma(gamut, red, green, blue);
float ootf_scale =
fluxer_hlg_ootf_scale_lut[fluxer_unit_lut_index(luma)];
red *= ootf_scale;
green *= ootf_scale;
blue *= ootf_scale;
float maximum = fmaxf(red, fmaxf(green, blue));
maximum_index = fluxer_unit_lut_index(maximum);
}
float scale = tone_scale_lut[maximum_index];
uint8_t *destination_pixel =
destination_row + (size_t)column * 4u;
fluxer_hdr_pipeline_pixel(
red, green, blue, scale, gamut, destination_pixel);
unsigned int alpha =
(unsigned int)fluxer_hdr_read_le16(source_pixel + 6) & mask;
destination_pixel[3] = (uint8_t)(
(alpha * 255u + (mask >> 1)) / mask);
}
}
return fluxer_native_deadline_status(deadline_monotonic_ms);
}
@@ -0,0 +1,283 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once
#include "vips_shim.h"
#include <assert.h>
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#define FLUXER_HDR_PQ_LUT_SIZE 4096
#define FLUXER_HDR_HLG_LUT_SIZE 4096
#define FLUXER_HDR_SRGB_LUT_SIZE 4096
#define FLUXER_PQ_SDR_TARGET_NORM 0.0203f
#define FLUXER_HLG_REFERENCE_PEAK_NORM 0.1f
enum fluxer_hdr_gamut {
FLUXER_HDR_GAMUT_SRGB = 0,
FLUXER_HDR_GAMUT_BT2020 = 1,
FLUXER_HDR_GAMUT_DISPLAY_P3 = 2,
};
enum fluxer_hdr_transfer {
FLUXER_HDR_TRANSFER_SRGB = 0,
FLUXER_HDR_TRANSFER_BT709 = 1,
FLUXER_HDR_TRANSFER_BT2020_12 = 2,
FLUXER_HDR_TRANSFER_LINEAR = 3,
FLUXER_HDR_TRANSFER_PQ = 4,
FLUXER_HDR_TRANSFER_HLG = 5,
};
extern float fluxer_pq_lut[FLUXER_HDR_PQ_LUT_SIZE];
extern float fluxer_hlg_lut[FLUXER_HDR_HLG_LUT_SIZE];
extern float fluxer_hlg_ootf_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
extern float fluxer_pq_tone_scale_lut[FLUXER_HDR_PQ_LUT_SIZE];
extern float fluxer_hlg_tone_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
extern uint8_t fluxer_srgb_lut[FLUXER_HDR_SRGB_LUT_SIZE];
extern float fluxer_pq_sdr_target_perceptual;
extern float fluxer_hlg_source_peak_perceptual;
extern float fluxer_hlg_sdr_target_perceptual;
int fluxer_hdr_luts_ready(void);
int fluxer_hdr_transfer_is_hdr(int transfer);
int fluxer_hdr_apply_sdr_gamut(uint8_t *rgba, int width, int height, int gamut,
int transfer, int deadline_rows,
long long deadline_monotonic_ms);
int fluxer_hdr_tone_map_rgba16(const uint8_t *source, size_t source_stride,
uint8_t *destination, size_t destination_stride,
int width, int height, int bit_depth, int gamut,
int transfer, int deadline_rows,
long long deadline_monotonic_ms);
static inline uint16_t fluxer_hdr_lut_index(uint16_t code, int bit_depth) {
assert(bit_depth == 10 || bit_depth == 12 || bit_depth == 16);
if (bit_depth == 16) return (uint16_t)(code >> 4);
if (bit_depth == 12) return code & 0x0fffu;
uint16_t code10 = code & 0x03ffu;
return (uint16_t)((code10 << 2) | (code10 >> 8));
}
static inline uint16_t fluxer_unit_lut_index(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return FLUXER_HDR_HLG_LUT_SIZE - 1;
return (uint16_t)(
value * (FLUXER_HDR_HLG_LUT_SIZE - 1) + 0.5f);
}
static inline float fluxer_bt2390_eetf_perceptual(
float encoded,
float max_luminance
) {
if (encoded <= 0.0f) return 0.0f;
if (max_luminance >= 1.0f) {
return encoded > 1.0f ? 1.0f : encoded;
}
float knee = 1.5f * max_luminance - 0.5f;
if (encoded < knee) return encoded;
if (encoded >= 1.0f) return max_luminance;
float position = (encoded - knee) / (1.0f - knee);
float squared = position * position;
float cubed = squared * position;
float start_basis = 2.0f * cubed - 3.0f * squared + 1.0f;
float tangent_basis = cubed - 2.0f * squared + position;
float end_basis = -2.0f * cubed + 3.0f * squared;
float mapped = start_basis * knee + tangent_basis * (1.0f - knee) +
end_basis * max_luminance;
if (mapped > max_luminance) mapped = max_luminance;
if (mapped < 0.0f) mapped = 0.0f;
return mapped;
}
static inline float fluxer_pq_oetf(float luminance) {
if (luminance <= 0.0f) return 0.0f;
if (luminance >= 1.0f) luminance = 1.0f;
const float m1 = 0.1593017578125f;
const float m2 = 78.84375f;
const float c1 = 0.8359375f;
const float c2 = 18.8515625f;
const float c3 = 18.6875f;
float power = powf(luminance, m1);
return powf((c1 + c2 * power) / (1.0f + c3 * power), m2);
}
static inline float fluxer_inverse_pq(float encoded) {
const float m1 = 0.1593017578125f;
const float m2 = 78.84375f;
const float c1 = 0.8359375f;
const float c2 = 18.8515625f;
const float c3 = 18.6875f;
float power = powf(encoded, 1.0f / m2);
float numerator = power - c1;
if (numerator < 0.0f) numerator = 0.0f;
float denominator = c2 - c3 * power;
if (denominator <= 0.0f) return 0.0f;
float luminance = powf(numerator / denominator, 1.0f / m1);
return luminance < 0.0f ? 0.0f : luminance;
}
static inline float fluxer_srgb_oetf(float value) {
if (value <= 0.0f) return 0.0f;
if (value >= 1.0f) return 1.0f;
if (value <= 0.0031308f) return 12.92f * value;
return 1.055f * powf(value, 1.0f / 2.4f) - 0.055f;
}
static inline uint8_t fluxer_quantize8(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return 255;
int quantized = (int)(value * 255.0f + 0.5f);
if (quantized < 0) return 0;
if (quantized > 255) return 255;
return (uint8_t)quantized;
}
static inline uint8_t fluxer_srgb_lut_quantize(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return 255;
size_t index = (size_t)(
value * (FLUXER_HDR_SRGB_LUT_SIZE - 1) + 0.5f);
assert(index < FLUXER_HDR_SRGB_LUT_SIZE);
return fluxer_srgb_lut[index];
}
static inline void fluxer_bt2020_to_bt709_linear(
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
*out_red = 1.6605f * red - 0.5876f * green - 0.0728f * blue;
*out_green = -0.1246f * red + 1.1329f * green - 0.0083f * blue;
*out_blue = -0.0182f * red - 0.1006f * green + 1.1187f * blue;
}
static inline void fluxer_display_p3_to_srgb_linear(
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
*out_red = 1.2249401f * red - 0.2249404f * green;
*out_green = -0.0420569f * red + 1.0420571f * green;
*out_blue = -0.0196376f * red - 0.0786361f * green + 1.0982735f * blue;
}
static inline float fluxer_inverse_srgb(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded <= 0.04045f) return encoded / 12.92f;
return powf((encoded + 0.055f) / 1.055f, 2.4f);
}
static inline float fluxer_inverse_bt709(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded < 0.081f) return encoded / 4.5f;
return powf((encoded + 0.099f) / 1.099f, 1.0f / 0.45f);
}
static inline float fluxer_inverse_bt2020_12(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded < 0.08145f) return encoded / 4.5f;
return powf((encoded + 0.0993f) / 1.0993f, 1.0f / 0.45f);
}
static inline void fluxer_hdr_convert_gamut_linear(
int gamut,
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
if (gamut == FLUXER_HDR_GAMUT_BT2020) {
fluxer_bt2020_to_bt709_linear(
red, green, blue, out_red, out_green, out_blue);
return;
}
if (gamut == FLUXER_HDR_GAMUT_DISPLAY_P3) {
fluxer_display_p3_to_srgb_linear(
red, green, blue, out_red, out_green, out_blue);
return;
}
assert(gamut == FLUXER_HDR_GAMUT_SRGB);
*out_red = red;
*out_green = green;
*out_blue = blue;
}
static inline float fluxer_hdr_linear_luma(
int gamut,
float red,
float green,
float blue
) {
if (gamut == FLUXER_HDR_GAMUT_BT2020) {
return 0.2627f * red + 0.6780f * green + 0.0593f * blue;
}
if (gamut == FLUXER_HDR_GAMUT_DISPLAY_P3) {
return 0.2289746f * red + 0.6917385f * green + 0.0792869f * blue;
}
assert(gamut == FLUXER_HDR_GAMUT_SRGB);
return 0.2126f * red + 0.7152f * green + 0.0722f * blue;
}
static inline float fluxer_hdr_tone_scale(
float maximum,
float target_normalized,
float source_peak_perceptual,
float target_perceptual
) {
if (maximum <= 0.0f) return 0.0f;
assert(source_peak_perceptual > 0.0f);
assert(source_peak_perceptual <= 1.0f);
float perceptual = fluxer_pq_oetf(maximum) /
source_peak_perceptual;
float mapped_perceptual = fluxer_bt2390_eetf_perceptual(
perceptual, target_perceptual);
float mapped = fluxer_inverse_pq(
mapped_perceptual * source_peak_perceptual);
return (mapped / maximum) / target_normalized;
}
static inline void fluxer_hdr_pipeline_pixel(
float red,
float green,
float blue,
float scale,
int gamut,
uint8_t *output
) {
float display_red = red * scale;
float display_green = green * scale;
float display_blue = blue * scale;
if (display_red < 0.0f) display_red = 0.0f;
if (display_green < 0.0f) display_green = 0.0f;
if (display_blue < 0.0f) display_blue = 0.0f;
if (display_red > 1.0f) display_red = 1.0f;
if (display_green > 1.0f) display_green = 1.0f;
if (display_blue > 1.0f) display_blue = 1.0f;
float linear_red = display_red;
float linear_green = display_green;
float linear_blue = display_blue;
fluxer_hdr_convert_gamut_linear(
gamut, display_red, display_green, display_blue,
&linear_red, &linear_green, &linear_blue);
if (linear_red < 0.0f) linear_red = 0.0f;
if (linear_green < 0.0f) linear_green = 0.0f;
if (linear_blue < 0.0f) linear_blue = 0.0f;
if (linear_red > 1.0f) linear_red = 1.0f;
if (linear_green > 1.0f) linear_green = 1.0f;
if (linear_blue > 1.0f) linear_blue = 1.0f;
output[0] = fluxer_srgb_lut_quantize(linear_red);
output[1] = fluxer_srgb_lut_quantize(linear_green);
output[2] = fluxer_srgb_lut_quantize(linear_blue);
}
+47 -484
View File
@@ -2,341 +2,10 @@
#include "native_shim_internal.h"
#define FLUXER_HDR_PQ_LUT_SIZE 4096
#define FLUXER_HDR_HLG_LUT_SIZE 4096
#define FLUXER_HDR_SRGB_LUT_SIZE 4096
#define FLUXER_PQ_SDR_TARGET_NORM 0.0203f
#define FLUXER_HLG_REFERENCE_PEAK_NORM 0.1f
#define FLUXER_HEIF_MAX_AUXILIARY_IMAGES 4096
#define FLUXER_HEIF_DEADLINE_ROWS 64
#define FLUXER_HEIF_ICC_PROFILE_BYTES_MAX ((size_t)4 * 1024 * 1024)
enum fluxer_heif_gamut {
FLUXER_HEIF_GAMUT_SRGB = 0,
FLUXER_HEIF_GAMUT_BT2020 = 1,
FLUXER_HEIF_GAMUT_DISPLAY_P3 = 2,
};
static float fluxer_pq_lut[FLUXER_HDR_PQ_LUT_SIZE];
static float fluxer_hlg_lut[FLUXER_HDR_HLG_LUT_SIZE];
static float fluxer_hlg_ootf_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
static float fluxer_pq_tone_scale_lut[FLUXER_HDR_PQ_LUT_SIZE];
static float fluxer_hlg_tone_scale_lut[FLUXER_HDR_HLG_LUT_SIZE];
static uint8_t fluxer_srgb_lut[FLUXER_HDR_SRGB_LUT_SIZE];
static pthread_once_t fluxer_hdr_lut_once = PTHREAD_ONCE_INIT;
static float fluxer_pq_sdr_target_perceptual;
static float fluxer_hlg_source_peak_perceptual;
static float fluxer_hlg_sdr_target_perceptual;
static inline float fluxer_pq_oetf(float luminance);
static inline float fluxer_hdr_tone_scale(
float maximum,
float target_normalized,
float source_peak_perceptual,
float target_perceptual
);
static inline float fluxer_srgb_oetf(float value);
static inline uint8_t fluxer_quantize8(float value);
static void fluxer_init_hdr_luts(void) {
const double m1 = 0.1593017578125;
const double m2 = 78.84375;
const double c1 = 0.8359375;
const double c2 = 18.8515625;
const double c3 = 18.6875;
for (int index = 0; index < FLUXER_HDR_PQ_LUT_SIZE; index++) {
double encoded = (double)index / (FLUXER_HDR_PQ_LUT_SIZE - 1);
double encoded_power = pow(encoded, 1.0 / m2);
double numerator = encoded_power - c1;
if (numerator < 0.0) numerator = 0.0;
double denominator = c2 - c3 * encoded_power;
double luminance = denominator > 0.0
? pow(numerator / denominator, 1.0 / m1)
: 0.0;
if (luminance < 0.0) luminance = 0.0;
if (luminance > 1.0) luminance = 1.0;
fluxer_pq_lut[index] = (float)luminance;
}
const double a = 0.17883277;
const double b = 0.28466892;
const double c = 0.55991073;
for (int index = 0; index < FLUXER_HDR_HLG_LUT_SIZE; index++) {
double encoded = (double)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
double scene = encoded <= 0.5
? (encoded * encoded) / 3.0
: (exp((encoded - c) / a) + b) / 12.0;
if (scene < 0.0) scene = 0.0;
if (scene > 1.0) scene = 1.0;
fluxer_hlg_lut[index] = (float)scene;
double normalized = (double)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
fluxer_hlg_ootf_scale_lut[index] = normalized > 0.0
? (float)pow(normalized, 0.2)
: 0.0f;
}
fluxer_pq_sdr_target_perceptual =
fluxer_pq_oetf(FLUXER_PQ_SDR_TARGET_NORM);
fluxer_hlg_source_peak_perceptual =
fluxer_pq_oetf(FLUXER_HLG_REFERENCE_PEAK_NORM);
fluxer_hlg_sdr_target_perceptual =
fluxer_pq_sdr_target_perceptual /
fluxer_hlg_source_peak_perceptual;
for (int index = 0; index < FLUXER_HDR_PQ_LUT_SIZE; index++) {
fluxer_pq_tone_scale_lut[index] = fluxer_hdr_tone_scale(
fluxer_pq_lut[index], FLUXER_PQ_SDR_TARGET_NORM,
1.0f,
fluxer_pq_sdr_target_perceptual);
}
for (int index = 0; index < FLUXER_HDR_HLG_LUT_SIZE; index++) {
float maximum = (float)index / (FLUXER_HDR_HLG_LUT_SIZE - 1);
float absolute_maximum =
maximum * FLUXER_HLG_REFERENCE_PEAK_NORM;
fluxer_hlg_tone_scale_lut[index] =
FLUXER_HLG_REFERENCE_PEAK_NORM * fluxer_hdr_tone_scale(
absolute_maximum, FLUXER_PQ_SDR_TARGET_NORM,
fluxer_hlg_source_peak_perceptual,
fluxer_hlg_sdr_target_perceptual);
}
for (int index = 0; index < FLUXER_HDR_SRGB_LUT_SIZE; index++) {
float linear = (float)index / (FLUXER_HDR_SRGB_LUT_SIZE - 1);
fluxer_srgb_lut[index] = fluxer_quantize8(
fluxer_srgb_oetf(linear));
}
}
static inline uint16_t fluxer_hdr_lut_index(uint16_t code, int bit_depth) {
assert(bit_depth == 10 || bit_depth == 12);
if (bit_depth == 12) return code & 0x0fffu;
uint16_t code10 = code & 0x03ffu;
return (uint16_t)((code10 << 2) | (code10 >> 8));
}
static inline uint16_t fluxer_unit_lut_index(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return FLUXER_HDR_HLG_LUT_SIZE - 1;
return (uint16_t)(
value * (FLUXER_HDR_HLG_LUT_SIZE - 1) + 0.5f);
}
static inline uint16_t fluxer_heif_read_le16(const uint8_t *value) {
return (uint16_t)((uint16_t)value[0] | ((uint16_t)value[1] << 8));
}
static inline float fluxer_bt2390_eetf_perceptual(
float encoded,
float max_luminance
) {
if (encoded <= 0.0f) return 0.0f;
if (max_luminance >= 1.0f) {
return encoded > 1.0f ? 1.0f : encoded;
}
float knee = 1.5f * max_luminance - 0.5f;
if (encoded < knee) return encoded;
if (encoded >= 1.0f) return max_luminance;
float position = (encoded - knee) / (1.0f - knee);
float squared = position * position;
float cubed = squared * position;
float start_basis = 2.0f * cubed - 3.0f * squared + 1.0f;
float tangent_basis = cubed - 2.0f * squared + position;
float end_basis = -2.0f * cubed + 3.0f * squared;
float mapped = start_basis * knee + tangent_basis * (1.0f - knee) +
end_basis * max_luminance;
if (mapped > max_luminance) mapped = max_luminance;
if (mapped < 0.0f) mapped = 0.0f;
return mapped;
}
static inline float fluxer_pq_oetf(float luminance) {
if (luminance <= 0.0f) return 0.0f;
if (luminance >= 1.0f) luminance = 1.0f;
const float m1 = 0.1593017578125f;
const float m2 = 78.84375f;
const float c1 = 0.8359375f;
const float c2 = 18.8515625f;
const float c3 = 18.6875f;
float power = powf(luminance, m1);
return powf((c1 + c2 * power) / (1.0f + c3 * power), m2);
}
static inline float fluxer_srgb_oetf(float value) {
if (value <= 0.0f) return 0.0f;
if (value >= 1.0f) return 1.0f;
if (value <= 0.0031308f) return 12.92f * value;
return 1.055f * powf(value, 1.0f / 2.4f) - 0.055f;
}
static inline uint8_t fluxer_quantize8(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return 255;
int quantized = (int)(value * 255.0f + 0.5f);
if (quantized < 0) return 0;
if (quantized > 255) return 255;
return (uint8_t)quantized;
}
static inline uint8_t fluxer_srgb_lut_quantize(float value) {
if (value <= 0.0f) return 0;
if (value >= 1.0f) return 255;
size_t index = (size_t)(
value * (FLUXER_HDR_SRGB_LUT_SIZE - 1) + 0.5f);
assert(index < FLUXER_HDR_SRGB_LUT_SIZE);
return fluxer_srgb_lut[index];
}
static inline void fluxer_bt2020_to_bt709_linear(
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
*out_red = 1.6605f * red - 0.5876f * green - 0.0728f * blue;
*out_green = -0.1246f * red + 1.1329f * green - 0.0083f * blue;
*out_blue = -0.0182f * red - 0.1006f * green + 1.1187f * blue;
}
static inline void fluxer_display_p3_to_srgb_linear(
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
*out_red = 1.2249401f * red - 0.2249404f * green;
*out_green = -0.0420569f * red + 1.0420571f * green;
*out_blue = -0.0196376f * red - 0.0786361f * green + 1.0982735f * blue;
}
static inline float fluxer_inverse_srgb(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded <= 0.04045f) return encoded / 12.92f;
return powf((encoded + 0.055f) / 1.055f, 2.4f);
}
static inline float fluxer_inverse_bt709(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded < 0.081f) return encoded / 4.5f;
return powf((encoded + 0.099f) / 1.099f, 1.0f / 0.45f);
}
static inline float fluxer_inverse_bt2020_12(float encoded) {
if (encoded <= 0.0f) return 0.0f;
if (encoded >= 1.0f) return 1.0f;
if (encoded < 0.08145f) return encoded / 4.5f;
return powf((encoded + 0.0993f) / 1.0993f, 1.0f / 0.45f);
}
static inline void fluxer_heif_convert_gamut_linear(
int gamut,
float red,
float green,
float blue,
float *out_red,
float *out_green,
float *out_blue
) {
if (gamut == FLUXER_HEIF_GAMUT_BT2020) {
fluxer_bt2020_to_bt709_linear(
red, green, blue, out_red, out_green, out_blue);
return;
}
if (gamut == FLUXER_HEIF_GAMUT_DISPLAY_P3) {
fluxer_display_p3_to_srgb_linear(
red, green, blue, out_red, out_green, out_blue);
return;
}
assert(gamut == FLUXER_HEIF_GAMUT_SRGB);
*out_red = red;
*out_green = green;
*out_blue = blue;
}
static inline float fluxer_heif_linear_luma(
int gamut,
float red,
float green,
float blue
) {
if (gamut == FLUXER_HEIF_GAMUT_BT2020) {
return 0.2627f * red + 0.6780f * green + 0.0593f * blue;
}
if (gamut == FLUXER_HEIF_GAMUT_DISPLAY_P3) {
return 0.2289746f * red + 0.6917385f * green + 0.0792869f * blue;
}
assert(gamut == FLUXER_HEIF_GAMUT_SRGB);
return 0.2126f * red + 0.7152f * green + 0.0722f * blue;
}
static inline float fluxer_inverse_pq(float encoded) {
const float m1 = 0.1593017578125f;
const float m2 = 78.84375f;
const float c1 = 0.8359375f;
const float c2 = 18.8515625f;
const float c3 = 18.6875f;
float power = powf(encoded, 1.0f / m2);
float numerator = power - c1;
if (numerator < 0.0f) numerator = 0.0f;
float denominator = c2 - c3 * power;
if (denominator <= 0.0f) return 0.0f;
float luminance = powf(numerator / denominator, 1.0f / m1);
return luminance < 0.0f ? 0.0f : luminance;
}
static inline float fluxer_hdr_tone_scale(
float maximum,
float target_normalized,
float source_peak_perceptual,
float target_perceptual
) {
if (maximum <= 0.0f) return 0.0f;
assert(source_peak_perceptual > 0.0f);
assert(source_peak_perceptual <= 1.0f);
float perceptual = fluxer_pq_oetf(maximum) /
source_peak_perceptual;
float mapped_perceptual = fluxer_bt2390_eetf_perceptual(
perceptual, target_perceptual);
float mapped = fluxer_inverse_pq(
mapped_perceptual * source_peak_perceptual);
return (mapped / maximum) / target_normalized;
}
static inline void fluxer_hdr_pipeline_pixel(
float red,
float green,
float blue,
float scale,
int gamut,
uint8_t *output
) {
float display_red = red * scale;
float display_green = green * scale;
float display_blue = blue * scale;
if (display_red < 0.0f) display_red = 0.0f;
if (display_green < 0.0f) display_green = 0.0f;
if (display_blue < 0.0f) display_blue = 0.0f;
if (display_red > 1.0f) display_red = 1.0f;
if (display_green > 1.0f) display_green = 1.0f;
if (display_blue > 1.0f) display_blue = 1.0f;
float linear_red = display_red;
float linear_green = display_green;
float linear_blue = display_blue;
fluxer_heif_convert_gamut_linear(
gamut, display_red, display_green, display_blue,
&linear_red, &linear_green, &linear_blue);
if (linear_red < 0.0f) linear_red = 0.0f;
if (linear_green < 0.0f) linear_green = 0.0f;
if (linear_blue < 0.0f) linear_blue = 0.0f;
if (linear_red > 1.0f) linear_red = 1.0f;
if (linear_green > 1.0f) linear_green = 1.0f;
if (linear_blue > 1.0f) linear_blue = 1.0f;
output[0] = fluxer_srgb_lut_quantize(linear_red);
output[1] = fluxer_srgb_lut_quantize(linear_green);
output[2] = fluxer_srgb_lut_quantize(linear_blue);
}
static unsigned char fluxer_ascii_lower(unsigned char value) {
if (value >= 'A' && value <= 'Z') {
return (unsigned char)(value + ('a' - 'A'));
@@ -625,13 +294,35 @@ static int fluxer_heif_nclx_gamut(int primaries, int *out_gamut) {
assert(out_gamut != NULL);
switch (primaries) {
case heif_color_primaries_ITU_R_BT_709_5:
*out_gamut = FLUXER_HEIF_GAMUT_SRGB;
*out_gamut = FLUXER_HDR_GAMUT_SRGB;
return FLUXER_NATIVE_STATUS_OK;
case heif_color_primaries_ITU_R_BT_2020_2_and_2100_0:
*out_gamut = FLUXER_HEIF_GAMUT_BT2020;
*out_gamut = FLUXER_HDR_GAMUT_BT2020;
return FLUXER_NATIVE_STATUS_OK;
case heif_color_primaries_SMPTE_EG_432_1:
*out_gamut = FLUXER_HEIF_GAMUT_DISPLAY_P3;
*out_gamut = FLUXER_HDR_GAMUT_DISPLAY_P3;
return FLUXER_NATIVE_STATUS_OK;
default:
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
}
static int fluxer_heif_nclx_transfer(int transfer, int *out_transfer) {
assert(out_transfer != NULL);
switch (transfer) {
case heif_transfer_characteristic_IEC_61966_2_1:
*out_transfer = FLUXER_HDR_TRANSFER_SRGB;
return FLUXER_NATIVE_STATUS_OK;
case heif_transfer_characteristic_ITU_R_BT_709_5:
case heif_transfer_characteristic_ITU_R_BT_601_6:
case heif_transfer_characteristic_ITU_R_BT_2020_2_10bit:
*out_transfer = FLUXER_HDR_TRANSFER_BT709;
return FLUXER_NATIVE_STATUS_OK;
case heif_transfer_characteristic_ITU_R_BT_2020_2_12bit:
*out_transfer = FLUXER_HDR_TRANSFER_BT2020_12;
return FLUXER_NATIVE_STATUS_OK;
case heif_transfer_characteristic_linear:
*out_transfer = FLUXER_HDR_TRANSFER_LINEAR;
return FLUXER_NATIVE_STATUS_OK;
default:
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
@@ -647,60 +338,15 @@ static int fluxer_heif_apply_sdr_nclx(
) {
assert(destination != NULL);
assert(profile != NULL);
int gamut = FLUXER_HEIF_GAMUT_SRGB;
int gamut = FLUXER_HDR_GAMUT_SRGB;
int status = fluxer_heif_nclx_gamut(profile->primaries, &gamut);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
int transfer = profile->transfer;
int is_srgb = transfer == heif_transfer_characteristic_IEC_61966_2_1;
int is_bt709 = transfer == heif_transfer_characteristic_ITU_R_BT_709_5 ||
transfer == heif_transfer_characteristic_ITU_R_BT_601_6 ||
transfer == heif_transfer_characteristic_ITU_R_BT_2020_2_10bit;
int is_bt2020_12 =
transfer == heif_transfer_characteristic_ITU_R_BT_2020_2_12bit;
int is_linear = transfer == heif_transfer_characteristic_linear;
if (!is_srgb && !is_bt709 && !is_bt2020_12 && !is_linear) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
if (is_srgb && gamut == FLUXER_HEIF_GAMUT_SRGB) {
return FLUXER_NATIVE_STATUS_OK;
}
size_t row_bytes = (size_t)width * 4u;
for (int row = 0; row < height; row++) {
if (row % FLUXER_HEIF_DEADLINE_ROWS == 0) {
status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
}
uint8_t *row_data = destination + (size_t)row * row_bytes;
for (int column = 0; column < width; column++) {
uint8_t *pixel = row_data + (size_t)column * 4u;
float red = (float)pixel[0] / 255.0f;
float green = (float)pixel[1] / 255.0f;
float blue = (float)pixel[2] / 255.0f;
if (is_srgb) {
red = fluxer_inverse_srgb(red);
green = fluxer_inverse_srgb(green);
blue = fluxer_inverse_srgb(blue);
} else if (is_bt709) {
red = fluxer_inverse_bt709(red);
green = fluxer_inverse_bt709(green);
blue = fluxer_inverse_bt709(blue);
} else if (is_bt2020_12) {
red = fluxer_inverse_bt2020_12(red);
green = fluxer_inverse_bt2020_12(green);
blue = fluxer_inverse_bt2020_12(blue);
}
float srgb_red = 0.0f;
float srgb_green = 0.0f;
float srgb_blue = 0.0f;
fluxer_heif_convert_gamut_linear(
gamut, red, green, blue,
&srgb_red, &srgb_green, &srgb_blue);
pixel[0] = fluxer_quantize8(fluxer_srgb_oetf(srgb_red));
pixel[1] = fluxer_quantize8(fluxer_srgb_oetf(srgb_green));
pixel[2] = fluxer_quantize8(fluxer_srgb_oetf(srgb_blue));
}
}
return fluxer_native_deadline_status(deadline_monotonic_ms);
int transfer = FLUXER_HDR_TRANSFER_SRGB;
status = fluxer_heif_nclx_transfer(profile->transfer, &transfer);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
return fluxer_hdr_apply_sdr_gamut(
destination, width, height, gamut, transfer,
FLUXER_HEIF_DEADLINE_ROWS, deadline_monotonic_ms);
}
static int fluxer_heif_cancel_decoding(void *opaque) {
@@ -860,67 +506,6 @@ static int fluxer_heif_decode_sdr(
return status;
}
struct fluxer_heif_hdr_transform {
int bit_depth;
int mask;
int gamut;
int is_hlg;
const float *linear_lut;
const float *tone_scale_lut;
};
static void fluxer_heif_transform_hdr_row(
const uint8_t *source,
uint8_t *destination,
int width,
const struct fluxer_heif_hdr_transform *transform
) {
assert(source != NULL);
assert(destination != NULL);
assert(transform != NULL);
for (int column = 0; column < width; column++) {
const uint8_t *source_pixel = source + (size_t)column * 8u;
uint16_t red_code =
fluxer_heif_read_le16(source_pixel) & transform->mask;
uint16_t green_code =
fluxer_heif_read_le16(source_pixel + 2) & transform->mask;
uint16_t blue_code =
fluxer_heif_read_le16(source_pixel + 4) & transform->mask;
uint16_t red_index = fluxer_hdr_lut_index(
red_code, transform->bit_depth);
uint16_t green_index = fluxer_hdr_lut_index(
green_code, transform->bit_depth);
uint16_t blue_index = fluxer_hdr_lut_index(
blue_code, transform->bit_depth);
float red = transform->linear_lut[red_index];
float green = transform->linear_lut[green_index];
float blue = transform->linear_lut[blue_index];
uint16_t maximum_index = red_index;
if (green_index > maximum_index) maximum_index = green_index;
if (blue_index > maximum_index) maximum_index = blue_index;
if (transform->is_hlg) {
float luma = fluxer_heif_linear_luma(
transform->gamut, red, green, blue);
float ootf_scale =
fluxer_hlg_ootf_scale_lut[fluxer_unit_lut_index(luma)];
red *= ootf_scale;
green *= ootf_scale;
blue *= ootf_scale;
float maximum = fmaxf(red, fmaxf(green, blue));
maximum_index = fluxer_unit_lut_index(maximum);
}
float scale = transform->tone_scale_lut[maximum_index];
uint8_t *destination_pixel = destination + (size_t)column * 4u;
fluxer_hdr_pipeline_pixel(
red, green, blue, scale, transform->gamut,
destination_pixel);
uint16_t alpha =
fluxer_heif_read_le16(source_pixel + 6) & transform->mask;
destination_pixel[3] = (uint8_t)(
(alpha * 255 + (transform->mask >> 1)) / transform->mask);
}
}
static int fluxer_heif_decode_hdr(
struct heif_image_handle *handle,
uint8_t *destination,
@@ -931,11 +516,6 @@ static int fluxer_heif_decode_hdr(
) {
int status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
if (pthread_once(&fluxer_hdr_lut_once, fluxer_init_hdr_luts) != 0) {
return FLUXER_NATIVE_STATUS_CODEC_FAILURE;
}
status = fluxer_native_deadline_status(deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) return status;
int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);
if (bit_depth != 10 && bit_depth != 12) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
@@ -943,10 +523,19 @@ static int fluxer_heif_decode_hdr(
if (!profile->nclx_present) {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
int gamut = FLUXER_HEIF_GAMUT_SRGB;
int transfer;
if (profile->transfer == heif_transfer_characteristic_ITU_R_BT_2100_0_PQ) {
transfer = FLUXER_HDR_TRANSFER_PQ;
} else if (profile->transfer ==
heif_transfer_characteristic_ITU_R_BT_2100_0_HLG) {
transfer = FLUXER_HDR_TRANSFER_HLG;
} else {
return FLUXER_NATIVE_STATUS_UNSUPPORTED;
}
int gamut = FLUXER_HDR_GAMUT_SRGB;
if (fluxer_heif_nclx_gamut(profile->primaries, &gamut) !=
FLUXER_NATIVE_STATUS_OK) {
gamut = FLUXER_HEIF_GAMUT_SRGB;
gamut = FLUXER_HDR_GAMUT_SRGB;
}
if ((size_t)width > SIZE_MAX / 8u) {
return FLUXER_NATIVE_STATUS_INVALID_DIMENSIONS;
@@ -961,37 +550,11 @@ static int fluxer_heif_decode_hdr(
status = fluxer_heif_interleaved_plane(
image, width, height, heif_chroma_interleaved_RRGGBBAA_LE,
64, bit_depth, (size_t)width * 8u, &plane, &stride);
struct fluxer_heif_hdr_transform transform = {
.bit_depth = bit_depth,
.mask = (1 << bit_depth) - 1,
.gamut = gamut,
.is_hlg =
profile->transfer == heif_transfer_characteristic_ITU_R_BT_2100_0_HLG,
.linear_lut =
profile->transfer == heif_transfer_characteristic_ITU_R_BT_2100_0_PQ
? fluxer_pq_lut
: fluxer_hlg_lut,
.tone_scale_lut =
profile->transfer == heif_transfer_characteristic_ITU_R_BT_2100_0_PQ
? fluxer_pq_tone_scale_lut
: fluxer_hlg_tone_scale_lut,
};
if (status == FLUXER_NATIVE_STATUS_OK) {
size_t destination_stride = (size_t)width * 4u;
for (int row = 0; row < height; row++) {
if (row % FLUXER_HEIF_DEADLINE_ROWS == 0) {
status = fluxer_native_deadline_status(
deadline_monotonic_ms);
if (status != FLUXER_NATIVE_STATUS_OK) break;
}
fluxer_heif_transform_hdr_row(
plane + (size_t)row * (size_t)stride,
destination + (size_t)row * destination_stride,
width, &transform);
}
if (status == FLUXER_NATIVE_STATUS_OK) {
status = fluxer_native_deadline_status(deadline_monotonic_ms);
}
status = fluxer_hdr_tone_map_rgba16(
plane, (size_t)stride, destination, (size_t)width * 4u,
width, height, bit_depth, gamut, transfer,
FLUXER_HEIF_DEADLINE_ROWS, deadline_monotonic_ms);
}
heif_image_release(image);
return status;
@@ -6,6 +6,7 @@
#define _DARWIN_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "hdr_color.h"
#include "vips_shim.h"
#include "webp_animation.h"