Skip to content

Commit 1aee261

Browse files
authored
Merge pull request #13 from awxkee/dev
Maintenance update
2 parents 17a55f6 + e6a1cdb commit 1aee261

File tree

9 files changed

+236
-195
lines changed

9 files changed

+236
-195
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ workspace = { members = ["app"] }
22

33
[package]
44
name = "gainforge"
5-
version = "0.3.4"
5+
version = "0.3.5"
66
edition = "2021"
77
description = "HDR tonemapping library"
88
readme = "README.md"
@@ -20,7 +20,7 @@ rust-version = "1.82.0"
2020
moxcms = "0.7"
2121
num-traits = "0.2"
2222
pxfm = "^0.1.1"
23-
quick-xml = { version = "0.37", features = ["serde", "serde-types", "serialize"], optional = true }
23+
quick-xml = { version = "0.38", features = ["serde", "serde-types", "serialize"], optional = true }
2424
serde = { version = "^1.0.219", features = ["derive"], optional = true }
2525

2626
[features]

app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
image = { version = "0.25.5", features = ["avif-native"] }
7+
image = { version = "0.25", default-features = false, features = ["avif-native", "png", "jpeg"] }
88
gainforge = { path = "../", features = ["uhdr"] }
99
num-traits = "0.2.19"
1010
moxcms = "0.7"

app/src/main.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,21 @@ fn main() {
186186
// saturation: Rgb::new(1.4, 1.4, 1.4),
187187
// offset: Rgb::default(),
188188
// })),
189-
// ToneMappingMethod::Rec2408(GainHdrMetadata::new(2000f32, 203.)),
190-
ToneMappingMethod::Filmic,
191-
// MappingColorSpace::Rgb(RgbToneMapperParameters {
192-
// gamut_clipping: GamutClipping::NoClip,
189+
ToneMappingMethod::Rec2408(GainHdrMetadata::new(2000f32, 203.)),
190+
// ToneMappingMethod::Filmic,
191+
MappingColorSpace::Rgb(RgbToneMapperParameters {
192+
gamut_clipping: GamutClipping::NoClip,
193+
exposure: 1f32,
194+
}),
195+
// MappingColorSpace::YRgb(CommonToneMapperParameters {
193196
// exposure: 1f32,
197+
// gamut_clipping: GamutClipping::NoClip,
194198
// }),
195-
// MappingColorSpace::YRgb(CommonToneMapperParameters {
199+
// MappingColorSpace::Jzazbz(JzazbzToneMapperParameters {
200+
// content_brightness: 2000.,
196201
// exposure: 1f32,
197202
// gamut_clipping: GamutClipping::NoClip,
198203
// }),
199-
MappingColorSpace::Jzazbz(JzazbzToneMapperParameters {
200-
content_brightness: 2000.,
201-
exposure: 1f32,
202-
gamut_clipping: GamutClipping::NoClip,
203-
}),
204204
)
205205
.unwrap();
206206
let dims = rgb.dimensions();

rust-toolchain.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[toolchain]
2+
channel = "stable"

src/gamma.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#![allow(clippy::excessive_precision)]
3030

3131
use moxcms::TransferCharacteristics;
32+
use pxfm::{f_expf, f_logf, f_powf};
3233

3334
#[inline(always)]
3435
/// Linear transfer function for sRGB
@@ -106,11 +107,11 @@ pub(crate) fn bt1361_from_linear(linear: f32) -> f32 {
106107
if linear < -0.25 {
107108
-0.25
108109
} else if linear < 0.0 {
109-
-0.27482420670236 * f32::powf(-4.0 * linear, 0.45) + 0.02482420670236
110+
-0.27482420670236 * f_powf(-4.0 * linear, 0.45) + 0.02482420670236
110111
} else if linear < 0.018053968510807 {
111112
linear * 4.5
112113
} else if linear < 1.0 {
113-
1.09929682680944 * f32::powf(linear, 0.45) - 0.09929682680944
114+
1.09929682680944 * f_powf(linear, 0.45) - 0.09929682680944
114115
} else {
115116
1.0
116117
}
@@ -122,11 +123,11 @@ pub(crate) fn bt1361_to_linear(gamma: f32) -> f32 {
122123
if gamma < -0.25 {
123124
-0.25
124125
} else if gamma < 0.0 {
125-
f32::powf((gamma - 0.02482420670236) / -0.27482420670236, 1.0 / 0.45) / -4.0
126+
f_powf((gamma - 0.02482420670236) / -0.27482420670236, 1.0 / 0.45) / -4.0
126127
} else if gamma < 4.5 * 0.018053968510807 {
127128
gamma / 4.5
128129
} else if gamma < 1.0 {
129-
f32::powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45)
130+
f_powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45)
130131
} else {
131132
1.0
132133
}
@@ -140,7 +141,7 @@ pub(crate) fn pure_gamma_function(x: f32, gamma: f32) -> f32 {
140141
} else if x >= 1. {
141142
1.
142143
} else {
143-
x.powf(gamma)
144+
f_powf(x, gamma)
144145
}
145146
}
146147

@@ -172,10 +173,10 @@ pub(crate) fn gamma2p8_to_linear(gamma: f32) -> f32 {
172173
/// Linear transfer function for PQ
173174
pub(crate) fn pq_to_linear(gamma: f32) -> f32 {
174175
if gamma > 0.0 {
175-
let pow_gamma = f32::powf(gamma, 1.0 / 78.84375);
176+
let pow_gamma = f_powf(gamma, 1.0 / 78.84375);
176177
let num = (pow_gamma - 0.8359375).max(0.);
177178
let den = (18.8515625 - 18.6875 * pow_gamma).max(f32::MIN);
178-
let linear = f32::powf(num / den, 1.0 / 0.1593017578125);
179+
let linear = f_powf(num / den, 1.0 / 0.1593017578125);
179180
// Scale so that SDR white is 1.0 (extended SDR).
180181
const PQ_MAX_NITS: f32 = 10000.;
181182
const SDR_WHITE_NITS: f32 = 203.;
@@ -195,10 +196,10 @@ pub(crate) fn pq_from_linear(linear: f32) -> f32 {
195196
if linear > 0.0 {
196197
// Scale from extended SDR range to [0.0, 1.0].
197198
let linear = (linear * SDR_REFERENCE_DISPLAY / PQ_MAX_NITS).clamp(0., 1.);
198-
let pow_linear = f32::powf(linear, 0.1593017578125);
199+
let pow_linear = f_powf(linear, 0.1593017578125);
199200
let num = 0.1640625 * pow_linear - 0.1640625;
200201
let den = 1.0 + 18.6875 * pow_linear;
201-
f32::powf(1.0 + num / den, 78.84375)
202+
f_powf(1.0 + num / den, 78.84375)
202203
} else {
203204
0.0
204205
}
@@ -211,10 +212,10 @@ pub(crate) fn hlg_to_linear(gamma: f32) -> f32 {
211212
return 0.0;
212213
}
213214
let linear = if gamma <= 0.5 {
214-
f32::powf((gamma * gamma) * (1.0 / 3.0), 1.2)
215+
f_powf((gamma * gamma) * (1.0 / 3.0), 1.2)
215216
} else {
216-
f32::powf(
217-
(f32::exp((gamma - 0.55991073) / 0.17883277) + 0.28466892) / 12.0,
217+
f_powf(
218+
(f_expf((gamma - 0.55991073) / 0.17883277) + 0.28466892) / 12.0,
218219
1.2,
219220
)
220221
};
@@ -230,13 +231,13 @@ pub(crate) fn hlg_from_linear(linear: f32) -> f32 {
230231
// Scale from extended SDR range to [0.0, 1.0].
231232
let mut linear = (linear * (SDR_WHITE_NITS / HLG_WHITE_NITS)).clamp(0., 1.);
232233
// Inverse OOTF followed by OETF see Table 5 and Note 5i in ITU-R BT.2100-2 page 7-8.
233-
linear = f32::powf(linear, 1.0 / 1.2);
234+
linear = f_powf(linear, 1.0 / 1.2);
234235
if linear < 0.0 {
235236
0.0
236237
} else if linear <= (1.0 / 12.0) {
237238
f32::sqrt(3.0 * linear)
238239
} else {
239-
0.17883277 * f32::ln(12.0 * linear - 0.28466892) + 0.55991073
240+
0.17883277 * f_logf(12.0 * linear - 0.28466892) + 0.55991073
240241
}
241242
}
242243

src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
//!
3131
//! ## Example
3232
//!
33-
//! ```rust
33+
//! ```rust,no_run,ignore
3434
//! let img = image::ImageReader::open("./assets/hdr.avif")?
3535
//! .decode()?;
3636
//! let rgb = img.to_rgb8();
@@ -72,7 +72,7 @@
7272
//! Some patches on `zune-image` are still being processed. Manually updating a
7373
//! dependency of `zune-image` might be required.
7474
//!
75-
//! ```rust
75+
//! ```rust,no_run,ignore
7676
//! pub struct GainMapAssociationGroup {
7777
//! pub image: Vec<u8>,
7878
//! pub gain_map: Vec<u8>,
@@ -238,6 +238,7 @@ mod gamma;
238238
mod iso_gain_map;
239239
mod mappers;
240240
mod mlaf;
241+
mod rgb_tone_mapper;
241242
mod spline;
242243
mod tonemapper;
243244

src/mappers.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,11 @@ impl<const CN: usize> ToneMap for ReinhardJodieToneMapper<CN> {
347347
let tv_g = chunk[1] / (1.0f32 + chunk[1]);
348348
let tv_b = chunk[2] / (1.0f32 + chunk[2]);
349349

350-
chunk[0] = lerp(chunk[0] / (1f32 + luma), tv_r, tv_r).min(1f32);
351-
chunk[1] = lerp(chunk[1] / (1f32 + luma), tv_g, tv_g).min(1f32);
352-
chunk[2] = lerp(chunk[1] / (1f32 + luma), tv_b, tv_b).min(1f32);
350+
let luma_scale = 1. / (1f32 + luma);
351+
352+
chunk[0] = lerp(chunk[0] * luma_scale, tv_r, tv_r).min(1f32);
353+
chunk[1] = lerp(chunk[1] * luma_scale, tv_g, tv_g).min(1f32);
354+
chunk[2] = lerp(chunk[1] * luma_scale, tv_b, tv_b).min(1f32);
353355
}
354356
}
355357

src/rgb_tone_mapper.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* // Copyright (c) Radzivon Bartoshyk 8/2025. All rights reserved.
3+
* //
4+
* // Redistribution and use in source and binary forms, with or without modification,
5+
* // are permitted provided that the following conditions are met:
6+
* //
7+
* // 1. Redistributions of source code must retain the above copyright notice, this
8+
* // list of conditions and the following disclaimer.
9+
* //
10+
* // 2. Redistributions in binary form must reproduce the above copyright notice,
11+
* // this list of conditions and the following disclaimer in the documentation
12+
* // and/or other materials provided with the distribution.
13+
* //
14+
* // 3. Neither the name of the copyright holder nor the names of its
15+
* // contributors may be used to endorse or promote products derived from
16+
* // this software without specific prior written permission.
17+
* //
18+
* // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
* // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
* // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
* // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
* // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
* // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
* // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
* // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*/
29+
use crate::mlaf::mlaf;
30+
use crate::{m_clamp, ForgeError, RgbToneMapperParameters, ToneMapper};
31+
use moxcms::{filmlike_clip, CmsError, InPlaceStage, Matrix3f, Rgb};
32+
use num_traits::AsPrimitive;
33+
use std::fmt::Debug;
34+
35+
pub(crate) struct ToneMapperImpl<T: Copy, const N: usize, const CN: usize, const GAMMA_SIZE: usize>
36+
{
37+
pub(crate) linear_map_r: Box<[f32; N]>,
38+
pub(crate) linear_map_g: Box<[f32; N]>,
39+
pub(crate) linear_map_b: Box<[f32; N]>,
40+
pub(crate) gamma_map_r: Box<[T; 65536]>,
41+
pub(crate) gamma_map_g: Box<[T; 65536]>,
42+
pub(crate) gamma_map_b: Box<[T; 65536]>,
43+
pub(crate) im_stage: Option<Box<dyn InPlaceStage + Sync + Send>>,
44+
pub(crate) tone_map: Box<crate::tonemapper::SyncToneMap>,
45+
pub(crate) params: RgbToneMapperParameters,
46+
}
47+
48+
pub(crate) struct MatrixStage<const CN: usize> {
49+
pub(crate) gamut_color_conversion: Matrix3f,
50+
}
51+
52+
impl<const CN: usize> InPlaceStage for MatrixStage<CN> {
53+
fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> {
54+
let c = self.gamut_color_conversion;
55+
for chunk in dst.chunks_exact_mut(CN) {
56+
let r = mlaf(
57+
mlaf(chunk[0] * c.v[0][0], chunk[1], c.v[0][1]),
58+
chunk[2],
59+
c.v[0][2],
60+
);
61+
let g = mlaf(
62+
mlaf(chunk[0] * c.v[1][0], chunk[1], c.v[1][1]),
63+
chunk[2],
64+
c.v[1][2],
65+
);
66+
let b = mlaf(
67+
mlaf(chunk[0] * c.v[2][0], chunk[1], c.v[2][1]),
68+
chunk[2],
69+
c.v[2][2],
70+
);
71+
72+
chunk[0] = m_clamp(r, 0.0, 1.0);
73+
chunk[1] = m_clamp(g, 0.0, 1.0);
74+
chunk[2] = m_clamp(b, 0.0, 1.0);
75+
}
76+
Ok(())
77+
}
78+
}
79+
80+
pub(crate) struct MatrixGamutClipping<const CN: usize> {
81+
pub(crate) gamut_color_conversion: Matrix3f,
82+
}
83+
84+
impl<const CN: usize> InPlaceStage for MatrixGamutClipping<CN> {
85+
fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> {
86+
let c = self.gamut_color_conversion;
87+
for chunk in dst.chunks_exact_mut(CN) {
88+
let r = mlaf(
89+
mlaf(chunk[0] * c.v[0][0], chunk[1], c.v[0][1]),
90+
chunk[2],
91+
c.v[0][2],
92+
);
93+
let g = mlaf(
94+
mlaf(chunk[0] * c.v[1][0], chunk[1], c.v[1][1]),
95+
chunk[2],
96+
c.v[1][2],
97+
);
98+
let b = mlaf(
99+
mlaf(chunk[0] * c.v[2][0], chunk[1], c.v[2][1]),
100+
chunk[2],
101+
c.v[2][2],
102+
);
103+
104+
let mut rgb = Rgb::new(r, g, b);
105+
if rgb.is_out_of_gamut() {
106+
rgb = filmlike_clip(rgb);
107+
chunk[0] = m_clamp(rgb.r, 0.0, 1.0);
108+
chunk[1] = m_clamp(rgb.g, 0.0, 1.0);
109+
chunk[2] = m_clamp(rgb.b, 0.0, 1.0);
110+
} else {
111+
chunk[0] = m_clamp(r, 0.0, 1.0);
112+
chunk[1] = m_clamp(g, 0.0, 1.0);
113+
chunk[2] = m_clamp(b, 0.0, 1.0);
114+
}
115+
}
116+
Ok(())
117+
}
118+
}
119+
120+
impl<
121+
T: Copy + AsPrimitive<usize> + Clone + Default + Debug,
122+
const N: usize,
123+
const CN: usize,
124+
const GAMMA_SIZE: usize,
125+
> ToneMapper<T> for ToneMapperImpl<T, N, CN, GAMMA_SIZE>
126+
where
127+
u32: AsPrimitive<T>,
128+
{
129+
fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError> {
130+
assert!(CN == 3 || CN == 4);
131+
if src.len() != dst.len() {
132+
return Err(ForgeError::LaneSizeMismatch);
133+
}
134+
if src.len() % CN != 0 {
135+
return Err(ForgeError::LaneMultipleOfChannels);
136+
}
137+
assert_eq!(src.len(), dst.len());
138+
let mut linearized_content = vec![0f32; src.len()];
139+
for (src, dst) in src
140+
.chunks_exact(CN)
141+
.zip(linearized_content.chunks_exact_mut(CN))
142+
{
143+
dst[0] = self.linear_map_r[src[0].as_()] * self.params.exposure;
144+
dst[1] = self.linear_map_g[src[1].as_()] * self.params.exposure;
145+
dst[2] = self.linear_map_b[src[2].as_()] * self.params.exposure;
146+
if CN == 4 {
147+
dst[3] = f32::from_bits(src[3].as_() as u32);
148+
}
149+
}
150+
151+
self.tonemap_linearized_lane(&mut linearized_content)?;
152+
153+
if let Some(c) = &self.im_stage {
154+
c.transform(&mut linearized_content)
155+
.map_err(|_| ForgeError::UnknownError)?;
156+
} else {
157+
for chunk in linearized_content.chunks_exact_mut(CN) {
158+
let rgb = Rgb::new(chunk[0], chunk[1], chunk[2]);
159+
chunk[0] = rgb.r.max(0.);
160+
chunk[1] = rgb.g.max(0.);
161+
chunk[2] = rgb.b.max(0.);
162+
}
163+
}
164+
165+
let scale_value = (GAMMA_SIZE - 1) as f32;
166+
167+
for (dst, src) in dst
168+
.chunks_exact_mut(CN)
169+
.zip(linearized_content.chunks_exact(CN))
170+
{
171+
let r = mlaf(0.5, src[0], scale_value).min(u16::MAX as f32) as u16;
172+
let g = mlaf(0.5, src[1], scale_value).min(u16::MAX as f32) as u16;
173+
let b = mlaf(0.5, src[2], scale_value).min(u16::MAX as f32) as u16;
174+
dst[0] = self.gamma_map_r[r as usize];
175+
dst[1] = self.gamma_map_g[g as usize];
176+
dst[2] = self.gamma_map_b[b as usize];
177+
if CN == 4 {
178+
dst[3] = src[3].to_bits().as_();
179+
}
180+
}
181+
182+
Ok(())
183+
}
184+
185+
fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError> {
186+
assert!(CN == 3 || CN == 4);
187+
if in_place.len() % CN != 0 {
188+
return Err(ForgeError::LaneMultipleOfChannels);
189+
}
190+
self.tone_map.process_lane(in_place);
191+
Ok(())
192+
}
193+
}

0 commit comments

Comments
 (0)