|
use image::codecs::hdr::HdrDecoder; |
|
use image::{DynamicImage, Rgb32FImage}; |
|
use itertools::Itertools; |
|
use libc::c_char; |
|
use libheif_sys::{ |
|
heif_channel_heif_channel_interleaved, heif_chroma_heif_chroma_interleaved_RRGGBB_BE, |
|
heif_color_primaries_heif_color_primaries_ITU_R_BT_2020_2_and_2100_0, |
|
heif_colorspace_heif_colorspace_RGB, heif_compression_format_heif_compression_HEVC, |
|
heif_content_light_level, heif_context_alloc, heif_context_encode_image, |
|
heif_context_get_encoder, heif_context_write_to_file, heif_encoder, heif_encoder_descriptor, |
|
heif_encoder_set_lossy_quality, heif_error, heif_get_encoder_descriptors, heif_image, |
|
heif_image_add_plane, heif_image_create, heif_image_get_plane, heif_image_handle, |
|
heif_image_set_content_light_level, heif_image_set_nclx_color_profile, |
|
heif_matrix_coefficients_heif_matrix_coefficients_unspecified, heif_nclx_color_profile_alloc, |
|
heif_transfer_characteristics_heif_transfer_characteristic_ITU_R_BT_2100_0_PQ, |
|
}; |
|
|
|
use std::ffi::c_int; |
|
use std::path::{Path, PathBuf}; |
|
use std::ptr::{null, null_mut}; |
|
use clap::Parser; |
|
|
|
fn check(value: heif_error) -> anyhow::Result<()> { |
|
if value.code == 0 { |
|
Ok(()) |
|
} else { |
|
Err(anyhow::anyhow!("Error: {:?}", value)) |
|
} |
|
} |
|
|
|
fn check_alloc<T>(value: *mut T) -> anyhow::Result<*mut T> { |
|
if value.is_null() { |
|
Err(anyhow::anyhow!("Allocation failed")) |
|
} else { |
|
Ok(value) |
|
} |
|
} |
|
|
|
fn read_source(path: &Path) -> anyhow::Result<Rgb32FImage> { |
|
let mut reader = std::io::BufReader::new(std::fs::File::open(path)?); |
|
|
|
let decoder = HdrDecoder::new(&mut reader)?; |
|
let image = DynamicImage::from_decoder(decoder)?; |
|
let image = match image { |
|
DynamicImage::ImageRgb32F(image) => image, |
|
_ => { |
|
return Err(anyhow::anyhow!("Unsupported image format")); |
|
} |
|
}; |
|
|
|
Ok(image) |
|
} |
|
|
|
// Constants for PQ (SMPTE ST 2084) |
|
const M1: f32 = 2610.0 / 16384.0; |
|
const M2: f32 = 2523.0 / 32.0; |
|
const C1: f32 = 3424.0 / 4096.0; |
|
const C2: f32 = 2413.0 / 128.0; |
|
const C3: f32 = 2392.0 / 128.0; |
|
|
|
// Linear RGB to BT.2020 |
|
fn linear_rgb_to_bt2020(r: f32, g: f32, b: f32) -> (f32, f32, f32) { |
|
let r_bt2020 = 0.627402 * r + 0.329292 * g + 0.043306 * b; |
|
let g_bt2020 = 0.069095 * r + 0.919544 * g + 0.011361 * b; |
|
let b_bt2020 = 0.016394 * r + 0.088028 * g + 0.895578 * b; |
|
(r_bt2020, g_bt2020, b_bt2020) |
|
} |
|
|
|
// Apply PQ transfer function |
|
fn apply_pq(value: f32) -> f32 { |
|
let y = value.powf(M1); |
|
((C1 + C2 * y) / (1.0 + C3 * y)).powf(M2) |
|
} |
|
|
|
// Convert HDR pixel to 10-bit PQ |
|
fn hdr_to_pq_10bit(r: f32, g: f32, b: f32, max: f32) -> (u16, u16, u16) { |
|
let r_norm = (r / max).min(1.0); |
|
let g_norm = (g / max).min(1.0); |
|
let b_norm = (b / max).min(1.0); |
|
|
|
// Convert to BT.2020 |
|
let (r_bt2020, g_bt2020, b_bt2020) = linear_rgb_to_bt2020(r_norm, g_norm, b_norm); |
|
|
|
// Apply PQ and scale to 10-bit range |
|
let r_pq = (apply_pq(r_bt2020) * 1023.0).round() as u16; |
|
let g_pq = (apply_pq(g_bt2020) * 1023.0).round() as u16; |
|
let b_pq = (apply_pq(b_bt2020) * 1023.0).round() as u16; |
|
|
|
// Clamp values to 10-bit range |
|
(r_pq.min(1023), g_pq.min(1023), b_pq.min(1023)) |
|
} |
|
|
|
fn encode_f32_to_u10be(source: u16) -> (u8, u8) { |
|
let byte1 = ((source >> 8) & 0xFF) as u8; |
|
let byte2 = (source & 0xFF) as u8; |
|
(byte1, byte2) |
|
} |
|
|
|
unsafe fn encode_output(source: Rgb32FImage, cll: u16, path: &Path) -> anyhow::Result<()> { |
|
let mut image: *mut heif_image = null_mut(); |
|
check(heif_image_create( |
|
source.width() as c_int, |
|
source.height() as c_int, |
|
heif_colorspace_heif_colorspace_RGB, |
|
heif_chroma_heif_chroma_interleaved_RRGGBB_BE, |
|
&raw mut image, |
|
))?; |
|
|
|
let profile = check_alloc(heif_nclx_color_profile_alloc())?; |
|
(*profile).color_primaries = |
|
heif_color_primaries_heif_color_primaries_ITU_R_BT_2020_2_and_2100_0; |
|
(*profile).transfer_characteristics = |
|
heif_transfer_characteristics_heif_transfer_characteristic_ITU_R_BT_2100_0_PQ; |
|
(*profile).matrix_coefficients = heif_matrix_coefficients_heif_matrix_coefficients_unspecified; |
|
(*profile).full_range_flag = 1; |
|
check(heif_image_set_nclx_color_profile(image, profile))?; |
|
|
|
let light_level = heif_content_light_level { |
|
max_content_light_level: cll, |
|
max_pic_average_light_level: 0, |
|
}; |
|
|
|
heif_image_set_content_light_level(image, &light_level); |
|
|
|
check(heif_image_add_plane( |
|
image, |
|
heif_channel_heif_channel_interleaved, |
|
source.width() as c_int, |
|
source.height() as c_int, |
|
10, |
|
))?; |
|
|
|
let mut stride_raw: c_int = 0; |
|
let plane_data_raw = check_alloc(heif_image_get_plane( |
|
image, |
|
heif_channel_heif_channel_interleaved, |
|
&raw mut stride_raw, |
|
))?; |
|
|
|
let byte_length = (stride_raw * (source.height() as c_int)) as usize; |
|
eprintln!("stride_raw: {}, byte_length: {}", stride_raw, byte_length); |
|
|
|
let plane_data = std::slice::from_raw_parts_mut(plane_data_raw, byte_length); |
|
|
|
let source_max = source.iter().cloned().reduce(f32::max).unwrap() * 8f32; |
|
let source_min = source.iter().cloned().reduce(f32::min).unwrap(); |
|
|
|
eprintln!("source_max: {}, source_min: {}", source_max, source_min); |
|
|
|
plane_data |
|
.iter_mut() |
|
.enumerate() |
|
.chunks(stride_raw as usize) |
|
.into_iter() |
|
.for_each(|stride| { |
|
stride |
|
.chunks(6) |
|
.into_iter() |
|
.for_each(|chunk| { |
|
let mut chunk = chunk.collect::<Vec<_>>(); |
|
if chunk.len() != 6 { |
|
return; |
|
} |
|
|
|
let byte_offset = chunk[0].0; |
|
|
|
let x = (byte_offset % stride_raw as usize) / 6; |
|
let y = byte_offset / stride_raw as usize; |
|
|
|
if x >= source.width() as usize || y >= source.height() as usize { |
|
return; |
|
} |
|
|
|
let source_pixel = source.get_pixel(x as u32, y as u32); |
|
let (source_r, source_g, source_b) = hdr_to_pq_10bit( |
|
source_pixel.0[0], |
|
source_pixel.0[1], |
|
source_pixel.0[2], |
|
source_max, |
|
); |
|
|
|
let (r0, r1) = encode_f32_to_u10be(source_r); |
|
let (g0, g1) = encode_f32_to_u10be(source_g); |
|
let (b0, b1) = encode_f32_to_u10be(source_b); |
|
|
|
*(chunk[0].1) = r0; |
|
*(chunk[1].1) = r1; |
|
|
|
*(chunk[2].1) = g0; |
|
*(chunk[3].1) = g1; |
|
|
|
*(chunk[4].1) = b0; |
|
*(chunk[5].1) = b1; |
|
}); |
|
}); |
|
|
|
let context = check_alloc(heif_context_alloc())?; |
|
|
|
let mut encoder_descriptor: *const heif_encoder_descriptor = null_mut(); |
|
let n_encoder_descriptor = heif_get_encoder_descriptors( |
|
heif_compression_format_heif_compression_HEVC, |
|
null(), |
|
&raw mut encoder_descriptor, |
|
c_int::MAX, |
|
); |
|
|
|
if n_encoder_descriptor < 1 { |
|
return Err(anyhow::anyhow!("No encoders found")); |
|
} |
|
|
|
let mut encoder: *mut heif_encoder = null_mut(); |
|
check(heif_context_get_encoder( |
|
context, |
|
encoder_descriptor, |
|
&raw mut encoder, |
|
))?; |
|
check(heif_encoder_set_lossy_quality(encoder, 90))?; |
|
|
|
let mut image_handle: *mut heif_image_handle = null_mut(); |
|
check(heif_context_encode_image( |
|
context, |
|
image, |
|
encoder, |
|
null_mut(), |
|
&raw mut image_handle, |
|
))?; |
|
|
|
check(heif_context_write_to_file( |
|
context, |
|
path.display().to_string().as_ptr() as *const c_char, |
|
))?; |
|
|
|
Ok(()) |
|
} |
|
|
|
#[derive(Parser, Debug)] |
|
#[command()] |
|
struct Args { |
|
#[arg()] |
|
input: PathBuf, |
|
|
|
#[arg()] |
|
output: Option<PathBuf>, |
|
|
|
#[arg(long)] |
|
cll: Option<u16>, |
|
} |
|
|
|
fn main() -> anyhow::Result<()> { |
|
let args = Args::parse(); |
|
let source = read_source(&args.input)?; |
|
let output = args.input.with_extension("heic"); |
|
|
|
unsafe { |
|
encode_output(source, args.cll.unwrap_or(0), &output)?; |
|
} |
|
|
|
Ok(()) |
|
} |