1use alloc::{boxed::Box, sync::Arc};
2use core::fmt;
3use zencodec::{
4 At, CodecError,
5 collection::{Cause, Collection, ImageId, ImageRequest},
6 image::{ImageOutput, OutputError, PackedImageOwner, Rect},
7 operation::{
8 CheckedCollection, CollectionOperationError, OperationPolicy, Outcome, PolicySet,
9 resolve_image,
10 },
11 storage::{SharedStorage, StorageBudget, StorageError},
12};
13use zenpixels::{Cicp, ColorContext, ColorOrigin, PixelDescriptor, PixelSlice, PixelSliceMut};
14use zenpixels_convert::{ConvertOptions, OutputPlan, OutputProfile, cms::PluggableCms};
15
16#[derive(Clone)]
19#[non_exhaustive]
20pub enum ColorTarget {
21 Current,
23 Named(Cicp),
25 Icc(Arc<[u8]>),
27}
28pub struct RenderRequest {
30 pub target: ColorTarget,
32 pub required: PixelDescriptor,
34 pub region: Option<Rect>,
36 pub conversion: ConvertOptions,
38}
39impl Default for RenderRequest {
40 fn default() -> Self {
41 Self {
42 target: ColorTarget::Named(Cicp::new(1, 13, 0, true)),
43 required: PixelDescriptor::RGBA8_SRGB,
44 region: None,
45 conversion: ConvertOptions::permissive(),
46 }
47 }
48}
49impl RenderRequest {
50 pub fn new(required: PixelDescriptor) -> Self {
52 Self {
53 target: ColorTarget::Current,
54 required,
55 region: None,
56 conversion: ConvertOptions::retain_channels(),
57 }
58 }
59 pub fn with_target(mut self, target: ColorTarget) -> Self {
61 self.target = target;
62 self
63 }
64 pub fn with_region(mut self, region: Option<Rect>) -> Self {
66 self.region = region;
67 self
68 }
69 pub fn with_conversion(mut self, conversion: ConvertOptions) -> Self {
71 self.conversion = conversion;
72 self
73 }
74}
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum RenderStage {
79 ColorPlan,
81 Geometry,
83 Color,
85 Publication,
87}
88pub struct RenderFailure {
90 pub stage: RenderStage,
92 cause: Cause,
93}
94impl fmt::Debug for RenderFailure {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("RenderFailure")
97 .field("stage", &self.stage)
98 .finish_non_exhaustive()
99 }
100}
101impl fmt::Display for RenderFailure {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "selected-image render failed during {:?}", self.stage)
104 }
105}
106impl core::error::Error for RenderFailure {
107 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
108 Some(self.cause.as_ref())
109 }
110}
111fn fail(stage: RenderStage, cause: impl Into<Cause>) -> At<RenderFailure> {
112 At::wrap(RenderFailure {
113 stage,
114 cause: cause.into(),
115 })
116}
117fn check(pulse: &dyn zencodec::Pulse) -> Result<(), Cause> {
118 pulse
119 .check()
120 .map_err(|reason| At::wrap(CodecError::new(None, reason.into())).into())
121}
122
123struct Color {
124 data: SharedStorage<u64>,
125 width: u32,
126 rows: u32,
127 stride: usize,
128 descriptor: PixelDescriptor,
129 context: Arc<ColorContext>,
130}
131impl PackedImageOwner for Color {
132 fn pixels(&self) -> Result<PixelSlice<'_>, At<OutputError>> {
133 PixelSlice::new(
134 bytemuck::cast_slice(self.data.as_slice()),
135 self.width,
136 self.rows,
137 self.stride,
138 self.descriptor,
139 )
140 .map(|pixels| pixels.with_color_context(self.context.clone()))
141 .map_err(|cause| {
142 At::wrap(OutputError::Mapping(zencodec::frame::MapError::Backend(
143 Box::new(cause),
144 )))
145 })
146 }
147}
148
149pub struct Renderer<'a> {
152 pub request: RenderRequest,
154 pub storage: StorageBudget,
156 pub cms: Option<&'a dyn PluggableCms>,
158}
159impl<'a> Renderer<'a> {
160 pub fn new(storage: StorageBudget) -> Self {
164 Self {
165 request: RenderRequest::default(),
166 storage,
167 cms: None,
168 }
169 }
170 pub fn with_request(mut self, request: RenderRequest) -> Self {
172 self.request = request;
173 self
174 }
175 pub fn with_cms(mut self, cms: &'a dyn PluggableCms) -> Self {
177 self.cms = Some(cms);
178 self
179 }
180
181 pub fn render_first<C: Collection, R: OperationPolicy>(
187 &self,
188 images: &mut CheckedCollection<C, R>,
189 ) -> Result<Option<Outcome<ImageOutput>>, At<CollectionOperationError>> {
190 let mut cursor = images.images()?;
191 images
192 .next(&mut cursor)?
193 .map(|id| self.render_selected(images, &id))
194 .transpose()
195 }
196
197 pub fn render_selected<C: Collection, R: OperationPolicy>(
201 &self,
202 images: &mut CheckedCollection<C, R>,
203 image: &ImageId,
204 ) -> Result<Outcome<ImageOutput>, At<CollectionOperationError>> {
205 images.decode_with(image, &ImageRequest::new(), |held, pulse| {
206 self.apply(held, Some(pulse))
207 })
208 }
209
210 pub fn render<R: OperationPolicy + ?Sized>(
213 &self,
214 image: ImageOutput,
215 policy: &mut R,
216 pulse: Option<&dyn zencodec::Pulse>,
217 ) -> Result<Outcome<ImageOutput>, At<RenderFailure>> {
218 let output = self.apply(image, pulse)?;
219 resolve_image(output, policy, PolicySet::new(), pulse)
220 .map_err(|e| fail(RenderStage::Publication, e))
221 }
222
223 #[deprecated(note = "use render with a PolicySet or BoundPolicy")]
226 pub fn render_with_preset<R: OperationPolicy + ?Sized>(
227 &self,
228 image: ImageOutput,
229 resolver: &mut R,
230 preset: PolicySet,
231 pulse: Option<&dyn zencodec::Pulse>,
232 ) -> Result<Outcome<ImageOutput>, At<RenderFailure>> {
233 let output = self.apply(image, pulse)?;
234 resolve_image(output, resolver, preset, pulse)
235 .map_err(|e| fail(RenderStage::Publication, e))
236 }
237
238 fn apply(
239 &self,
240 image: ImageOutput,
241 pulse: Option<&dyn zencodec::Pulse>,
242 ) -> Result<ImageOutput, At<RenderFailure>> {
243 let control = pulse.unwrap_or(&zencodec::NoPulse);
244 check(control).map_err(|e| fail(RenderStage::ColorPlan, e))?;
245 let mut plan = image
247 .with_pixels(|pixels| {
248 let target = match &self.request.target {
249 ColorTarget::Current => OutputProfile::SameAsOrigin,
250 ColorTarget::Named(cicp) => OutputProfile::Named(*cicp),
251 ColorTarget::Icc(icc) => OutputProfile::Icc(icc.clone()),
252 };
253 OutputPlan::new_with_descriptor(
254 pixels.descriptor(),
255 pixels.color_context().cloned(),
256 &ColorOrigin::assumed(),
257 target,
258 self.request.required,
259 &self.request.conversion,
260 self.cms,
261 )
262 })
263 .map_err(|e| fail(RenderStage::ColorPlan, e))?
264 .map_err(|e| fail(RenderStage::ColorPlan, e))?;
265 let projected = crate::geometry::project(&image, self.request.region, &self.storage, pulse)
266 .map_err(|e| fail(RenderStage::Geometry, e))?;
267 drop(image);
270 let unchanged = projected
271 .with_pixels(|pixels| {
272 plan.borrow_rows(pixels.clone()).map(|borrowed| {
273 borrowed.is_some_and(|view| {
274 view.descriptor() == pixels.descriptor()
275 && view.color_context() == pixels.color_context()
276 })
277 })
278 })
279 .map_err(|e| fail(RenderStage::Color, e))?
280 .map_err(|e| fail(RenderStage::Color, e))?;
281 let output = if unchanged {
282 projected
283 } else {
284 let color = projected
285 .with_pixels(|pixels| self.convert(pixels, &mut plan, control))
286 .map_err(|e| fail(RenderStage::Color, e))?
287 .map_err(|e| fail(RenderStage::Color, e))?;
288 projected
289 .replace_packed_color(color)
290 .map_err(|e| fail(RenderStage::Color, e))?
291 };
292 Ok(output)
293 }
294
295 fn convert(
296 &self,
297 source: PixelSlice<'_>,
298 plan: &mut OutputPlan,
299 pulse: &dyn zencodec::Pulse,
300 ) -> Result<Color, Cause> {
301 let descriptor = plan.output_descriptor();
302 let (width, rows) = (source.width(), source.rows());
303 let stride = (width as usize)
304 .checked_add(8)
305 .map(|n| n & !7)
306 .and_then(|n| n.checked_mul(descriptor.bytes_per_pixel()))
307 .ok_or(StorageError::SizeOverflow)?;
308 let bytes = stride
309 .checked_mul(rows as usize)
310 .ok_or(StorageError::SizeOverflow)?;
311 check(pulse)?;
312 plan.prepare(width)?;
313 check(pulse)?;
314 let mut data = self.storage.filled(bytes / 8, 0xa5a5_a5a5_a5a5_a5a5u64)?;
315 let mut context = None;
316 for y in 0..rows {
319 check(pulse)?;
320 let offset = y as usize * stride;
321 let destination = PixelSliceMut::new(
322 &mut bytemuck::cast_slice_mut(data.as_mut_slice())[offset..offset + stride],
323 width,
324 1,
325 stride,
326 descriptor,
327 )?;
328 let converted = plan.convert_rows(source.crop_view(0, y, width, 1), destination)?;
329 context = converted.color_context().cloned();
330 }
331 check(pulse)?;
332 Ok(Color {
333 data: data.freeze(),
334 width,
335 rows,
336 stride,
337 descriptor,
338 context: context.ok_or(OutputError::MissingPlane)?,
339 })
340 }
341}