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#[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 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 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 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 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 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 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 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 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}