use anyhow::anyhow;
use clap::{Parser, Subcommand};
use image::codecs::png::PngEncoder;
use librashader::presets::context::ContextItem;
use librashader::presets::{ShaderFeatures, ShaderPreset, ShaderPresetPack, WildcardContext};
use librashader::reflect::cross::{GlslVersion, HlslShaderModel, MslVersion, SpirvCross};
use librashader::reflect::naga::{Naga, NagaLoweringOptions};
use librashader::reflect::semantics::ShaderSemantics;
use librashader::reflect::{CompileShader, FromCompilation, ReflectShader, SpirvCompilation};
use librashader::runtime::Size;
use librashader::{FastHashMap, ShortString};
use librashader_runtime::parameters::RuntimeParameters;
use librashader_test::render::{CommonFrameOptions, RenderTest};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Args, Debug)]
struct PresetArgs {
#[arg(short, long)]
preset: PathBuf,
#[arg(short, long, value_delimiter = ',', num_args = 1..)]
wildcards: Option<Vec<String>>,
}
#[derive(clap::Args, Debug)]
struct ShaderFeatureArgs {
#[arg(long, short = 'd')]
features: Vec<ShaderDefinesEnums>,
}
#[derive(clap::Args, Debug)]
struct RenderArgs {
#[arg(short, long, default_value_t = 0)]
frame: usize,
#[arg(short, long)]
dimensions: Option<String>,
#[arg(long, value_delimiter = ',', num_args = 1..)]
params: Option<Vec<String>>,
#[arg(long)]
passes_enabled: Option<usize>,
#[arg(short, long)]
image: PathBuf,
#[clap(flatten)]
options: Option<FrameOptionsArgs>,
}
impl From<FrameOptionsArgs> for CommonFrameOptions {
fn from(value: FrameOptionsArgs) -> Self {
Self {
clear_history: false,
frame_direction: value.frame_direction,
rotation: value.rotation,
total_subframes: value.total_subframes,
current_subframe: value.current_subframe,
aspect_ratio: value.aspect_ratio.unwrap_or(0.0),
frametime_delta: value.frametime_delta.unwrap_or(0),
frames_per_second: value.frames_per_second.unwrap_or(1.0),
..Default::default()
}
}
}
impl From<ShaderFeatureArgs> for ShaderFeatures {
fn from(value: ShaderFeatureArgs) -> Self {
let mut features = ShaderFeatures::NONE;
for feature in value.features {
if matches!(feature, ShaderDefinesEnums::FrametimeUniforms) {
features |= ShaderFeatures::FRAMETIME_UNIFORMS;
}
if matches!(feature, ShaderDefinesEnums::OriginalAspectUniforms) {
features |= ShaderFeatures::ORIGINAL_ASPECT_UNIFORMS;
}
}
features
}
}
#[derive(clap::Args, Debug)]
struct FrameOptionsArgs {
#[arg(long, default_value_t = 1, allow_hyphen_values = true)]
pub frame_direction: i32,
#[arg(long, default_value_t = 0)]
pub rotation: u32,
#[arg(long, default_value_t = 1)]
pub total_subframes: u32,
#[arg(long, default_value_t = 1)]
pub current_subframe: u32,
#[arg(long)]
pub aspect_ratio: Option<f32>,
#[arg(long)]
pub frames_per_second: Option<f32>,
#[arg(long)]
pub frametime_delta: Option<u32>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Render {
#[clap(flatten)]
preset: PresetArgs,
#[clap(flatten)]
render: RenderArgs,
#[arg(short, long)]
out: PathBuf,
#[arg(value_enum, short, long)]
runtime: Runtime,
},
Compare {
#[clap(flatten)]
preset: PresetArgs,
#[clap(flatten)]
render: RenderArgs,
#[arg(value_enum, short, long)]
left: Runtime,
#[arg(value_enum, short, long)]
right: Runtime,
#[arg(short, long)]
out: Option<PathBuf>,
},
Parse {
#[clap(flatten)]
preset: PresetArgs,
#[clap(flatten)]
flags: ShaderFeatureArgs,
},
Pack {
#[clap(flatten)]
preset: PresetArgs,
#[clap(flatten)]
flags: ShaderFeatureArgs,
#[arg(short, long)]
out: PathBuf,
#[arg(value_enum, short, long)]
format: PackFormat,
#[arg(value_enum, short, long, default_value_t = PackShaderLanguage::GLSL)]
language: PackShaderLanguage,
},
Preprocess {
#[arg(short, long)]
shader: PathBuf,
#[arg(value_enum, short, long)]
output: PreprocessOutput,
#[clap(flatten)]
flags: ShaderFeatureArgs,
},
Transpile {
#[arg(short, long)]
shader: PathBuf,
#[arg(value_enum, short = 'o', long)]
stage: TranspileStage,
#[arg(value_enum, short, long)]
format: TranspileFormat,
#[arg(short, long)]
version: Option<String>,
#[clap(flatten)]
flags: ShaderFeatureArgs,
},
Reflect {
#[clap(flatten)]
preset: PresetArgs,
#[clap(flatten)]
flags: ShaderFeatureArgs,
#[arg(short, long)]
index: usize,
#[arg(value_enum, short, long, default_value = "cross")]
backend: ReflectionBackend,
},
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum PreprocessOutput {
#[clap(name = "fragment")]
Fragment,
#[clap(name = "vertex")]
Vertex,
#[clap(name = "params")]
Params,
#[clap(name = "passformat")]
Format,
#[clap(name = "json")]
Json,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum TranspileStage {
#[clap(name = "fragment")]
Fragment,
#[clap(name = "vertex")]
Vertex,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum TranspileFormat {
#[clap(name = "glsl")]
GLSL,
#[clap(name = "hlsl")]
HLSL,
#[clap(name = "wgsl")]
WGSL,
#[clap(name = "msl")]
MSL,
#[clap(name = "spirv")]
SPIRV,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum PackFormat {
#[clap(name = "json")]
JSON,
#[clap(name = "msgpack")]
MsgPack,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum PackShaderLanguage {
#[clap(name = "glsl")]
GLSL,
#[clap(name = "wgsl")]
WGSL,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum Runtime {
#[cfg(feature = "opengl")]
#[clap(name = "opengl3")]
OpenGL3,
#[cfg(feature = "opengl")]
#[clap(name = "opengl4")]
OpenGL4,
#[cfg(feature = "vulkan")]
#[clap(name = "vulkan")]
Vulkan,
#[cfg(feature = "wgpu")]
#[clap(name = "wgpu")]
Wgpu,
#[cfg(all(windows, feature = "d3d9"))]
#[clap(name = "d3d9")]
Direct3D9,
#[cfg(all(windows, feature = "d3d11"))]
#[clap(name = "d3d11")]
Direct3D11,
#[cfg(all(windows, feature = "d3d12"))]
#[clap(name = "d3d12")]
Direct3D12,
#[cfg(all(target_vendor = "apple", feature = "metal"))]
#[clap(name = "metal")]
Metal,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ShaderDefinesEnums {
#[clap(name = "originalaspect-uniforms")]
OriginalAspectUniforms,
#[clap(name = "frametime-uniforms")]
FrametimeUniforms,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ReflectionBackend {
#[clap(name = "cross")]
SpirvCross,
#[clap(name = "naga")]
Naga,
}
macro_rules! get_runtime {
($rt:ident, $image:ident) => {
match $rt {
#[cfg(feature = "opengl")]
Runtime::OpenGL3 => &mut librashader_test::render::gl::OpenGl3::new($image.as_path())?,
#[cfg(feature = "opengl")]
Runtime::OpenGL4 => &mut librashader_test::render::gl::OpenGl4::new($image.as_path())?,
#[cfg(feature = "vulkan")]
Runtime::Vulkan => &mut librashader_test::render::vk::Vulkan::new($image.as_path())?,
#[cfg(feature = "wgpu")]
Runtime::Wgpu => &mut librashader_test::render::wgpu::Wgpu::new($image.as_path())?,
#[cfg(all(windows, feature = "d3d9"))]
Runtime::Direct3D9 => {
&mut librashader_test::render::d3d9::Direct3D9::new($image.as_path())?
}
#[cfg(all(windows, feature = "d3d11"))]
Runtime::Direct3D11 => {
&mut librashader_test::render::d3d11::Direct3D11::new($image.as_path())?
}
#[cfg(all(windows, feature = "d3d12"))]
Runtime::Direct3D12 => {
&mut librashader_test::render::d3d12::Direct3D12::new($image.as_path())?
}
#[cfg(all(target_vendor = "apple", feature = "metal"))]
Runtime::Metal => &mut librashader_test::render::mtl::Metal::new($image.as_path())?,
}
};
}
pub fn main() -> Result<(), anyhow::Error> {
let args = Args::parse();
match args.command {
Commands::Render {
preset,
render,
out,
runtime,
} => {
let PresetArgs { preset, wildcards } = preset;
let RenderArgs {
frame,
dimensions,
params,
passes_enabled,
image,
options,
} = render;
let test: &mut dyn RenderTest = get_runtime!(runtime, image);
let dimensions = parse_dimension(dimensions, test.image_size())?;
let mut features = ShaderFeatures::NONE;
if options
.as_ref()
.is_some_and(|args| args.aspect_ratio.is_some())
{
features |= ShaderFeatures::ORIGINAL_ASPECT_UNIFORMS;
}
if options
.as_ref()
.is_some_and(|args| args.frames_per_second.is_some())
|| options
.as_ref()
.is_some_and(|args| args.frametime_delta.is_some())
{
features |= ShaderFeatures::FRAMETIME_UNIFORMS;
}
let preset = get_shader_preset(preset, wildcards, features)?;
let params = parse_params(params)?;
let image = test.render_with_preset_and_params(
preset,
frame,
Some(dimensions),
Some(&|rp| set_params(rp, ¶ms, passes_enabled)),
options.map(CommonFrameOptions::from),
)?;
if out.as_path() == Path::new("-") {
let out = std::io::stdout();
image.write_with_encoder(PngEncoder::new(out))?;
} else {
image.save(out)?;
}
}
Commands::Compare {
preset,
render,
left,
right,
out,
} => {
let PresetArgs { preset, wildcards } = preset;
let RenderArgs {
frame,
dimensions,
params,
passes_enabled,
image,
options,
} = render;
let left: &mut dyn RenderTest = get_runtime!(left, image);
let right: &mut dyn RenderTest = get_runtime!(right, image);
let mut features = ShaderFeatures::NONE;
if options
.as_ref()
.is_some_and(|args| args.aspect_ratio.is_some())
{
features |= ShaderFeatures::ORIGINAL_ASPECT_UNIFORMS;
}
if options
.as_ref()
.is_some_and(|args| args.frames_per_second.is_some())
|| options
.as_ref()
.is_some_and(|args| args.frametime_delta.is_some())
{
features |= ShaderFeatures::FRAMETIME_UNIFORMS;
}
let dimensions = parse_dimension(dimensions, left.image_size())?;
let params = parse_params(params)?;
let left_preset = get_shader_preset(preset.clone(), wildcards.clone(), features)?;
let left_image = left.render_with_preset_and_params(
left_preset,
frame,
Some(dimensions),
Some(&|rp| set_params(rp, ¶ms, passes_enabled)),
None,
)?;
let right_preset = get_shader_preset(preset.clone(), wildcards.clone(), features)?;
let right_image = right.render_with_preset_and_params(
right_preset,
frame,
Some(dimensions),
Some(&|rp| set_params(rp, ¶ms, passes_enabled)),
options.map(CommonFrameOptions::from),
)?;
let similarity = image_compare::rgba_hybrid_compare(&left_image, &right_image)?;
print!("{}", similarity.score);
if let Some(out) = out {
let image = similarity.image.to_color_map();
if out.as_path() == Path::new("-") {
let out = std::io::stdout();
image.write_with_encoder(PngEncoder::new(out))?;
} else {
image.save(out)?;
}
}
}
Commands::Parse { preset, flags } => {
let PresetArgs { preset, wildcards } = preset;
let preset = get_shader_preset(preset, wildcards, flags.into())?;
let out = serde_json::to_string_pretty(&preset)?;
print!("{out:}");
}
Commands::Preprocess {
shader,
output,
flags,
} => {
let source =
librashader::preprocess::ShaderSource::load(shader.as_path(), flags.into())?;
match output {
PreprocessOutput::Fragment => print!("{}", source.fragment),
PreprocessOutput::Vertex => print!("{}", source.vertex),
PreprocessOutput::Params => {
print!("{}", serde_json::to_string_pretty(&source.parameters)?)
}
PreprocessOutput::Format => print!("{:?}", source.format),
PreprocessOutput::Json => print!("{}", serde_json::to_string_pretty(&source)?),
}
}
Commands::Transpile {
shader,
stage,
format,
version,
flags,
} => {
let source =
librashader::preprocess::ShaderSource::load(shader.as_path(), flags.into())?;
let compilation = SpirvCompilation::try_from(&source)?;
let output = match format {
TranspileFormat::GLSL => {
let mut compilation =
librashader::reflect::targets::GLSL::from_compilation(compilation)?;
compilation.validate()?;
let version = version
.map(|s| parse_glsl_version(&s))
.unwrap_or(Ok(GlslVersion::Glsl330))?;
let output = compilation.compile(version)?;
TranspileOutput {
vertex: output.vertex,
fragment: output.fragment,
}
}
TranspileFormat::HLSL => {
let mut compilation =
librashader::reflect::targets::HLSL::from_compilation(compilation)?;
compilation.validate()?;
let shader_model = version
.map(|s| parse_hlsl_version(&s))
.unwrap_or(Ok(HlslShaderModel::ShaderModel5_0))?;
let output = compilation.compile(Some(shader_model))?;
TranspileOutput {
vertex: output.vertex,
fragment: output.fragment,
}
}
TranspileFormat::WGSL => {
let mut compilation =
librashader::reflect::targets::WGSL::from_compilation(compilation)?;
compilation.validate()?;
let output = compilation.compile(NagaLoweringOptions {
write_pcb_as_ubo: true,
sampler_bind_group: 1,
suppress_derivative_uniformity: false,
})?;
TranspileOutput {
vertex: output.vertex,
fragment: output.fragment,
}
}
TranspileFormat::MSL => {
let mut compilation =
<librashader::reflect::targets::MSL as FromCompilation<
SpirvCompilation,
SpirvCross,
>>::from_compilation(compilation)?;
compilation.validate()?;
let version = version
.map(|s| parse_msl_version(&s))
.unwrap_or(Ok(MslVersion::new(1, 2, 0)))?;
let output = compilation.compile(Some(version))?;
TranspileOutput {
vertex: output.vertex,
fragment: output.fragment,
}
}
TranspileFormat::SPIRV => {
let mut compilation =
<librashader::reflect::targets::SPIRV as FromCompilation<
SpirvCompilation,
SpirvCross,
>>::from_compilation(compilation)?;
compilation.validate()?;
let output = compilation.compile(None)?;
let raw = version.is_some_and(|s| s == "raw-id");
TranspileOutput {
vertex: spirv_to_dis(output.vertex, raw)?,
fragment: spirv_to_dis(output.fragment, raw)?,
}
}
};
let print = match stage {
TranspileStage::Fragment => output.fragment,
TranspileStage::Vertex => output.vertex,
};
print!("{print}")
}
Commands::Reflect {
preset,
flags,
index,
backend,
} => {
let PresetArgs { preset, wildcards } = preset;
let preset = get_shader_preset(preset, wildcards, flags.into())?;
let Some(shader) = preset.passes.get(index) else {
return Err(anyhow!("Invalid pass index for the preset"));
};
let source = librashader::preprocess::ShaderSource::load(
shader.path.as_path(),
preset.features,
)?;
let compilation = SpirvCompilation::try_from(&source)?;
let semantics =
ShaderSemantics::create_pass_semantics::<anyhow::Error>(&preset, index)?;
let reflection = match backend {
ReflectionBackend::SpirvCross => {
let mut compilation =
<librashader::reflect::targets::SPIRV as FromCompilation<
SpirvCompilation,
SpirvCross,
>>::from_compilation(compilation)?;
compilation.reflect(index, &semantics)?
}
ReflectionBackend::Naga => {
let mut compilation =
<librashader::reflect::targets::SPIRV as FromCompilation<
SpirvCompilation,
Naga,
>>::from_compilation(compilation)?;
compilation.reflect(index, &semantics)?
}
};
print!("{}", serde_json::to_string_pretty(&reflection)?);
}
Commands::Pack {
preset,
flags,
out,
format,
language,
} => {
let PresetArgs { preset, wildcards } = preset;
let preset = get_shader_preset(preset, wildcards, flags.into())?;
let mut preset = ShaderPresetPack::load_from_preset::<anyhow::Error>(preset)?;
if matches!(language, PackShaderLanguage::WGSL) {
for pass in preset.passes.iter_mut() {
let compilation = SpirvCompilation::try_from(&pass.data)?;
let mut wgsl =
librashader::reflect::targets::WGSL::from_compilation(compilation)?;
wgsl.validate()?;
let output = wgsl.compile(NagaLoweringOptions {
write_pcb_as_ubo: false,
sampler_bind_group: 1,
suppress_derivative_uniformity: false,
})?;
pass.data.vertex = output.vertex;
pass.data.fragment = output.fragment;
}
preset.language = librashader::presets::ShaderSourceLanguage::Wgsl;
}
let output_bytes = match format {
PackFormat::JSON => serde_json::to_vec_pretty(&preset)?,
PackFormat::MsgPack => rmp_serde::to_vec(&preset)?,
};
if out.as_path() == Path::new("-") {
let mut out = std::io::stdout();
out.write_all(output_bytes.as_slice())?;
} else {
let mut file = File::create(out.as_path())?;
file.write_all(output_bytes.as_slice())?;
}
}
}
Ok(())
}
struct TranspileOutput {
vertex: String,
fragment: String,
}
fn get_shader_preset(
preset: PathBuf,
wildcards: Option<Vec<String>>,
flags: ShaderFeatures,
) -> anyhow::Result<ShaderPreset> {
let mut context = WildcardContext::new();
context.add_path_defaults(preset.as_path());
if let Some(wildcards) = wildcards {
for string in wildcards {
let Some((left, right)) = string.split_once("=") else {
return Err(anyhow!("Encountered invalid context string {string}"));
};
context.append_item(ContextItem::ExternContext(
left.to_string(),
right.to_string(),
))
}
}
let preset = ShaderPreset::try_parse_with_context(preset, flags, context)?;
Ok(preset)
}
fn parse_params(
assignments: Option<Vec<String>>,
) -> anyhow::Result<Option<FastHashMap<ShortString, f32>>> {
let Some(assignments) = assignments else {
return Ok(None);
};
let mut map = FastHashMap::default();
for string in assignments {
let Some((left, right)) = string.split_once("=") else {
return Err(anyhow!("Encountered invalid parameter string {string}"));
};
let value = right
.parse::<f32>()
.map_err(|_| anyhow!("Encountered invalid parameter value: {right}"))?;
map.insert(ShortString::from(left), value);
}
Ok(Some(map))
}
fn set_params(
params: &RuntimeParameters,
assignments: &Option<FastHashMap<ShortString, f32>>,
passes_enabled: Option<usize>,
) {
if let Some(passes_enabled) = passes_enabled {
params.set_passes_enabled(passes_enabled)
};
let Some(assignments) = assignments else {
return;
};
params.update_parameters(|params| {
for (key, param) in assignments {
params.insert(key.clone(), *param);
}
});
}
fn spirv_to_dis(spirv: Vec<u32>, raw: bool) -> anyhow::Result<String> {
let binary = spq_spvasm::SpirvBinary::from(spirv);
spq_spvasm::Disassembler::new()
.print_header(true)
.name_ids(!raw)
.name_type_ids(!raw)
.name_const_ids(!raw)
.indent(true)
.disassemble(&binary)
}
fn parse_glsl_version(version_str: &str) -> anyhow::Result<GlslVersion> {
if version_str.contains("es") {
let Some(version) = version_str.strip_suffix("es").map(|s| s.trim()) else {
return Err(anyhow!("Unknown GLSL version"));
};
Ok(match version {
"100" => GlslVersion::Glsl100Es,
"300" => GlslVersion::Glsl300Es,
"310" => GlslVersion::Glsl310Es,
"320" => GlslVersion::Glsl320Es,
_ => return Err(anyhow!("Unknown GLSL version")),
})
} else {
Ok(match version_str {
"100" => GlslVersion::Glsl100Es,
"110" => GlslVersion::Glsl110,
"120" => GlslVersion::Glsl120,
"130" => GlslVersion::Glsl130,
"140" => GlslVersion::Glsl140,
"150" => GlslVersion::Glsl150,
"300" => GlslVersion::Glsl300Es,
"330" => GlslVersion::Glsl330,
"310" => GlslVersion::Glsl310Es,
"320" => GlslVersion::Glsl320Es,
"400" => GlslVersion::Glsl400,
"410" => GlslVersion::Glsl410,
"420" => GlslVersion::Glsl420,
"430" => GlslVersion::Glsl430,
"440" => GlslVersion::Glsl440,
"450" => GlslVersion::Glsl450,
"460" => GlslVersion::Glsl460,
_ => return Err(anyhow!("Unknown GLSL version")),
})
}
}
fn version_to_usize(version_str: &str) -> anyhow::Result<usize> {
let version: &str = if version_str.contains("_") {
&version_str.replace("_", "")
} else if version_str.contains(".") {
&version_str.replace(".", "")
} else {
version_str
};
let version = version
.parse::<usize>()
.map_err(|_| anyhow!("Invalid version string"))?;
Ok(version)
}
fn parse_hlsl_version(version_str: &str) -> anyhow::Result<HlslShaderModel> {
let version = version_to_usize(version_str)?;
Ok(match version {
30 => HlslShaderModel::ShaderModel3_0,
40 => HlslShaderModel::ShaderModel4_0,
50 => HlslShaderModel::ShaderModel5_0,
51 => HlslShaderModel::ShaderModel5_1,
60 => HlslShaderModel::ShaderModel6_0,
61 => HlslShaderModel::ShaderModel6_1,
62 => HlslShaderModel::ShaderModel6_2,
63 => HlslShaderModel::ShaderModel6_3,
64 => HlslShaderModel::ShaderModel6_4,
65 => HlslShaderModel::ShaderModel6_5,
66 => HlslShaderModel::ShaderModel6_6,
67 => HlslShaderModel::ShaderModel6_7,
68 => HlslShaderModel::ShaderModel6_8,
_ => return Err(anyhow!("Unknown Shader Model")),
})
}
fn parse_msl_version(version_str: &str) -> anyhow::Result<MslVersion> {
let version = version_to_usize(version_str)?;
Ok(match version {
10 => MslVersion::new(1, 0, 0),
11 => MslVersion::new(1, 1, 0),
12 => MslVersion::new(1, 2, 0),
20 => MslVersion::new(2, 0, 0),
21 => MslVersion::new(2, 1, 0),
22 => MslVersion::new(2, 2, 0),
23 => MslVersion::new(2, 3, 0),
24 => MslVersion::new(2, 4, 0),
30 => MslVersion::new(3, 0, 0),
31 => MslVersion::new(3, 1, 0),
32 => MslVersion::new(3, 2, 0),
n if n >= 10000 => {
let major = n / 10000;
let minor = (n - (major * 10000)) / 100;
let patch = n - ((major * 10000) + (minor * 100));
MslVersion::new(major as u32, minor as u32, patch as u32)
}
_ => return Err(anyhow!("Unknown MSL version")),
})
}
fn parse_dimension(dimstr: Option<String>, image_dim: Size<u32>) -> anyhow::Result<Size<u32>> {
let Some(dimstr) = dimstr else {
return Ok(image_dim);
};
if dimstr.contains("x") {
if let Some((Ok(width), Ok(height))) = dimstr
.split_once("x")
.map(|(width, height)| (width.parse::<u32>(), height.parse::<u32>()))
{
if width < 1 || height < 1 {
return Err(anyhow!("Dimensions must be larger than 1x1"));
}
if width > 16384 || height > 16384 {
return Err(anyhow!("Dimensions must not be larger than 16384x16384"));
}
return Ok(Size::new(width, height));
}
}
if dimstr.ends_with("%") && dimstr.len() > 1 {
if let Ok(percent) = dimstr.trim_end_matches("%").parse::<u32>() {
let percent = percent as f32 / 100f32;
let width = (image_dim.width as f32 * percent) as u32;
let height = (image_dim.height as f32 * percent) as u32;
if width < 1 || height < 1 {
return Err(anyhow!("Dimensions must be larger than 1x1"));
}
if width > 16384 || height > 16384 {
return Err(anyhow!("Dimensions must not be larger than 16384x16384"));
}
return Ok(Size { width, height });
}
}
Err(anyhow!(
"Invalid dimension syntax, must either in form WIDTHxHEIGHT or SCALE%"
))
}