rafx_assets/assets/image/
importer_image.rs

1use crate::assets::image::{
2    ImageAssetColorSpaceConfig, ImageAssetData, ImageAssetDataFormatConfig,
3};
4use crate::schema::{
5    GpuImageAssetAccessor, GpuImageAssetRecord, GpuImageBasisCompressionTypeEnum,
6    GpuImageColorSpaceEnum, GpuImageImportedDataRecord, GpuImageMipGenerationEnum,
7};
8use crate::{
9    ImageAssetBasisCompressionSettings, ImageAssetBasisCompressionType, ImageAssetMipGeneration,
10};
11use hydrate_base::AssetId;
12use hydrate_data::{Record, RecordAccessor, RecordBuilder};
13use hydrate_pipeline::{
14    Builder, BuilderContext, ImportContext, Importer, JobInput, JobOutput, JobProcessor,
15    PipelineResult, RunContext, ScanContext,
16};
17use image::GenericImageView;
18use rafx_api::RafxResourceType;
19use serde::{Deserialize, Serialize};
20use std::panic::RefUnwindSafe;
21use std::path::Path;
22use std::sync::Arc;
23use type_uuid::*;
24
25// Wrapper for image crate's supported formats
26#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
27pub enum ImageFileFormat {
28    Png,
29    Jpeg,
30    Gif,
31    WebP,
32    Pnm,
33    Tiff,
34    Tga,
35    Dds,
36    Bmp,
37    Ico,
38    Hdr,
39    Farbfeld,
40    Avif,
41}
42
43impl From<image::ImageFormat> for ImageFileFormat {
44    fn from(other: image::ImageFormat) -> ImageFileFormat {
45        match other {
46            image::ImageFormat::Png => ImageFileFormat::Png,
47            image::ImageFormat::Jpeg => ImageFileFormat::Jpeg,
48            image::ImageFormat::Gif => ImageFileFormat::Gif,
49            image::ImageFormat::WebP => ImageFileFormat::WebP,
50            image::ImageFormat::Pnm => ImageFileFormat::Pnm,
51            image::ImageFormat::Tiff => ImageFileFormat::Tiff,
52            image::ImageFormat::Tga => ImageFileFormat::Tga,
53            image::ImageFormat::Dds => ImageFileFormat::Dds,
54            image::ImageFormat::Bmp => ImageFileFormat::Bmp,
55            image::ImageFormat::Ico => ImageFileFormat::Ico,
56            image::ImageFormat::Hdr => ImageFileFormat::Hdr,
57            image::ImageFormat::Farbfeld => ImageFileFormat::Farbfeld,
58            image::ImageFormat::Avif => ImageFileFormat::Avif,
59            _ => unimplemented!(),
60        }
61    }
62}
63
64impl Into<image::ImageFormat> for ImageFileFormat {
65    fn into(self) -> image::ImageFormat {
66        match self {
67            ImageFileFormat::Png => image::ImageFormat::Png,
68            ImageFileFormat::Jpeg => image::ImageFormat::Jpeg,
69            ImageFileFormat::Gif => image::ImageFormat::Gif,
70            ImageFileFormat::WebP => image::ImageFormat::WebP,
71            ImageFileFormat::Pnm => image::ImageFormat::Pnm,
72            ImageFileFormat::Tiff => image::ImageFormat::Tiff,
73            ImageFileFormat::Tga => image::ImageFormat::Tga,
74            ImageFileFormat::Dds => image::ImageFormat::Dds,
75            ImageFileFormat::Bmp => image::ImageFormat::Bmp,
76            ImageFileFormat::Ico => image::ImageFormat::Ico,
77            ImageFileFormat::Hdr => image::ImageFormat::Hdr,
78            ImageFileFormat::Farbfeld => image::ImageFormat::Farbfeld,
79            ImageFileFormat::Avif => image::ImageFormat::Avif,
80        }
81    }
82}
83
84#[derive(TypeUuid, Serialize, Deserialize, Clone, Debug)]
85#[uuid = "149d9973-6c02-4bcd-af6b-b7549aa92977"]
86pub struct ImageImporterOptions {
87    pub mip_generation: ImageAssetMipGeneration,
88    pub color_space: ImageAssetColorSpaceConfig,
89    pub data_format: ImageAssetDataFormatConfig,
90}
91
92impl Default for ImageImporterOptions {
93    fn default() -> Self {
94        ImageImporterOptions {
95            mip_generation: ImageAssetMipGeneration::NoMips,
96            color_space: ImageAssetColorSpaceConfig::Linear,
97            data_format: ImageAssetDataFormatConfig::Uncompressed,
98        }
99    }
100}
101
102#[derive(Clone)]
103pub struct ImageImporterRuleOptions {
104    pub mip_generation: ImageAssetMipGeneration,
105    pub color_space: ImageAssetColorSpaceConfig,
106    pub data_format: ImageAssetDataFormatConfig,
107}
108
109pub trait ImageImporterRule: Send + Sync + RefUnwindSafe {
110    fn try_apply(
111        &self,
112        path: &Path,
113    ) -> Option<ImageImporterRuleOptions>;
114}
115
116pub struct ImageImporterRuleFilenameContains {
117    pub search_string: String,
118    pub rule_options: ImageImporterRuleOptions,
119}
120
121impl ImageImporterRule for ImageImporterRuleFilenameContains {
122    fn try_apply(
123        &self,
124        path: &Path,
125    ) -> Option<ImageImporterRuleOptions> {
126        if let Some(file_name) = path.file_name() {
127            if file_name
128                .to_string_lossy()
129                .to_lowercase()
130                .contains(&self.search_string)
131            {
132                return Some(self.rule_options.clone());
133            }
134        }
135
136        None
137    }
138}
139
140pub struct ImageImporterConfig {
141    default: ImageImporterRuleOptions,
142    rules: Vec<Box<dyn ImageImporterRule>>,
143}
144
145impl ImageImporterConfig {
146    pub fn new(default: ImageImporterRuleOptions) -> Self {
147        ImageImporterConfig {
148            default,
149            rules: vec![],
150        }
151    }
152
153    pub fn add_config(
154        &mut self,
155        rule: Box<dyn ImageImporterRule>,
156    ) {
157        self.rules.push(rule)
158    }
159
160    pub fn add_filename_contains_override<S: Into<String>>(
161        &mut self,
162        search_string: S,
163        rule_options: ImageImporterRuleOptions,
164    ) {
165        self.add_config(Box::new(ImageImporterRuleFilenameContains {
166            search_string: search_string.into().to_lowercase(),
167            rule_options,
168        }));
169    }
170}
171
172#[derive(TypeUuid)]
173#[uuid = "e7c83acb-f73b-4b3c-b14d-fe5cc17c0fa3"]
174pub struct GpuImageImporterSimple {
175    pub image_importer_config: Arc<ImageImporterConfig>,
176}
177
178impl GpuImageImporterSimple {
179    fn default_settings(
180        &self,
181        path: &Path,
182    ) -> ImageImporterOptions {
183        for rule in &self.image_importer_config.rules {
184            if let Some(options) = rule.try_apply(path) {
185                log::trace!("FOUND RULE FOR {:?}", path);
186                return ImageImporterOptions {
187                    mip_generation: options.mip_generation,
188                    data_format: options.data_format,
189                    color_space: options.color_space,
190                };
191            }
192        }
193        return ImageImporterOptions {
194            mip_generation: self.image_importer_config.default.mip_generation,
195            data_format: self.image_importer_config.default.data_format,
196            color_space: self.image_importer_config.default.color_space,
197        };
198    }
199
200    pub fn set_default_asset_properties(
201        default_settings: &ImageImporterOptions,
202        asset_record: &mut RecordBuilder<GpuImageAssetRecord>,
203    ) {
204        match default_settings.data_format {
205            ImageAssetDataFormatConfig::Uncompressed => {
206                asset_record.basis_compression().set(false).unwrap();
207            }
208            ImageAssetDataFormatConfig::BasisCompressed(settings) => {
209                asset_record.basis_compression().set(true).unwrap();
210
211                asset_record
212                    .basis_compression_settings()
213                    .compression_type()
214                    .set(match settings.compression_type {
215                        ImageAssetBasisCompressionType::Etc1S => {
216                            GpuImageBasisCompressionTypeEnum::Etc1S
217                        }
218                        ImageAssetBasisCompressionType::Uastc => {
219                            GpuImageBasisCompressionTypeEnum::Uastc
220                        }
221                    })
222                    .unwrap();
223                asset_record
224                    .basis_compression_settings()
225                    .quality()
226                    .set(settings.quality)
227                    .unwrap();
228            }
229        }
230        asset_record
231            .color_space()
232            .set(match default_settings.color_space {
233                ImageAssetColorSpaceConfig::Srgb => GpuImageColorSpaceEnum::Srgb,
234                ImageAssetColorSpaceConfig::Linear => GpuImageColorSpaceEnum::Linear,
235            })
236            .unwrap();
237        asset_record
238            .mip_generation()
239            .set(match default_settings.mip_generation {
240                ImageAssetMipGeneration::NoMips => GpuImageMipGenerationEnum::NoMips,
241                ImageAssetMipGeneration::Precomupted => GpuImageMipGenerationEnum::Precomputed,
242                ImageAssetMipGeneration::Runtime => GpuImageMipGenerationEnum::Runtime,
243            })
244            .unwrap();
245    }
246}
247
248impl Importer for GpuImageImporterSimple {
249    fn supported_file_extensions(&self) -> &[&'static str] {
250        &["png", "jpg", "jpeg", "tga", "tif", "tiff", "bmp"]
251    }
252
253    fn scan_file(
254        &self,
255        context: ScanContext,
256    ) -> PipelineResult<()> {
257        context.add_default_importable::<GpuImageAssetRecord>()?;
258        Ok(())
259    }
260
261    fn import_file(
262        &self,
263        context: ImportContext,
264    ) -> PipelineResult<()> {
265        let (image_bytes, width, height) = {
266            profiling::scope!("Load Image from Disk");
267            let decoded_image = ::image::open(context.path).unwrap();
268            let (width, height) = decoded_image.dimensions();
269            let image_bytes = decoded_image.into_rgba8().to_vec();
270            (image_bytes, width, height)
271        };
272
273        //
274        // Create import data
275        //
276        let import_data = GpuImageImportedDataRecord::new_builder(context.schema_set);
277        import_data
278            .image_bytes()
279            .set(Arc::new(image_bytes))
280            .unwrap();
281        import_data.width().set(width).unwrap();
282        import_data.height().set(height).unwrap();
283
284        //
285        // Create the default asset
286        //
287        let mut default_asset = GpuImageAssetRecord::new_builder(context.schema_set);
288        let default_settings = self.default_settings(context.path);
289
290        GpuImageImporterSimple::set_default_asset_properties(&default_settings, &mut default_asset);
291
292        //
293        // Return the created objects
294        //
295        context
296            .add_default_importable(default_asset.into_inner()?, Some(import_data.into_inner()?));
297        Ok(())
298    }
299}
300
301#[derive(Hash, Serialize, Deserialize)]
302pub struct GpuImageJobInput {
303    pub asset_id: AssetId,
304}
305impl JobInput for GpuImageJobInput {}
306
307#[derive(Serialize, Deserialize)]
308pub struct GpuImageJobOutput {}
309impl JobOutput for GpuImageJobOutput {}
310
311#[derive(Default, TypeUuid)]
312#[uuid = "5311c92e-470e-4fdc-88cd-3abaf1c28f39"]
313pub struct GpuImageJobProcessor;
314
315impl JobProcessor for GpuImageJobProcessor {
316    type InputT = GpuImageJobInput;
317    type OutputT = GpuImageJobOutput;
318
319    fn version(&self) -> u32 {
320        1
321    }
322
323    fn run<'a>(
324        &self,
325        context: &'a RunContext<'a, Self::InputT>,
326    ) -> PipelineResult<GpuImageJobOutput> {
327        //
328        // Read asset properties
329        //
330        let asset_data = context
331            .asset::<GpuImageAssetRecord>(context.input.asset_id)
332            .unwrap();
333        let basis_compression = asset_data.basis_compression().get().unwrap();
334        let color_space = match asset_data.color_space().get().unwrap() {
335            GpuImageColorSpaceEnum::Srgb => ImageAssetColorSpaceConfig::Srgb,
336            GpuImageColorSpaceEnum::Linear => ImageAssetColorSpaceConfig::Linear,
337        };
338        let mip_generation = match asset_data.mip_generation().get().unwrap() {
339            GpuImageMipGenerationEnum::NoMips => ImageAssetMipGeneration::NoMips,
340            GpuImageMipGenerationEnum::Precomputed => ImageAssetMipGeneration::Precomupted,
341            GpuImageMipGenerationEnum::Runtime => ImageAssetMipGeneration::Runtime,
342        };
343
344        let format_config = if basis_compression {
345            let compression_type = match asset_data
346                .basis_compression_settings()
347                .compression_type()
348                .get()
349                .unwrap()
350            {
351                GpuImageBasisCompressionTypeEnum::Uastc => ImageAssetBasisCompressionType::Uastc,
352                GpuImageBasisCompressionTypeEnum::Etc1S => ImageAssetBasisCompressionType::Etc1S,
353            };
354            let quality = asset_data
355                .basis_compression_settings()
356                .quality()
357                .get()
358                .unwrap();
359
360            ImageAssetDataFormatConfig::BasisCompressed(ImageAssetBasisCompressionSettings {
361                compression_type,
362                quality,
363            })
364        } else {
365            ImageAssetDataFormatConfig::Uncompressed
366        };
367
368        //
369        // Read imported data
370        //
371        let import_data = context
372            .imported_data::<GpuImageImportedDataRecord>(context.input.asset_id)
373            .unwrap();
374
375        let image_bytes = import_data.image_bytes().get().unwrap().clone();
376        let width = import_data.width().get().unwrap();
377        let height = import_data.height().get().unwrap();
378
379        //
380        // Create the processed data
381        //
382        let processed_data = ImageAssetData::from_raw_rgba32(
383            width,
384            height,
385            color_space,
386            format_config,
387            mip_generation,
388            RafxResourceType::TEXTURE,
389            &image_bytes,
390        )
391        .unwrap();
392
393        //
394        // Serialize and return
395        //
396        context.produce_default_artifact(context.input.asset_id, processed_data)?;
397
398        Ok(GpuImageJobOutput {})
399    }
400}
401
402#[derive(TypeUuid, Default)]
403#[uuid = "7fe7e10b-6b99-4acc-8bf9-09cc17fedcdf"]
404pub struct GpuImageBuilder {}
405
406impl Builder for GpuImageBuilder {
407    fn asset_type(&self) -> &'static str {
408        GpuImageAssetAccessor::schema_name()
409    }
410
411    fn start_jobs(
412        &self,
413        context: BuilderContext,
414    ) -> PipelineResult<()> {
415        //Future: Might produce jobs per-platform
416        context.enqueue_job::<GpuImageJobProcessor>(
417            context.data_set,
418            context.schema_set,
419            context.job_api,
420            GpuImageJobInput {
421                asset_id: context.asset_id,
422            },
423        )?;
424        Ok(())
425    }
426}