///|
/// Explicit numerical conversion. Identity conversion retains every source bit.
/// Float->UINT rejects fractions and nonfinite values; no implicit saturation.
pub fn Plane::convert(
self : Plane,
kind : PixelType,
limits~ : Limits = Limits::default(),
) -> Plane raise ExrError {
limits.validate()
if self.window.pixels() > limits.max_pixels {
raise Limit("converted plane pixel count")
}
if kind == self.channel.kind {
return Plane::from_bits(self.channel, self.window, self.data, limits~)
}
let out = sink()
if mul(self.window.pixels(), kind.bytes()) > limits.max_decoded_bytes {
raise Limit("converted plane bytes")
}
let r = reader(self.data)
while r.pos < r.end {
let bits = r.uint(self.channel.kind.bytes()).to_uint()
if self.channel.kind == F16 && kind == F32 {
out.uint(half_to_float_bits(bits.reinterpret_as_int()).to_uint64(), 4)
continue
}
if self.channel.kind == F32 && kind == F16 {
out.uint(float_bits_to_half(bits).to_uint64(), 2)
continue
}
let value = match self.channel.kind {
U32 => bits.to_double()
F16 => half_value(bits.reinterpret_as_int())
F32 => Float::reinterpret_from_uint(bits).to_double()
}
match kind {
U32 => {
if !finite(value) ||
value < 0.0 ||
value > 4294967295.0 ||
value.trunc() != value {
raise Invalid("lossy float to UINT conversion", r.pos)
}
out.uint(value.to_uint64(), 4)
}
F16 =>
out.uint(
float_bits_to_half(Float::from_double(value).reinterpret_as_uint()).to_uint64(),
2,
)
F32 => out.f32(value)
}
}
Plane::from_bits(
{ ..self.channel, kind, },
self.window,
out.finish(),
limits~,
)
}
///|
/// Map sample values to a new plane; never mutates the input.
/// F16/F32 conversion follows round-to-nearest. UINT validation is strict.
pub fn Plane::map_values(
self : Plane,
function : (Double) -> Double,
output_type~ : PixelType = F32,
limits~ : Limits = Limits::default(),
) -> Plane raise ExrError {
limits.validate()
let values : Array[Double] = []
let w = self.window
if w.pixels() > limits.max_pixels ||
mul(w.pixels(), output_type.bytes()) > limits.max_decoded_bytes {
raise Limit("mapped plane allocation")
}
for row = 0; row < w.height(); row = row + 1 {
for col = 0; col < w.width(); col = col + 1 {
values.push(function(self.value_at(w.x0 + col, w.y0 + row)))
}
}
Plane::from_values({ ..self.channel, kind: output_type, }, w, values, limits~)
}
///|
pub fn Image::replace_plane(
self : Image,
replacement : Plane,
limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
let mut found = false
let planes = self.planes.map(p => {
if p.channel.name == replacement.channel.name {
found = true
replacement
} else {
p
}
})
if !found {
planes.push(replacement)
}
self.with_planes(self.data_window, planes, limits)
}
///|
test "explicit conversion preserves half payloads and rejects fractional UINT" {
let w = Window::new(0, 0, 3, 0)
let p = Plane::from_bits(
{ name: "Y", kind: F16, linear: false, },
w,
b"\x00\x80\x01\x7c\xff\x7f\x00\x3c",
)
assert_eq(p.convert(F32).convert(F16).sample_bits(), p.sample_bits())
assert_eq(p.convert(F16).sample_bits(), p.sample_bits())
assert_true(
try {
ignore(p.convert(U32))
false
} catch {
_ => true
},
)
let exact = Plane::from_values({ name: "id", kind: U32, linear: true, }, w, [
0.0, 1.0, 16777216.0, 4294967295.0,
])
assert_eq(exact.convert(F32).value_at(2, 0), 16777216.0)
assert_true(
try {
ignore(exact.convert(F32).convert(U32))
false
} catch {
_ => true
},
)
let mapped = p.map_values(_ => 2.0)
assert_eq(mapped.statistics().mean, Some(2.0))
let image = Image::new(w, [p]).replace_plane(mapped)
assert_eq(image.plane("Y").channel().kind, F32)
}