Skip to main content

warcraft_rs/commands/
blp.rs

1//! BLP texture command implementations
2
3use anyhow::{Context, Result};
4use clap::{Subcommand, ValueEnum};
5use image::{ImageFormat, ImageReader, imageops::FilterType};
6use std::path::{Path, PathBuf};
7use wow_blp::{
8    convert::{
9        AlphaBits, Blp2Format, BlpOldFormat, BlpTarget, DxtAlgorithm, blp_to_image, image_to_blp,
10    },
11    encode::save_blp,
12    parser::load_blp,
13    types::BlpContent,
14};
15
16#[derive(Subcommand)]
17pub enum BlpCommands {
18    /// Display information about a BLP file
19    Info {
20        /// Path to the BLP file
21        file: PathBuf,
22
23        /// Show detailed mipmap information
24        #[arg(long)]
25        mipmaps: bool,
26
27        /// Show raw header data
28        #[arg(long)]
29        raw: bool,
30
31        /// Show compression statistics and ratios
32        #[arg(long)]
33        compression: bool,
34
35        /// Show file size breakdown
36        #[arg(long)]
37        size: bool,
38
39        /// Find best mipmap level for target size
40        #[arg(long)]
41        best_mipmap_for: Option<u32>,
42
43        /// Show all information (equivalent to --mipmaps --compression --size)
44        #[arg(long)]
45        all: bool,
46    },
47
48    /// Validate BLP file integrity
49    Validate {
50        /// Path to the BLP file
51        file: PathBuf,
52
53        /// Strict validation mode
54        #[arg(long)]
55        strict: bool,
56    },
57
58    /// Convert BLP files to/from other image formats
59    Convert {
60        /// Input file path (BLP or other image format)
61        input: PathBuf,
62
63        /// Output file path
64        output: PathBuf,
65
66        /// Input format (auto-detected from extension if not specified)
67        #[arg(short = 'i', long)]
68        input_format: Option<InputFormat>,
69
70        /// Output format (auto-detected from extension if not specified)
71        #[arg(short = 'o', long)]
72        output_format: Option<OutputFormat>,
73
74        /// BLP version to use when encoding to BLP
75        #[arg(long, default_value = "blp1")]
76        blp_version: BlpVersionCli,
77
78        /// BLP encoding format to use
79        #[arg(long, default_value = "jpeg")]
80        blp_format: BlpFormat,
81
82        /// Alpha bits (0, 1, 4, or 8). Auto-detected from input if not specified.
83        #[arg(long)]
84        alpha_bits: Option<u8>,
85
86        /// Mipmap level to extract when converting from BLP
87        #[arg(long, default_value = "0")]
88        mipmap_level: usize,
89
90        /// Skip mipmap generation when encoding to BLP
91        #[arg(long)]
92        no_mipmaps: bool,
93
94        /// Mipmap filtering algorithm
95        #[arg(long, default_value = "lanczos3")]
96        mipmap_filter: MipmapFilter,
97
98        /// DXT compression quality
99        #[arg(long, default_value = "medium")]
100        dxt_compression: DxtCompression,
101    },
102}
103
104#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
105pub enum InputFormat {
106    Blp,
107    Png,
108    Jpeg,
109    Gif,
110    Bmp,
111    Ico,
112    Tiff,
113    Webp,
114    Pnm,
115    Dds,
116    Tga,
117    #[value(name = "openexr")]
118    OpenExr,
119    Farbfeld,
120}
121
122#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
123pub enum OutputFormat {
124    Blp,
125    Png,
126    Jpeg,
127    Gif,
128    Bmp,
129    Ico,
130    Tiff,
131    Pnm,
132    Tga,
133    #[value(name = "openexr")]
134    OpenExr,
135    Farbfeld,
136}
137
138#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
139pub enum BlpVersionCli {
140    Blp0,
141    Blp1,
142    Blp2,
143}
144
145impl From<BlpVersionCli> for wow_blp::types::BlpVersion {
146    fn from(value: BlpVersionCli) -> Self {
147        match value {
148            BlpVersionCli::Blp0 => wow_blp::types::BlpVersion::Blp0,
149            BlpVersionCli::Blp1 => wow_blp::types::BlpVersion::Blp1,
150            BlpVersionCli::Blp2 => wow_blp::types::BlpVersion::Blp2,
151        }
152    }
153}
154
155#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
156pub enum BlpFormat {
157    Raw1,
158    Raw3,
159    Jpeg,
160    Dxt1,
161    Dxt3,
162    Dxt5,
163}
164
165#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
166pub enum MipmapFilter {
167    /// Nearest Neighbor
168    Nearest,
169    /// Linear Filter
170    Triangle,
171    /// Cubic Filter
172    CatmullRom,
173    /// Gaussian Filter
174    Gaussian,
175    /// Lanczos with window 3
176    Lanczos3,
177}
178
179#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
180pub enum DxtCompression {
181    /// Range fit, fast, poor quality
182    Fastest,
183    /// Cluster algorithm, slow, good quality
184    Medium,
185    /// Iterative cluster algorithm, very slow, great quality
186    Finest,
187}
188
189// Conversion implementations
190
191impl From<MipmapFilter> for FilterType {
192    fn from(value: MipmapFilter) -> FilterType {
193        match value {
194            MipmapFilter::Nearest => FilterType::Nearest,
195            MipmapFilter::Triangle => FilterType::Triangle,
196            MipmapFilter::CatmullRom => FilterType::CatmullRom,
197            MipmapFilter::Gaussian => FilterType::Gaussian,
198            MipmapFilter::Lanczos3 => FilterType::Lanczos3,
199        }
200    }
201}
202
203impl From<DxtCompression> for DxtAlgorithm {
204    fn from(value: DxtCompression) -> DxtAlgorithm {
205        match value {
206            DxtCompression::Fastest => DxtAlgorithm::RangeFit,
207            DxtCompression::Medium => DxtAlgorithm::ClusterFit,
208            DxtCompression::Finest => DxtAlgorithm::IterativeClusterFit,
209        }
210    }
211}
212
213impl TryFrom<OutputFormat> for ImageFormat {
214    type Error = anyhow::Error;
215
216    fn try_from(val: OutputFormat) -> Result<ImageFormat> {
217        match val {
218            OutputFormat::Blp => anyhow::bail!("BLP format handled separately"),
219            OutputFormat::Png => Ok(ImageFormat::Png),
220            OutputFormat::Jpeg => Ok(ImageFormat::Jpeg),
221            OutputFormat::Gif => Ok(ImageFormat::Gif),
222            OutputFormat::Bmp => Ok(ImageFormat::Bmp),
223            OutputFormat::Ico => Ok(ImageFormat::Ico),
224            OutputFormat::Tiff => Ok(ImageFormat::Tiff),
225            OutputFormat::Pnm => Ok(ImageFormat::Pnm),
226            OutputFormat::Tga => Ok(ImageFormat::Tga),
227            OutputFormat::OpenExr => Ok(ImageFormat::OpenExr),
228            OutputFormat::Farbfeld => Ok(ImageFormat::Farbfeld),
229        }
230    }
231}
232
233fn guess_input_format(path: &Path) -> Option<InputFormat> {
234    let ext = path.extension()?.to_str()?.to_lowercase();
235    match ext.as_str() {
236        "blp" => Some(InputFormat::Blp),
237        "png" => Some(InputFormat::Png),
238        "jpg" | "jpeg" => Some(InputFormat::Jpeg),
239        "gif" => Some(InputFormat::Gif),
240        "bmp" => Some(InputFormat::Bmp),
241        "ico" => Some(InputFormat::Ico),
242        "tiff" | "tif" => Some(InputFormat::Tiff),
243        "webp" => Some(InputFormat::Webp),
244        "pnm" | "pbm" | "pgm" | "ppm" | "pam" => Some(InputFormat::Pnm),
245        "dds" => Some(InputFormat::Dds),
246        "tga" => Some(InputFormat::Tga),
247        "exr" => Some(InputFormat::OpenExr),
248        "ff" | "farbfeld" => Some(InputFormat::Farbfeld),
249        _ => None,
250    }
251}
252
253fn guess_output_format(path: &Path) -> Option<OutputFormat> {
254    let ext = path.extension()?.to_str()?.to_lowercase();
255    match ext.as_str() {
256        "blp" => Some(OutputFormat::Blp),
257        "png" => Some(OutputFormat::Png),
258        "jpg" | "jpeg" => Some(OutputFormat::Jpeg),
259        "gif" => Some(OutputFormat::Gif),
260        "bmp" => Some(OutputFormat::Bmp),
261        "ico" => Some(OutputFormat::Ico),
262        "tiff" | "tif" => Some(OutputFormat::Tiff),
263        "pnm" | "pbm" | "pgm" | "ppm" | "pam" => Some(OutputFormat::Pnm),
264        "tga" => Some(OutputFormat::Tga),
265        "exr" => Some(OutputFormat::OpenExr),
266        "ff" | "farbfeld" => Some(OutputFormat::Farbfeld),
267        _ => None,
268    }
269}
270
271fn make_blp_target(
272    version: BlpVersionCli,
273    format: BlpFormat,
274    alpha_bits: u8,
275    dxt_algo: DxtCompression,
276) -> Result<BlpTarget> {
277    use wow_blp::types::BlpVersion;
278    let version: BlpVersion = version.into();
279    match version {
280        BlpVersion::Blp0 => match format {
281            BlpFormat::Raw1 => {
282                let alpha_bits = match alpha_bits {
283                    0 => AlphaBits::NoAlpha,
284                    1 => AlphaBits::Bit1,
285                    4 => AlphaBits::Bit4,
286                    8 => AlphaBits::Bit8,
287                    _ => anyhow::bail!("Invalid alpha bits {} for BLP0 Raw1 format", alpha_bits),
288                };
289                Ok(BlpTarget::Blp0(BlpOldFormat::Raw1 { alpha_bits }))
290            }
291            BlpFormat::Jpeg => {
292                let has_alpha = match alpha_bits {
293                    0 => false,
294                    8 => true,
295                    _ => anyhow::bail!(
296                        "Invalid alpha bits {} for BLP0 JPEG format (only 0 or 8 supported)",
297                        alpha_bits
298                    ),
299                };
300                Ok(BlpTarget::Blp0(BlpOldFormat::Jpeg { has_alpha }))
301            }
302            _ => anyhow::bail!("BLP0 only supports Raw1 and JPEG formats"),
303        },
304        BlpVersion::Blp1 => match format {
305            BlpFormat::Raw1 => {
306                let alpha_bits = match alpha_bits {
307                    0 => AlphaBits::NoAlpha,
308                    1 => AlphaBits::Bit1,
309                    4 => AlphaBits::Bit4,
310                    8 => AlphaBits::Bit8,
311                    _ => anyhow::bail!("Invalid alpha bits {} for BLP1 Raw1 format", alpha_bits),
312                };
313                Ok(BlpTarget::Blp1(BlpOldFormat::Raw1 { alpha_bits }))
314            }
315            BlpFormat::Jpeg => {
316                let has_alpha = match alpha_bits {
317                    0 => false,
318                    8 => true,
319                    _ => anyhow::bail!(
320                        "Invalid alpha bits {} for BLP1 JPEG format (only 0 or 8 supported)",
321                        alpha_bits
322                    ),
323                };
324                Ok(BlpTarget::Blp1(BlpOldFormat::Jpeg { has_alpha }))
325            }
326            _ => anyhow::bail!("BLP1 only supports Raw1 and JPEG formats"),
327        },
328        BlpVersion::Blp2 => match format {
329            BlpFormat::Raw1 => {
330                let alpha_bits = match alpha_bits {
331                    0 => AlphaBits::NoAlpha,
332                    1 => AlphaBits::Bit1,
333                    4 => AlphaBits::Bit4,
334                    8 => AlphaBits::Bit8,
335                    _ => anyhow::bail!("Invalid alpha bits {} for BLP2 Raw1 format", alpha_bits),
336                };
337                Ok(BlpTarget::Blp2(Blp2Format::Raw1 { alpha_bits }))
338            }
339            BlpFormat::Raw3 => Ok(BlpTarget::Blp2(Blp2Format::Raw3)),
340            BlpFormat::Jpeg => {
341                let has_alpha = match alpha_bits {
342                    0 => false,
343                    8 => true,
344                    _ => anyhow::bail!(
345                        "Invalid alpha bits {} for BLP2 JPEG format (only 0 or 8 supported)",
346                        alpha_bits
347                    ),
348                };
349                Ok(BlpTarget::Blp2(Blp2Format::Jpeg { has_alpha }))
350            }
351            BlpFormat::Dxt1 => {
352                let has_alpha = match alpha_bits {
353                    0 => false,
354                    1 => true,
355                    _ => anyhow::bail!(
356                        "Invalid alpha bits {} for BLP2 DXT1 format (only 0 or 1 supported)",
357                        alpha_bits
358                    ),
359                };
360                Ok(BlpTarget::Blp2(Blp2Format::Dxt1 {
361                    has_alpha,
362                    compress_algorithm: dxt_algo.into(),
363                }))
364            }
365            BlpFormat::Dxt3 => {
366                let has_alpha = match alpha_bits {
367                    0 => false,
368                    8 => true,
369                    _ => anyhow::bail!(
370                        "Invalid alpha bits {} for BLP2 DXT3 format (only 0 or 8 supported)",
371                        alpha_bits
372                    ),
373                };
374                Ok(BlpTarget::Blp2(Blp2Format::Dxt3 {
375                    has_alpha,
376                    compress_algorithm: dxt_algo.into(),
377                }))
378            }
379            BlpFormat::Dxt5 => {
380                let has_alpha = match alpha_bits {
381                    0 => false,
382                    8 => true,
383                    _ => anyhow::bail!(
384                        "Invalid alpha bits {} for BLP2 DXT5 format (only 0 or 8 supported)",
385                        alpha_bits
386                    ),
387                };
388                Ok(BlpTarget::Blp2(Blp2Format::Dxt5 {
389                    has_alpha,
390                    compress_algorithm: dxt_algo.into(),
391                }))
392            }
393        },
394    }
395}
396
397/// Check if an image has an alpha channel based on its color type
398fn image_has_alpha(image: &image::DynamicImage) -> bool {
399    use image::DynamicImage;
400    matches!(
401        image,
402        DynamicImage::ImageLumaA8(_)
403            | DynamicImage::ImageLumaA16(_)
404            | DynamicImage::ImageRgba8(_)
405            | DynamicImage::ImageRgba16(_)
406            | DynamicImage::ImageRgba32F(_)
407    )
408}
409
410/// Determine appropriate alpha bits based on BLP format and input image
411fn determine_alpha_bits(
412    blp_format: BlpFormat,
413    has_alpha: bool,
414    user_specified: Option<u8>,
415) -> Result<u8> {
416    // If user specified, validate and return
417    if let Some(bits) = user_specified {
418        return Ok(bits);
419    }
420
421    // Auto-detect based on format and input alpha
422    Ok(match blp_format {
423        BlpFormat::Dxt1 => {
424            // DXT1 only supports 0 or 1-bit alpha
425            if has_alpha { 1 } else { 0 }
426        }
427        BlpFormat::Dxt3 | BlpFormat::Dxt5 | BlpFormat::Jpeg => {
428            // These formats support 0 or 8-bit alpha
429            if has_alpha { 8 } else { 0 }
430        }
431        BlpFormat::Raw1 | BlpFormat::Raw3 => {
432            // Raw formats support 0, 1, 4, or 8-bit alpha - use 8 for full quality
433            if has_alpha { 8 } else { 0 }
434        }
435    })
436}
437
438fn convert_blp(args: ConvertArgs) -> Result<()> {
439    // Determine input format
440    let input_format = args
441        .input_format
442        .or_else(|| guess_input_format(&args.input))
443        .context("Failed to determine input format. Please specify with --input-format")?;
444
445    // Determine output format
446    let output_format = args
447        .output_format
448        .or_else(|| guess_output_format(&args.output))
449        .context("Failed to determine output format. Please specify with --output-format")?;
450
451    log::info!("Converting from {input_format:?} to {output_format:?}");
452
453    // Load input image
454    let input_image = if input_format == InputFormat::Blp {
455        let blp_image = load_blp(&args.input)
456            .with_context(|| format!("Failed to load BLP file: {}", args.input.display()))?;
457
458        blp_to_image(&blp_image, args.mipmap_level)
459            .with_context(|| format!("Failed to convert BLP mipmap level {}", args.mipmap_level))?
460    } else {
461        ImageReader::open(&args.input)
462            .with_context(|| format!("Failed to open image file: {}", args.input.display()))?
463            .decode()
464            .with_context(|| format!("Failed to decode image: {}", args.input.display()))?
465    };
466
467    // Save output
468    match output_format {
469        OutputFormat::Blp => {
470            let has_alpha = image_has_alpha(&input_image);
471
472            // Auto-detect or validate alpha bits
473            let alpha_bits = determine_alpha_bits(args.blp_format, has_alpha, args.alpha_bits)?;
474
475            // Provide helpful warning for JPEG format with alpha
476            if args.blp_format == BlpFormat::Jpeg && has_alpha && alpha_bits > 0 {
477                log::info!(
478                    "Input image has alpha channel. JPEG-compressed BLP will store \
479                     alpha separately (not in the JPEG stream)."
480                );
481            }
482
483            log::info!(
484                "Using {} alpha bits (input has alpha: {})",
485                alpha_bits,
486                has_alpha
487            );
488
489            let target = make_blp_target(
490                args.blp_version,
491                args.blp_format,
492                alpha_bits,
493                args.dxt_compression,
494            )?;
495            let blp = image_to_blp(
496                input_image,
497                !args.no_mipmaps,
498                target,
499                args.mipmap_filter.into(),
500            )
501            .context("Failed to convert image to BLP")?;
502
503            save_blp(&blp, &args.output)
504                .with_context(|| format!("Failed to save BLP file: {}", args.output.display()))?;
505        }
506        _ => {
507            let img_format = output_format.try_into()?;
508            input_image
509                .save_with_format(&args.output, img_format)
510                .with_context(|| format!("Failed to save image: {}", args.output.display()))?;
511        }
512    }
513
514    println!(
515        "✓ Converted {} to {}",
516        args.input.display(),
517        args.output.display()
518    );
519    Ok(())
520}
521
522fn show_blp_info(
523    file: PathBuf,
524    show_mipmaps: bool,
525    show_raw: bool,
526    show_compression: bool,
527    show_size: bool,
528    best_mipmap_for: Option<u32>,
529    show_all: bool,
530) -> Result<()> {
531    let blp =
532        load_blp(&file).with_context(|| format!("Failed to load BLP file: {}", file.display()))?;
533
534    // Apply --all flag
535    let show_mipmaps = show_mipmaps || show_all;
536    let show_compression = show_compression || show_all;
537    let show_size = show_size || show_all;
538
539    println!("BLP File Information: {}", file.display());
540    println!("=====================================");
541
542    // Basic info
543    println!("Version: {:?}", blp.header.version);
544    println!("Dimensions: {}x{}", blp.header.width, blp.header.height);
545    println!("Content Type: {:?}", blp.header.content);
546    println!("Compression: {:?}", blp.compression_type());
547    println!("Alpha Bits: {}", blp.alpha_bit_depth());
548    println!("Has Mipmaps: {}", blp.header.has_mipmaps());
549    println!("Image Count: {}", blp.image_count());
550
551    // Content-specific info
552    match &blp.content {
553        BlpContent::Jpeg(jpeg) => {
554            println!("JPEG Header Size: {} bytes", jpeg.header.len());
555        }
556        BlpContent::Raw1(raw) => {
557            println!("Palette Colors: {}", raw.cmap.len());
558        }
559        BlpContent::Raw3(_) => {
560            println!("Format Details: Uncompressed BGRA");
561        }
562        BlpContent::Dxt1(_) => {
563            println!("Format Details: S3TC BC1 (4 bpp)");
564        }
565        BlpContent::Dxt3(_) => {
566            println!("Format Details: S3TC BC2 (8 bpp, explicit alpha)");
567        }
568        BlpContent::Dxt5(_) => {
569            println!("Format Details: S3TC BC3 (8 bpp, interpolated alpha)");
570        }
571    }
572
573    // Compression statistics
574    if show_compression {
575        println!("\nCompression Statistics:");
576        println!("----------------------");
577        let compression_ratio = blp.compression_ratio();
578        println!("Compression Ratio: {compression_ratio:.2}:1");
579        println!(
580            "Compression Efficiency: {:.1}%",
581            (1.0 - 1.0 / compression_ratio) * 100.0
582        );
583    }
584
585    // File size breakdown
586    if show_size {
587        println!("\nFile Size Information:");
588        println!("---------------------");
589        let estimated_size = blp.estimated_file_size();
590        println!(
591            "Estimated File Size: {} bytes ({:.2} KB)",
592            estimated_size,
593            estimated_size as f32 / 1024.0
594        );
595
596        let mipmap_info = blp.mipmap_info();
597        let total_uncompressed = mipmap_info
598            .iter()
599            .map(|info| info.width * info.height * 4)
600            .sum::<u32>();
601        println!(
602            "Uncompressed Size: {} bytes ({:.2} KB)",
603            total_uncompressed,
604            total_uncompressed as f32 / 1024.0
605        );
606    }
607
608    // Best mipmap for target size
609    if let Some(target_size) = best_mipmap_for {
610        println!("\nBest Mipmap for {target_size}x{target_size} target:");
611        println!("-------------------------------");
612        let best_level = blp.best_mipmap_for_size(target_size);
613        let (width, height) = blp.header.mipmap_size(best_level);
614        println!("Best Level: {best_level} ({width}x{height})");
615    }
616
617    // Mipmap info (using new convenience method)
618    if show_mipmaps {
619        println!("\nMipmap Information:");
620        println!("-------------------");
621        let mipmap_info = blp.mipmap_info();
622        for info in &mipmap_info {
623            println!(
624                "  Level {}: {}x{} ({} bytes, {} pixels)",
625                info.level, info.width, info.height, info.data_size, info.pixel_count
626            );
627        }
628    }
629
630    // Raw header data
631    if show_raw {
632        println!("\nRaw Header Data:");
633        println!("----------------");
634        println!("  Version: {:?}", blp.header.version);
635        println!("  Content Tag: {:?}", blp.header.content);
636        println!("  Flags: {:?}", blp.header.flags);
637        println!("  Width: {}", blp.header.width);
638        println!("  Height: {}", blp.header.height);
639        println!("  Mipmap Locator: {:?}", blp.header.mipmap_locator);
640    }
641
642    Ok(())
643}
644
645fn validate_blp(file: PathBuf, strict: bool) -> Result<()> {
646    println!("Validating BLP file: {}", file.display());
647
648    // Try to load the file
649    let blp = match load_blp(&file) {
650        Ok(blp) => blp,
651        Err(e) => {
652            println!("✗ Failed to load BLP file: {e}");
653            return Err(e.into());
654        }
655    };
656
657    let mut errors: Vec<String> = Vec::new();
658    let mut warnings: Vec<String> = Vec::new();
659
660    // Check version
661    log::debug!("BLP version: {:?}", blp.header.version);
662
663    // Check dimensions
664    if blp.header.width == 0 || blp.header.height == 0 {
665        errors.push("Invalid dimensions (0 width or height)".to_string());
666    }
667
668    if !blp.header.width.is_power_of_two() || !blp.header.height.is_power_of_two() {
669        if strict {
670            errors.push("Dimensions are not powers of 2".to_string());
671        } else {
672            warnings.push("Dimensions are not powers of 2 (non-standard but may work)".to_string());
673        }
674    }
675
676    // Check mipmap consistency
677    if blp.header.has_mipmaps() {
678        let expected_levels = (blp.header.width.max(blp.header.height) as f32).log2() as usize + 1;
679        let actual_levels = blp.image_count();
680
681        if actual_levels < expected_levels {
682            warnings.push(format!(
683                "Incomplete mipmap chain: expected {expected_levels} levels, got {actual_levels}"
684            ));
685        }
686    }
687
688    // Format-specific validation
689    match &blp.content {
690        BlpContent::Jpeg(jpeg) => {
691            // JPEG-specific validations
692            if jpeg.header.is_empty() {
693                errors.push("JPEG header is empty".to_string());
694            }
695        }
696        BlpContent::Dxt1(_) | BlpContent::Dxt3(_) | BlpContent::Dxt5(_) => {
697            // DXT requires dimensions to be multiples of 4
698            if blp.header.width % 4 != 0 || blp.header.height % 4 != 0 {
699                errors.push("DXT format requires dimensions to be multiples of 4".to_string());
700            }
701        }
702        _ => {}
703    }
704
705    // Print results
706    if errors.is_empty() && warnings.is_empty() {
707        println!("✓ BLP file is valid");
708        Ok(())
709    } else {
710        if !errors.is_empty() {
711            println!("\nErrors:");
712            for error in &errors {
713                println!("  ✗ {error}");
714            }
715        }
716
717        if !warnings.is_empty() {
718            println!("\nWarnings:");
719            for warning in &warnings {
720                println!("  ⚠ {warning}");
721            }
722        }
723
724        if errors.is_empty() {
725            println!("\n✓ BLP file is valid with warnings");
726            Ok(())
727        } else {
728            anyhow::bail!("BLP file validation failed with {} error(s)", errors.len())
729        }
730    }
731}
732
733// Helper struct for convert arguments
734struct ConvertArgs {
735    input: PathBuf,
736    output: PathBuf,
737    input_format: Option<InputFormat>,
738    output_format: Option<OutputFormat>,
739    blp_version: BlpVersionCli,
740    blp_format: BlpFormat,
741    alpha_bits: Option<u8>,
742    mipmap_level: usize,
743    no_mipmaps: bool,
744    mipmap_filter: MipmapFilter,
745    dxt_compression: DxtCompression,
746}
747
748pub fn execute(command: BlpCommands) -> Result<()> {
749    match command {
750        BlpCommands::Convert {
751            input,
752            output,
753            input_format,
754            output_format,
755            blp_version,
756            blp_format,
757            alpha_bits,
758            mipmap_level,
759            no_mipmaps,
760            mipmap_filter,
761            dxt_compression,
762        } => convert_blp(ConvertArgs {
763            input,
764            output,
765            input_format,
766            output_format,
767            blp_version,
768            blp_format,
769            alpha_bits,
770            mipmap_level,
771            no_mipmaps,
772            mipmap_filter,
773            dxt_compression,
774        }),
775        BlpCommands::Info {
776            file,
777            mipmaps,
778            raw,
779            compression,
780            size,
781            best_mipmap_for,
782            all,
783        } => show_blp_info(file, mipmaps, raw, compression, size, best_mipmap_for, all),
784        BlpCommands::Validate { file, strict } => validate_blp(file, strict),
785    }
786}