feat(domain): Add crop_to_image() and extract_meta() to RasterDocument
- crop_to_image(): Non-destructive crop returning DynamicImage - extract_meta(): Extract BasicMeta and EXIF metadata - Completes migration of RasterDocument features from app/ to domain/ Refs: Migration Step 1.2
This commit is contained in:
parent
3cf99ad19d
commit
8ff43ea5d7
2 changed files with 525 additions and 0 deletions
167
DEVNOTE/Feature-Comparison.md
Normal file
167
DEVNOTE/Feature-Comparison.md
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# Feature-Vergleich: app/document vs domain/document/types
|
||||||
|
|
||||||
|
**Ziel:** Identifizieren welche Features von `src/app/document/` nach `src/domain/document/types/` portiert werden müssen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RasterDocument
|
||||||
|
|
||||||
|
### Struct-Felder
|
||||||
|
|
||||||
|
| Feld | app/ | domain/ | Status |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| `document: DynamicImage` | ✅ | ✅ | OK |
|
||||||
|
| `native_width: u32` | ✅ | ✅ | OK |
|
||||||
|
| `native_height: u32` | ✅ | ✅ | OK |
|
||||||
|
| `transform: TransformState` | ✅ | ✅ | OK |
|
||||||
|
| `handle: ImageHandle` | ✅ pub | ✅ private | **⚠️ domain: public machen oder getter** |
|
||||||
|
| `fine_rotation_angle: f32` | ❌ | ✅ | ℹ️ Extra feature in domain |
|
||||||
|
| `interpolation_quality` | ❌ | ✅ | ℹ️ Extra feature in domain |
|
||||||
|
|
||||||
|
**Entscheidung:** Domain-Version hat mehr Features → Domain behalten, `handle` public machen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Methoden-Vergleich
|
||||||
|
|
||||||
|
| Methode | app/ | domain/ | Aktion |
|
||||||
|
|---------|------|---------|--------|
|
||||||
|
| **Core Operations** | | | |
|
||||||
|
| `open()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `render()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `save()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| | | | |
|
||||||
|
| **Transformations (Trait)** | | | |
|
||||||
|
| `rotate()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `flip()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `transform_state()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| | | | |
|
||||||
|
| **Renderable (Trait)** | | | |
|
||||||
|
| `info()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| | | | |
|
||||||
|
| **Dimensions** | | | |
|
||||||
|
| `dimensions()` | ✅ | ✅ | ✅ OK (beide haben es!) |
|
||||||
|
| `native_dimensions()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| | | | |
|
||||||
|
| **Crop** | | | |
|
||||||
|
| `crop()` | ✅ | ✅ | ✅ OK (beide haben es!) |
|
||||||
|
| `crop_to_image()` | ✅ | ❌ | 📋 **Portieren nach domain/** |
|
||||||
|
| | | | |
|
||||||
|
| **Handle/Image Access** | | | |
|
||||||
|
| `handle` (field pub) | ✅ | ❌ | 📋 **Public machen oder getter** |
|
||||||
|
| `handle()` (getter) | ❌ | ✅ | ✅ OK (domain hat getter) |
|
||||||
|
| `image()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| `get_rendered_image()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| | | | |
|
||||||
|
| **Metadata** | | | |
|
||||||
|
| `extract_meta()` | ✅ | ❌ | 📋 **Portieren nach domain/** |
|
||||||
|
| | | | |
|
||||||
|
| **Internal Helpers** | | | |
|
||||||
|
| `refresh_handle()` | ✅ private | ❌ | ℹ️ Evtl. bereits integriert |
|
||||||
|
| `apply_rotation()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| `apply_flip()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| `create_image_handle_from_image()` | ❌ | ✅ | ℹ️ Extra in domain |
|
||||||
|
| | | | |
|
||||||
|
| **Extra Features (domain)** | | | |
|
||||||
|
| `rotate_fine()` | ❌ | ✅ | ℹ️ Feature in domain |
|
||||||
|
| `reset_fine_rotation()` | ❌ | ✅ | ℹ️ Feature in domain |
|
||||||
|
| `set_interpolation_quality()` | ❌ | ✅ | ℹ️ Feature in domain |
|
||||||
|
| `resize_to_format()` | ❌ | ✅ | ℹ️ Feature in domain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Zusammenfassung RasterDocument
|
||||||
|
|
||||||
|
**Domain-Version ist fortgeschrittener** ✅
|
||||||
|
- Mehr Features (fine rotation, interpolation quality, resize)
|
||||||
|
- Bessere API (getter statt public fields)
|
||||||
|
- Saubere Helper-Funktionen
|
||||||
|
|
||||||
|
**Aus app/ portieren:**
|
||||||
|
1. ✅ `crop_to_image()` - Nicht-destruktives Crop
|
||||||
|
2. ✅ `extract_meta()` - Metadaten-Extraktion
|
||||||
|
3. ✅ `handle` public machen ODER getter `handle()` nutzen (bereits vorhanden!)
|
||||||
|
|
||||||
|
**Entscheidung:** Domain-Version als Basis, nur 2 Methoden fehlen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VectorDocument
|
||||||
|
|
||||||
|
### Methoden-Vergleich
|
||||||
|
|
||||||
|
| Methode | app/ | domain/ | Aktion |
|
||||||
|
|---------|------|---------|--------|
|
||||||
|
| `open()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `render()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `dimensions()` | ✅ | ❌ | 📋 **Portieren** |
|
||||||
|
| `handle` (pub field) | ✅ | ❌ private | 📋 **Public machen oder getter** |
|
||||||
|
| `extract_meta()` | ✅ | ❌ | 📋 **Portieren** |
|
||||||
|
| `crop()` | ❌ | ❌ | 📋 **Neu implementieren** (Design-Entscheidung) |
|
||||||
|
|
||||||
|
**Aus app/ portieren:**
|
||||||
|
1. `dimensions()`
|
||||||
|
2. `extract_meta()`
|
||||||
|
3. `handle()` getter oder public
|
||||||
|
4. NEU: `crop()` implementieren (render-based)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PortableDocument
|
||||||
|
|
||||||
|
### Methoden-Vergleich
|
||||||
|
|
||||||
|
| Methode | app/ | domain/ | Aktion |
|
||||||
|
|---------|------|---------|--------|
|
||||||
|
| `open()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `render()` | ✅ | ✅ | ✅ OK |
|
||||||
|
| `dimensions()` | ✅ | ❌ | 📋 **Portieren** |
|
||||||
|
| `handle` (pub field) | ✅ | ❌ private | 📋 **Public machen oder getter** |
|
||||||
|
| `extract_meta()` | ✅ | ❌ | 📋 **Portieren** |
|
||||||
|
| `crop()` | ❌ | ❌ | 📋 **Neu implementieren** (Design-Entscheidung) |
|
||||||
|
| Thumbnails | ✅ | ✅ | ℹ️ Prüfen ob identisch |
|
||||||
|
|
||||||
|
**Aus app/ portieren:**
|
||||||
|
1. `dimensions()`
|
||||||
|
2. `extract_meta()`
|
||||||
|
3. `handle()` getter oder public
|
||||||
|
4. NEU: `crop()` implementieren (render-based)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action Items für Schritt 1.2-1.4
|
||||||
|
|
||||||
|
### Schritt 1.2: RasterDocument (60 Min)
|
||||||
|
- [x] `crop()` - Bereits vorhanden! ✅
|
||||||
|
- [x] `dimensions()` - Bereits vorhanden! ✅
|
||||||
|
- [x] `crop_to_image()` hinzufügen ✅
|
||||||
|
- [x] `extract_meta()` hinzufügen ✅ (oder in core/metadata.rs)
|
||||||
|
- [x] `handle()` getter - Bereits vorhanden! ✅
|
||||||
|
|
||||||
|
### Schritt 1.3: VectorDocument (45 Min)
|
||||||
|
- [ ] `dimensions()` implementieren
|
||||||
|
- [ ] `handle()` getter hinzufügen
|
||||||
|
- [ ] `extract_meta()` implementieren
|
||||||
|
- [ ] `crop()` implementieren (render-based, neu!)
|
||||||
|
|
||||||
|
### Schritt 1.4: PortableDocument (45 Min)
|
||||||
|
- [ ] `dimensions()` implementieren
|
||||||
|
- [ ] `handle()` getter hinzufügen
|
||||||
|
- [ ] `extract_meta()` implementieren
|
||||||
|
- [ ] `crop()` implementieren (render-based, neu!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Überraschende Erkenntnisse
|
||||||
|
|
||||||
|
1. **Domain hat bereits crop() für Raster!** ✅
|
||||||
|
2. **Domain hat bereits dimensions()!** ✅
|
||||||
|
3. **Domain hat bereits handle() getter!** ✅
|
||||||
|
4. **Domain hat MEHR Features** (fine rotation, interpolation) ✅
|
||||||
|
|
||||||
|
**→ Domain-Implementierung ist besser! Nur 2-3 Methoden fehlen pro Type.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** Vergleich abgeschlossen
|
||||||
|
**Nächster Schritt:** 1.2 - RasterDocument ergänzen
|
||||||
358
src/domain/document/types/raster.rs
Normal file
358
src/domain/document/types/raster.rs
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// src/domain/document/types/raster.rs
|
||||||
|
//
|
||||||
|
// Raster image document support (PNG, JPEG, WebP, etc.).
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use image::{DynamicImage, GenericImageView, ImageReader};
|
||||||
|
|
||||||
|
use cosmic::widget::image::Handle as ImageHandle;
|
||||||
|
|
||||||
|
use crate::domain::document::core::document::{
|
||||||
|
DocResult, DocumentInfo, FlipDirection, InterpolationQuality, Renderable, RenderOutput,
|
||||||
|
Rotation, RotationMode, TransformState, Transformable,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Represents a raster image document (PNG, JPEG, WebP, ...).
|
||||||
|
pub struct RasterDocument {
|
||||||
|
/// The decoded image document.
|
||||||
|
document: DynamicImage,
|
||||||
|
/// Native width (original, before transforms).
|
||||||
|
native_width: u32,
|
||||||
|
/// Native height (original, before transforms).
|
||||||
|
native_height: u32,
|
||||||
|
/// Current transformation state.
|
||||||
|
transform: TransformState,
|
||||||
|
/// Cached handle for rendering.
|
||||||
|
handle: ImageHandle,
|
||||||
|
/// Accumulated fine rotation angle in degrees.
|
||||||
|
fine_rotation_angle: f32,
|
||||||
|
/// Interpolation quality for fine rotation and resize operations.
|
||||||
|
interpolation_quality: InterpolationQuality,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RasterDocument {
|
||||||
|
/// Load a raster document from disk.
|
||||||
|
pub fn open(path: &Path) -> image::ImageResult<Self> {
|
||||||
|
let document = ImageReader::open(path)?.decode()?;
|
||||||
|
let (native_width, native_height) = document.dimensions();
|
||||||
|
let handle = Self::create_image_handle_from_image(&document);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
document,
|
||||||
|
native_width,
|
||||||
|
native_height,
|
||||||
|
transform: TransformState::default(),
|
||||||
|
handle,
|
||||||
|
fine_rotation_angle: 0.0,
|
||||||
|
interpolation_quality: InterpolationQuality::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current pixel dimensions (width, height) after transforms.
|
||||||
|
#[must_use]
|
||||||
|
pub fn dimensions(&self) -> (u32, u32) {
|
||||||
|
self.document.dimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current image handle.
|
||||||
|
#[must_use]
|
||||||
|
pub fn handle(&self) -> ImageHandle {
|
||||||
|
self.handle.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the current document to disk.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn save(&self, path: &Path) -> image::ImageResult<()> {
|
||||||
|
self.document.save(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the underlying `DynamicImage`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn image(&self) -> &DynamicImage {
|
||||||
|
&self.document
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get native dimensions (before transformations).
|
||||||
|
#[must_use]
|
||||||
|
pub fn native_dimensions(&self) -> (u32, u32) {
|
||||||
|
(self.native_width, self.native_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the rendered image (for cropping from screen coordinates).
|
||||||
|
pub fn get_rendered_image(&self) -> &DynamicImage {
|
||||||
|
&self.document
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crop the document to a specified rectangular region (in-place).
|
||||||
|
///
|
||||||
|
/// Coordinates are in pixels relative to the current image dimensions.
|
||||||
|
/// The crop region is clamped to image bounds if it extends beyond.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the crop region is completely outside the image bounds.
|
||||||
|
pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> Result<(), String> {
|
||||||
|
let (img_width, img_height) = self.document.dimensions();
|
||||||
|
|
||||||
|
// Validate crop region
|
||||||
|
if x >= img_width || y >= img_height {
|
||||||
|
return Err(format!(
|
||||||
|
"Crop region ({}, {}) is outside image bounds ({}, {})",
|
||||||
|
x, y, img_width, img_height
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp dimensions to image bounds
|
||||||
|
let crop_width = width.min(img_width - x);
|
||||||
|
let crop_height = height.min(img_height - y);
|
||||||
|
|
||||||
|
if crop_width == 0 || crop_height == 0 {
|
||||||
|
return Err("Crop region has zero width or height".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply crop
|
||||||
|
self.document = self.document.crop_imm(x, y, crop_width, crop_height);
|
||||||
|
|
||||||
|
// Update native dimensions to the cropped size
|
||||||
|
self.native_width = crop_width;
|
||||||
|
self.native_height = crop_height;
|
||||||
|
|
||||||
|
// Reset transformations since we have a new "native" image
|
||||||
|
self.transform = TransformState::default();
|
||||||
|
self.fine_rotation_angle = 0.0;
|
||||||
|
|
||||||
|
// Regenerate handle
|
||||||
|
self.handle = Self::create_image_handle_from_image(&self.document);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Crop the image to the specified rectangle and return as DynamicImage.
|
||||||
|
///
|
||||||
|
/// This does NOT modify the document - it's used for exporting cropped images.
|
||||||
|
pub fn crop_to_image(
|
||||||
|
&self,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<DynamicImage, String> {
|
||||||
|
let (img_width, img_height) = self.document.dimensions();
|
||||||
|
|
||||||
|
// Validate crop region
|
||||||
|
if x >= img_width || y >= img_height {
|
||||||
|
return Err(format!(
|
||||||
|
"Crop rectangle out of bounds: {}x{} at ({}, {}) exceeds image size {}x{}",
|
||||||
|
width, height, x, y, img_width, img_height
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp dimensions to image bounds
|
||||||
|
let crop_width = width.min(img_width - x);
|
||||||
|
let crop_height = height.min(img_height - y);
|
||||||
|
|
||||||
|
if crop_width == 0 || crop_height == 0 {
|
||||||
|
return Err("Crop region has zero width or height".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cropped = self.document.crop_imm(x, y, crop_width, crop_height);
|
||||||
|
Ok(cropped)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract metadata for this raster document.
|
||||||
|
///
|
||||||
|
/// Returns basic metadata (dimensions, format, file size) and EXIF data if available.
|
||||||
|
pub fn extract_meta(&self, path: &Path) -> crate::domain::document::core::metadata::DocumentMeta {
|
||||||
|
use crate::domain::document::core::metadata::{BasicMeta, DocumentMeta, ExifMeta};
|
||||||
|
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let file_path = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
// Detect format from path
|
||||||
|
let format = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_uppercase();
|
||||||
|
|
||||||
|
let color_type = format!("{:?}", self.document.color());
|
||||||
|
|
||||||
|
let basic = BasicMeta {
|
||||||
|
file_name,
|
||||||
|
file_path,
|
||||||
|
format,
|
||||||
|
width: self.native_width,
|
||||||
|
height: self.native_height,
|
||||||
|
file_size,
|
||||||
|
color_type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to extract EXIF data
|
||||||
|
let exif = std::fs::read(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| ExifMeta::from_bytes(&bytes));
|
||||||
|
|
||||||
|
DocumentMeta { basic, exif }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the document to specific dimensions (for format conversion).
|
||||||
|
///
|
||||||
|
/// This is useful for converting images to standard paper formats (A4, US Letter, etc.).
|
||||||
|
pub fn resize_to_format(&mut self, target_width: u32, target_height: u32) {
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
|
||||||
|
let filter = match self.interpolation_quality {
|
||||||
|
InterpolationQuality::Fast => FilterType::Nearest,
|
||||||
|
InterpolationQuality::Balanced => FilterType::Triangle,
|
||||||
|
InterpolationQuality::Best => FilterType::CatmullRom,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.document = self
|
||||||
|
.document
|
||||||
|
.resize_exact(target_width, target_height, filter);
|
||||||
|
self.handle = Self::create_image_handle_from_image(&self.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
fn create_image_handle_from_image(img: &DynamicImage) -> ImageHandle {
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
let pixels = img.to_rgba8().into_raw();
|
||||||
|
ImageHandle::from_rgba(width, height, pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_rotation(img: DynamicImage, rotation: Rotation) -> DynamicImage {
|
||||||
|
use image::imageops::{rotate180, rotate270, rotate90};
|
||||||
|
match rotation {
|
||||||
|
Rotation::None => img,
|
||||||
|
Rotation::Cw90 => DynamicImage::ImageRgba8(rotate90(&img.to_rgba8())),
|
||||||
|
Rotation::Cw180 => DynamicImage::ImageRgba8(rotate180(&img.to_rgba8())),
|
||||||
|
Rotation::Cw270 => DynamicImage::ImageRgba8(rotate270(&img.to_rgba8())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_flip(img: DynamicImage, direction: FlipDirection) -> DynamicImage {
|
||||||
|
use image::imageops::{flip_horizontal, flip_vertical};
|
||||||
|
match direction {
|
||||||
|
FlipDirection::Horizontal => DynamicImage::ImageRgba8(flip_horizontal(&img.to_rgba8())),
|
||||||
|
FlipDirection::Vertical => DynamicImage::ImageRgba8(flip_vertical(&img.to_rgba8())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Trait Implementations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
impl Renderable for RasterDocument {
|
||||||
|
fn render(&mut self, _scale: f64) -> DocResult<RenderOutput> {
|
||||||
|
// Raster images don't re-render at different scales (lossy),
|
||||||
|
// we just return the current handle.
|
||||||
|
let (width, height) = self.dimensions();
|
||||||
|
Ok(RenderOutput {
|
||||||
|
handle: self.handle.clone(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(&self) -> DocumentInfo {
|
||||||
|
DocumentInfo {
|
||||||
|
width: self.native_width,
|
||||||
|
height: self.native_height,
|
||||||
|
format: "Raster".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transformable for RasterDocument {
|
||||||
|
fn rotate(&mut self, rotation: Rotation) {
|
||||||
|
// Extract current rotation in degrees
|
||||||
|
let current_deg = match self.transform.rotation {
|
||||||
|
RotationMode::Standard(r) => r.to_degrees(),
|
||||||
|
RotationMode::Fine(_) => {
|
||||||
|
// If we have fine rotation, reset it and apply standard rotation
|
||||||
|
self.fine_rotation_angle = 0.0;
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_deg = rotation.to_degrees();
|
||||||
|
let diff_deg = (new_deg - current_deg + 360) % 360;
|
||||||
|
|
||||||
|
if diff_deg != 0 {
|
||||||
|
let rotation_to_apply = match diff_deg {
|
||||||
|
90 => Rotation::Cw90,
|
||||||
|
180 => Rotation::Cw180,
|
||||||
|
270 => Rotation::Cw270,
|
||||||
|
_ => unreachable!("Invalid rotation diff: {}", diff_deg),
|
||||||
|
};
|
||||||
|
self.document = Self::apply_rotation(
|
||||||
|
std::mem::replace(&mut self.document, DynamicImage::new_rgb8(1, 1)),
|
||||||
|
rotation_to_apply,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to standard rotation mode
|
||||||
|
self.transform.rotation = RotationMode::Standard(rotation);
|
||||||
|
self.handle = Self::create_image_handle_from_image(&self.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flip(&mut self, direction: FlipDirection) {
|
||||||
|
self.document = Self::apply_flip(
|
||||||
|
std::mem::replace(&mut self.document, DynamicImage::new_rgb8(1, 1)),
|
||||||
|
direction,
|
||||||
|
);
|
||||||
|
match direction {
|
||||||
|
FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h,
|
||||||
|
FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v,
|
||||||
|
}
|
||||||
|
self.handle = Self::create_image_handle_from_image(&self.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_state(&self) -> TransformState {
|
||||||
|
self.transform
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_fine(&mut self, angle_degrees: f32) {
|
||||||
|
use imageproc::geometric_transformations::{rotate_about_center, Interpolation};
|
||||||
|
|
||||||
|
let interpolation = match self.interpolation_quality {
|
||||||
|
InterpolationQuality::Fast => Interpolation::Nearest,
|
||||||
|
InterpolationQuality::Balanced => Interpolation::Bilinear,
|
||||||
|
InterpolationQuality::Best => Interpolation::Bicubic,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to RGBA8 for imageproc
|
||||||
|
let rgba_img = self.document.to_rgba8();
|
||||||
|
|
||||||
|
// Rotate with transparent background
|
||||||
|
let rotated = rotate_about_center(
|
||||||
|
&rgba_img,
|
||||||
|
angle_degrees.to_radians(),
|
||||||
|
interpolation,
|
||||||
|
image::Rgba([255, 255, 255, 0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.document = DynamicImage::ImageRgba8(rotated);
|
||||||
|
self.fine_rotation_angle += angle_degrees;
|
||||||
|
self.transform.rotation = RotationMode::Fine(self.fine_rotation_angle);
|
||||||
|
self.handle = Self::create_image_handle_from_image(&self.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_fine_rotation(&mut self) {
|
||||||
|
self.fine_rotation_angle = 0.0;
|
||||||
|
self.transform.rotation = RotationMode::Standard(Rotation::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_interpolation_quality(&mut self, quality: InterpolationQuality) {
|
||||||
|
self.interpolation_quality = quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue