mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 06:48:11 +08:00
Raw-rs: Flip and rotate image based on camera orientation (#1954)
* add Orientation tag * fix errors * Apply the transformation to the image * Fix BASE_PATH in raw-rs tests * Create output folder if it doesn't exist * Nit --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
70dacac474
commit
d9ae01bf90
@@ -1,7 +1,7 @@
|
|||||||
use crate::tiff::file::TiffRead;
|
use crate::tiff::file::TiffRead;
|
||||||
use crate::tiff::tags::SonyDataOffset;
|
use crate::tiff::tags::SonyDataOffset;
|
||||||
use crate::tiff::Ifd;
|
use crate::tiff::Ifd;
|
||||||
use crate::{RawImage, SubtractBlack};
|
use crate::{RawImage, SubtractBlack, Transform};
|
||||||
|
|
||||||
use bitstream_io::{BitRead, BitReader, Endianness, BE};
|
use bitstream_io::{BitRead, BitReader, Endianness, BE};
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
@@ -26,6 +26,7 @@ pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage
|
|||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
maximum: (1 << 12) - 1,
|
maximum: (1 << 12) - 1,
|
||||||
black: SubtractBlack::None,
|
black: SubtractBlack::None,
|
||||||
|
transform: Transform::Horizontal,
|
||||||
camera_model: None,
|
camera_model: None,
|
||||||
camera_white_balance_multiplier: None,
|
camera_white_balance_multiplier: None,
|
||||||
white_balance_multiplier: None,
|
white_balance_multiplier: None,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::tiff::file::{Endian, TiffRead};
|
|||||||
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
||||||
use crate::tiff::values::CurveLookupTable;
|
use crate::tiff::values::CurveLookupTable;
|
||||||
use crate::tiff::{Ifd, TiffError};
|
use crate::tiff::{Ifd, TiffError};
|
||||||
use crate::{RawImage, SubtractBlack};
|
use crate::{RawImage, SubtractBlack, Transform};
|
||||||
|
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
use tag_derive::Tag;
|
use tag_derive::Tag;
|
||||||
@@ -50,6 +50,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
|
|||||||
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
||||||
maximum: (1 << 14) - 1,
|
maximum: (1 << 14) - 1,
|
||||||
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
|
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
|
||||||
|
transform: Transform::Horizontal,
|
||||||
camera_model: None,
|
camera_model: None,
|
||||||
camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
||||||
white_balance_multiplier: None,
|
white_balance_multiplier: None,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::tiff::file::TiffRead;
|
use crate::tiff::file::TiffRead;
|
||||||
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
||||||
use crate::tiff::{Ifd, TiffError};
|
use crate::tiff::{Ifd, TiffError};
|
||||||
use crate::{RawImage, SubtractBlack};
|
use crate::{RawImage, SubtractBlack, Transform};
|
||||||
|
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
use tag_derive::Tag;
|
use tag_derive::Tag;
|
||||||
@@ -58,6 +58,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
|
|||||||
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
||||||
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
|
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
|
||||||
black: SubtractBlack::CfaGrid(ifd.black_level),
|
black: SubtractBlack::CfaGrid(ifd.black_level),
|
||||||
|
transform: Transform::Horizontal,
|
||||||
camera_model: None,
|
camera_model: None,
|
||||||
camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
camera_white_balance_multiplier: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
||||||
white_balance_multiplier: None,
|
white_balance_multiplier: None,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ fn linear_demosaic_rggb(raw_image: RawImage) -> Image<u16> {
|
|||||||
data: image,
|
data: image,
|
||||||
width: raw_image.width,
|
width: raw_image.width,
|
||||||
height: raw_image.height,
|
height: raw_image.height,
|
||||||
|
transform: raw_image.transform,
|
||||||
rgb_to_camera: raw_image.rgb_to_camera,
|
rgb_to_camera: raw_image.rgb_to_camera,
|
||||||
histogram: None,
|
histogram: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use crate::metadata::identify::CameraModel;
|
|||||||
|
|
||||||
use tag_derive::Tag;
|
use tag_derive::Tag;
|
||||||
use tiff::file::TiffRead;
|
use tiff::file::TiffRead;
|
||||||
use tiff::tags::{Compression, ImageLength, ImageWidth, StripByteCounts, SubIfd, Tag};
|
use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag};
|
||||||
|
use tiff::values::Transform;
|
||||||
use tiff::{Ifd, TiffError};
|
use tiff::{Ifd, TiffError};
|
||||||
|
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
@@ -26,6 +27,7 @@ pub struct RawImage {
|
|||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub cfa_pattern: [u8; 4],
|
pub cfa_pattern: [u8; 4],
|
||||||
|
pub transform: Transform,
|
||||||
pub maximum: u16,
|
pub maximum: u16,
|
||||||
pub black: SubtractBlack,
|
pub black: SubtractBlack,
|
||||||
pub camera_model: Option<CameraModel>,
|
pub camera_model: Option<CameraModel>,
|
||||||
@@ -42,6 +44,7 @@ pub struct Image<T> {
|
|||||||
/// We can assume this will be 3 for all non-obscure, modern cameras.
|
/// We can assume this will be 3 for all non-obscure, modern cameras.
|
||||||
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> for more information.
|
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> for more information.
|
||||||
pub channels: u8,
|
pub channels: u8,
|
||||||
|
pub transform: Transform,
|
||||||
pub rgb_to_camera: Option<[[f64; 3]; 3]>,
|
pub rgb_to_camera: Option<[[f64; 3]; 3]>,
|
||||||
pub(crate) histogram: Option<[[usize; 0x2000]; 3]>,
|
pub(crate) histogram: Option<[[usize; 0x2000]; 3]>,
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,7 @@ pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
|
|||||||
let ifd = Ifd::new_first_ifd(&mut file)?;
|
let ifd = Ifd::new_first_ifd(&mut file)?;
|
||||||
|
|
||||||
let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap();
|
let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap();
|
||||||
|
let transform = ifd.get_value::<Orientation, _>(&mut file)?;
|
||||||
|
|
||||||
let mut raw_image = if camera_model.model == "DSLR-A100" {
|
let mut raw_image = if camera_model.model == "DSLR-A100" {
|
||||||
decoder::arw1::decode_a100(ifd, &mut file)
|
decoder::arw1::decode_a100(ifd, &mut file)
|
||||||
@@ -78,6 +82,7 @@ pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
|
|||||||
};
|
};
|
||||||
|
|
||||||
raw_image.camera_model = Some(camera_model);
|
raw_image.camera_model = Some(camera_model);
|
||||||
|
raw_image.transform = transform;
|
||||||
|
|
||||||
Ok(raw_image)
|
Ok(raw_image)
|
||||||
}
|
}
|
||||||
@@ -90,6 +95,7 @@ pub fn process_8bit(raw_image: RawImage) -> Image<u8> {
|
|||||||
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
|
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
|
transform: image.transform,
|
||||||
rgb_to_camera: image.rgb_to_camera,
|
rgb_to_camera: image.rgb_to_camera,
|
||||||
histogram: image.histogram,
|
histogram: image.histogram,
|
||||||
}
|
}
|
||||||
@@ -101,6 +107,7 @@ pub fn process_16bit(raw_image: RawImage) -> Image<u16> {
|
|||||||
let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image);
|
let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image);
|
||||||
let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image);
|
let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image);
|
||||||
let image = crate::postprocessing::convert_to_rgb::convert_to_rgb(image);
|
let image = crate::postprocessing::convert_to_rgb::convert_to_rgb(image);
|
||||||
|
let image = crate::postprocessing::transform::transform(image);
|
||||||
crate::postprocessing::gamma_correction::gamma_correction(image)
|
crate::postprocessing::gamma_correction::gamma_correction(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod convert_to_rgb;
|
pub mod convert_to_rgb;
|
||||||
pub mod gamma_correction;
|
pub mod gamma_correction;
|
||||||
|
pub mod transform;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use crate::Image;
|
||||||
|
use crate::Transform;
|
||||||
|
|
||||||
|
pub fn transform(mut image: Image<u16>) -> Image<u16> {
|
||||||
|
if image.transform.is_identity() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
let channels = image.channels as usize;
|
||||||
|
let mut data = vec![0; channels * image.width * image.height];
|
||||||
|
|
||||||
|
let (final_width, final_height) = if image.transform.will_swap_coordinates() {
|
||||||
|
(image.height, image.width)
|
||||||
|
} else {
|
||||||
|
(image.width, image.height)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut initial_index = inverse_transform_index(image.transform, 0, 0, image.width, image.height);
|
||||||
|
let column_step = inverse_transform_index(image.transform, 0, 1, image.width, image.height) as i64 - initial_index as i64;
|
||||||
|
let row_step = inverse_transform_index(image.transform, 1, 0, image.width, image.height) as i64 - inverse_transform_index(image.transform, 0, final_width, image.width, image.height) as i64;
|
||||||
|
|
||||||
|
for row in 0..final_height {
|
||||||
|
for col in 0..final_width {
|
||||||
|
let transformed_index = final_width * row + col;
|
||||||
|
|
||||||
|
let copy_from_range = channels * initial_index..channels * (initial_index + 1);
|
||||||
|
let copy_to_range = channels * transformed_index..channels * (transformed_index + 1);
|
||||||
|
data[copy_to_range].copy_from_slice(&image.data[copy_from_range]);
|
||||||
|
|
||||||
|
initial_index = (initial_index as i64 + column_step) as usize;
|
||||||
|
}
|
||||||
|
initial_index = (initial_index as i64 + row_step) as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.data = data;
|
||||||
|
image.width = final_width;
|
||||||
|
image.height = final_height;
|
||||||
|
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inverse_transform_index(transform: Transform, mut row: usize, mut column: usize, width: usize, height: usize) -> usize {
|
||||||
|
let value = match transform {
|
||||||
|
Transform::Horizontal => 0,
|
||||||
|
Transform::MirrorHorizontal => 1,
|
||||||
|
Transform::Rotate180 => 3,
|
||||||
|
Transform::MirrorVertical => 2,
|
||||||
|
Transform::MirrorHorizontalRotate270 => 4,
|
||||||
|
Transform::Rotate90 => 6,
|
||||||
|
Transform::MirrorHorizontalRotate90 => 7,
|
||||||
|
Transform::Rotate270 => 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
if value & 4 != 0 {
|
||||||
|
std::mem::swap(&mut row, &mut column)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value & 2 != 0 {
|
||||||
|
row = height - 1 - row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if value & 1 != 0 {
|
||||||
|
column = width - 1 - column;
|
||||||
|
}
|
||||||
|
|
||||||
|
width * row + column
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ pub enum TagId {
|
|||||||
Make = 0x10f,
|
Make = 0x10f,
|
||||||
Model = 0x110,
|
Model = 0x110,
|
||||||
StripOffsets = 0x111,
|
StripOffsets = 0x111,
|
||||||
|
Orientation = 0x112,
|
||||||
SamplesPerPixel = 0x115,
|
SamplesPerPixel = 0x115,
|
||||||
RowsPerStrip = 0x116,
|
RowsPerStrip = 0x116,
|
||||||
StripByteCounts = 0x117,
|
StripByteCounts = 0x117,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
|
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
|
||||||
use super::{Ifd, TagId, TiffError, TiffRead};
|
use super::{Ifd, TagId, TiffError, TiffRead};
|
||||||
|
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
@@ -18,6 +18,7 @@ pub struct PhotometricInterpretation;
|
|||||||
pub struct Make;
|
pub struct Make;
|
||||||
pub struct Model;
|
pub struct Model;
|
||||||
pub struct StripOffsets;
|
pub struct StripOffsets;
|
||||||
|
pub struct Orientation;
|
||||||
pub struct SamplesPerPixel;
|
pub struct SamplesPerPixel;
|
||||||
pub struct RowsPerStrip;
|
pub struct RowsPerStrip;
|
||||||
pub struct StripByteCounts;
|
pub struct StripByteCounts;
|
||||||
@@ -89,6 +90,13 @@ impl SimpleTag for StripOffsets {
|
|||||||
const NAME: &'static str = "Strip Offsets";
|
const NAME: &'static str = "Strip Offsets";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SimpleTag for Orientation {
|
||||||
|
type Type = TypeOrientation;
|
||||||
|
|
||||||
|
const ID: TagId = TagId::Orientation;
|
||||||
|
const NAME: &'static str = "Orientation";
|
||||||
|
}
|
||||||
|
|
||||||
impl SimpleTag for SamplesPerPixel {
|
impl SimpleTag for SamplesPerPixel {
|
||||||
type Type = TypeShort;
|
type Type = TypeShort;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
|
|
||||||
use super::file::TiffRead;
|
use super::file::TiffRead;
|
||||||
use super::values::{CurveLookupTable, Rational};
|
use super::values::{CurveLookupTable, Rational, Transform};
|
||||||
use super::{Ifd, IfdTagType, TiffError};
|
use super::{Ifd, IfdTagType, TiffError};
|
||||||
|
|
||||||
pub struct TypeAscii;
|
pub struct TypeAscii;
|
||||||
@@ -357,6 +357,7 @@ impl<T: PrimitiveType, const N: usize> TagType for ConstArray<T, N> {
|
|||||||
|
|
||||||
pub struct TypeString;
|
pub struct TypeString;
|
||||||
pub struct TypeSonyToneCurve;
|
pub struct TypeSonyToneCurve;
|
||||||
|
pub struct TypeOrientation;
|
||||||
|
|
||||||
impl TagType for TypeString {
|
impl TagType for TypeString {
|
||||||
type Output = String;
|
type Output = String;
|
||||||
@@ -378,3 +379,21 @@ impl TagType for TypeSonyToneCurve {
|
|||||||
Ok(CurveLookupTable::from_sony_tone_table(values))
|
Ok(CurveLookupTable::from_sony_tone_table(values))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TagType for TypeOrientation {
|
||||||
|
type Output = Transform;
|
||||||
|
|
||||||
|
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||||
|
Ok(match TypeShort::read(file)? {
|
||||||
|
1 => Transform::Horizontal,
|
||||||
|
2 => Transform::MirrorHorizontal,
|
||||||
|
3 => Transform::Rotate180,
|
||||||
|
4 => Transform::MirrorVertical,
|
||||||
|
5 => Transform::MirrorHorizontalRotate270,
|
||||||
|
6 => Transform::Rotate90,
|
||||||
|
7 => Transform::MirrorHorizontalRotate90,
|
||||||
|
8 => Transform::Rotate270,
|
||||||
|
_ => return Err(TiffError::InvalidValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,3 +50,30 @@ impl CurveLookupTable {
|
|||||||
self.table[x]
|
self.table[x]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Transform {
|
||||||
|
Horizontal,
|
||||||
|
MirrorHorizontal,
|
||||||
|
Rotate180,
|
||||||
|
MirrorVertical,
|
||||||
|
MirrorHorizontalRotate270,
|
||||||
|
Rotate90,
|
||||||
|
MirrorHorizontalRotate90,
|
||||||
|
Rotate270,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transform {
|
||||||
|
pub fn is_identity(&self) -> bool {
|
||||||
|
*self == Transform::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn will_swap_coordinates(&self) -> bool {
|
||||||
|
use Transform as Tr;
|
||||||
|
|
||||||
|
match *self {
|
||||||
|
Tr::Horizontal | Tr::MirrorHorizontal | Tr::Rotate180 | Tr::MirrorVertical => false,
|
||||||
|
Tr::MirrorHorizontalRotate270 | Tr::Rotate90 | Tr::MirrorHorizontalRotate90 | Tr::Rotate270 => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ use image::{ColorType, ImageEncoder};
|
|||||||
use libraw::Processor;
|
use libraw::Processor;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::fs::{read_dir, File};
|
use std::fs::{create_dir, metadata, read_dir, File};
|
||||||
use std::io::{BufWriter, Cursor, Read};
|
use std::io::{BufWriter, Cursor, Read};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"];
|
const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"];
|
||||||
const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/";
|
const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/";
|
||||||
const BASE_PATH: &str = "./tests/images";
|
const BASE_PATH: &str = "./tests/images/";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_images_match_with_libraw() {
|
fn test_images_match_with_libraw() {
|
||||||
@@ -73,6 +73,11 @@ fn store_image(path: &Path, suffix: &str, data: &mut [u8], width: usize, height:
|
|||||||
output_path.push(parent);
|
output_path.push(parent);
|
||||||
}
|
}
|
||||||
output_path.push("output");
|
output_path.push("output");
|
||||||
|
|
||||||
|
if metadata(&output_path).is_err() {
|
||||||
|
create_dir(&output_path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(filename) = path.file_stem() {
|
if let Some(filename) = path.file_stem() {
|
||||||
let new_filename = format!("{}_{}.{}", filename.to_string_lossy(), suffix, "png");
|
let new_filename = format!("{}_{}.{}", filename.to_string_lossy(), suffix, "png");
|
||||||
output_path.push(new_filename);
|
output_path.push(new_filename);
|
||||||
|
|||||||
Reference in New Issue
Block a user