Skip to main content

hdim_core/
lib.rs

1//! Core logic for High Definition Image Manipulator (hdim).
2//!
3//! This crate provides the foundational data structures and image processing
4//! algorithms used throughout the hdim workspace. It handles image loading,
5//! adjustment state management, and the undo/redo history stack.
6
7pub mod adjustments;
8pub mod consts;
9#[cfg(feature = "exif")]
10pub mod exif;
11pub mod history;
12pub mod localization;
13pub mod state;
14pub mod transform;
15pub mod utils;
16use crate::history::History;
17use crate::state::TransformState;
18use anyhow::Result;
19use image::{DynamicImage, GenericImageView};
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23/// Represents the set of image adjustments that can be applied to a [HdimImage].
24///
25/// All adjustments are stored as `f32` values, typically in the range of -100.0 to 100.0,
26/// although the exact interpretation depends on the specific adjustment implementation.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Adjustments {
29    /// Overall lightness of the image.
30    pub brightness: f32,
31    /// Difference between light and dark areas.
32    pub contrast: f32,
33    /// Overall light captured in the image.
34    pub exposure: f32,
35    /// Reduces contrast in shadows for a "matte" look.
36    pub fade: f32,
37    /// Adds simulated analog texture.
38    pub grain: f32,
39    /// Shifts the entire color spectrum.
40    pub hue: f32,
41    /// Adds digital luminance/chroma noise.
42    pub noise: f32,
43    /// Intensity of colors.
44    pub saturation: f32,
45    /// Smart saturation that protects skin tones.
46    pub vibrance: f32,
47    /// Shift between blue (cool) and yellow (warm) tones.
48    pub warmth: f32,
49}
50
51impl Default for Adjustments {
52    fn default() -> Self {
53        Self {
54            brightness: 0.0,
55            contrast: 0.0,
56            exposure: 0.0,
57            fade: 0.0,
58            grain: 0.0,
59            hue: 0.0,
60            noise: 0.0,
61            saturation: 0.0,
62            vibrance: 0.0,
63            warmth: 0.0,
64        }
65    }
66}
67
68/// The primary image structure used for editing sessions.
69///
70/// `HdimImage` wraps a [DynamicImage] and maintains its adjustment state
71/// and modification history.
72#[derive(Debug, Clone)]
73pub struct HdimImage {
74    /// Original path of the image on disk.
75    pub path: PathBuf,
76    /// Raw image data loaded into memory.
77    pub data: Arc<DynamicImage>,
78    /// Width of the image in pixels.
79    pub width: u32,
80    /// Height of the image in pixels.
81    pub height: u32,
82    /// Current set of adjustments applied to the image.
83    pub adjustments: Adjustments,
84    /// History of image states for undo/redo functionality.
85    pub history: History,
86}
87
88impl HdimImage {
89    /// Creates a new [HdimImage] by loading it from a file path.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the image cannot be opened or decoded by the `image` crate.
94    ///
95    /// # Examples
96    ///
97    /// ```no_run
98    /// use hdim_core::HdimImage;
99    /// use std::path::Path;
100    ///
101    /// let path = Path::new("tests/images/4k.jpg");
102    /// let hdim_image = HdimImage::from_path(path).unwrap();
103    /// ```
104    pub fn from_path(path: &Path) -> Result<Self> {
105        let data = image::open(path)?;
106        let (width, height) = data.dimensions();
107        let adjustments = Adjustments::default();
108        let data_arc = Arc::new(data);
109
110        Ok(HdimImage {
111            path: path.to_path_buf(),
112            data: data_arc.clone(),
113            width,
114            height,
115            adjustments,
116            history: History::new(data_arc, adjustments),
117        })
118    }
119
120    /// Records the current image state and adjustments into history.
121    pub fn record_state(&mut self) {
122        self.history
123            .record_state(self.data.clone(), self.adjustments);
124    }
125
126    /// Moves one step backward in history and updates the current state.
127    pub fn undo(&mut self) -> bool {
128        if let Some(state) = self.history.undo() {
129            self.data = state.data;
130            self.adjustments = state.adjustments;
131            let (width, height) = self.data.dimensions();
132            self.width = width;
133            self.height = height;
134            true
135        } else {
136            false
137        }
138    }
139
140    /// Moves one step forward in history and updates the current state.
141    pub fn redo(&mut self) -> bool {
142        if let Some(state) = self.history.redo() {
143            self.data = state.data;
144            self.adjustments = state.adjustments;
145            let (width, height) = self.data.dimensions();
146            self.width = width;
147            self.height = height;
148            true
149        } else {
150            false
151        }
152    }
153
154    /// Permanently applies a [TransformState] to the base image data.
155    pub fn transform_image(&mut self, transform: &TransformState) {
156        let new_data = transform::apply_transform(&self.data, transform);
157        let (width, height) = new_data.dimensions();
158        self.data = Arc::new(new_data);
159        self.width = width;
160        self.height = height;
161        // After a transform, we should record the new state
162        self.record_state();
163    }
164
165    /// Applies the current set of [Adjustments] to the raw image data.
166    ///
167    /// This method performs a sequential application of light, color, and effect
168    /// transformations, returning a new [DynamicImage].
169    ///
170    /// # Examples
171    ///
172    /// ```no_run
173    /// use hdim_core::HdimImage;
174    /// use std::path::Path;
175    ///
176    /// let path = Path::new("tests/images/4k.jpg");
177    /// let mut hdim_image = HdimImage::from_path(path).unwrap();
178    /// hdim_image.adjustments.brightness = 10.0;
179    /// let adjusted = hdim_image.apply_adjustments();
180    /// ```
181    pub fn apply_adjustments(&self) -> DynamicImage {
182        let mut adjusted_image = (*self.data).clone();
183        let adj = self.adjustments;
184
185        // Order of application matters
186        adjusted_image = self.apply_light_adjustments(adjusted_image, &adj);
187        adjusted_image = self.apply_color_adjustments(adjusted_image, &adj);
188        adjusted_image = self.apply_effect_adjustments(adjusted_image, &adj);
189
190        adjusted_image
191    }
192
193    /// Internal helper to apply light-based transformations (exposure, brightness, contrast).
194    fn apply_light_adjustments(&self, mut image: DynamicImage, adj: &Adjustments) -> DynamicImage {
195        if adj.exposure != 0.0 {
196            image = adjustments::exposure::apply_exposure(&image, adj.exposure);
197        }
198        if adj.brightness != 0.0 {
199            image = adjustments::brightness::apply_brightness(&image, adj.brightness);
200        }
201        if adj.contrast != 0.0 {
202            image = adjustments::contrast::apply_contrast(&image, adj.contrast);
203        }
204        image
205    }
206
207    /// Internal helper to apply color-based transformations (warmth, vibrance, saturation, hue).
208    fn apply_color_adjustments(&self, mut image: DynamicImage, adj: &Adjustments) -> DynamicImage {
209        if adj.warmth != 0.0 {
210            image = adjustments::warmth::apply_warmth(&image, adj.warmth);
211        }
212        if adj.vibrance != 0.0 {
213            image = adjustments::vibrance::apply_vibrance(&image, adj.vibrance);
214        }
215        if adj.saturation != 0.0 {
216            image = adjustments::saturation::apply_saturation(&image, adj.saturation);
217        }
218        if adj.hue != 0.0 {
219            image = adjustments::hue::apply_hue(&image, adj.hue);
220        }
221        image
222    }
223
224    /// Internal helper to apply effects (fade, grain, noise).
225    fn apply_effect_adjustments(&self, mut image: DynamicImage, adj: &Adjustments) -> DynamicImage {
226        if adj.fade != 0.0 {
227            image = adjustments::fade::apply_fade(&image, adj.fade);
228        }
229        if adj.grain != 0.0 {
230            image = adjustments::grain::apply_grain(&image, adj.grain);
231        }
232        if adj.noise != 0.0 {
233            image = adjustments::noise::apply_noise(&image, adj.noise);
234        }
235        image
236    }
237
238    /// Saves the image with the current adjustments and handles EXIF metadata.
239    ///
240    /// If the `exif` feature is enabled, this method will attempt to preserve or strip
241    /// EXIF data based on the `strip` argument.
242    ///
243    /// # Arguments
244    ///
245    /// * `path` - The destination path for the saved image.
246    /// * `format` - The image format to use for saving.
247    /// * `strip` - Whether to strip sensitive EXIF data.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the image cannot be saved or if EXIF handling fails.
252    pub fn save_with_exif(
253        &self,
254        path: &Path,
255        format: image::ImageFormat,
256        strip: bool,
257    ) -> Result<()> {
258        let adjusted_image = self.apply_adjustments();
259
260        #[cfg(feature = "exif")]
261        {
262            use img_parts::ImageEXIF;
263            if let Some(exif_bytes) = exif::get_exif_bytes_for_save(&self.path, strip)? {
264                let mut buffer = std::io::Cursor::new(Vec::new());
265                adjusted_image.write_to(&mut buffer, format)?;
266                let mut bytes = buffer.into_inner();
267
268                if format == image::ImageFormat::Jpeg {
269                    let mut jpeg = img_parts::jpeg::Jpeg::from_bytes(bytes.into())?;
270                    jpeg.set_exif(Some(exif_bytes.into()));
271                    let mut out = Vec::new();
272                    jpeg.encoder().write_to(&mut out)?;
273                    bytes = out;
274                } else if format == image::ImageFormat::Png {
275                    let mut png = img_parts::png::Png::from_bytes(bytes.into())?;
276                    png.set_exif(Some(exif_bytes.into()));
277                    let mut out = Vec::new();
278                    png.encoder().write_to(&mut out)?;
279                    bytes = out;
280                }
281
282                std::fs::write(path, bytes)?;
283                return Ok(());
284            }
285        }
286
287        adjusted_image.save_with_format(path, format)?;
288        Ok(())
289    }
290}
291
292/// Simple width and height dimensions.
293#[derive(Debug, Clone, Copy, PartialEq)]
294pub struct Size {
295    /// Width in pixels or units.
296    pub width: u32,
297    /// Height in pixels or units.
298    pub height: u32,
299}
300
301/// Calculates the target dimensions for resizing an image to fit within a maximum size.
302///
303/// It accounts for the non-square aspect ratio of terminal character cells
304/// (approximately 1:2) by doubling the target height.
305pub fn calculate_resize(image: &DynamicImage, max_size: Size) -> Size {
306    let (width, height) = image.dimensions();
307
308    // Terminal cells are taller (approx 1:2 ratio)
309    // We target a "virtual" canvas that is double the terminal height
310    let target_width = max_size.width;
311    let target_height = max_size.height * 2;
312
313    let width_ratio = target_width as f64 / width as f64;
314    let height_ratio = target_height as f64 / height as f64;
315    let ratio = width_ratio.min(height_ratio);
316
317    Size {
318        width: (width as f64 * ratio) as u32,
319        height: (height as f64 * ratio) as u32,
320    }
321}