parent
94a358f3a9
commit
4920795b36
@ -0,0 +1,286 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
|
/// Scales everything (as best as it can) to a different user unit, without breaking the semantics
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if args.input == args.output {
|
||||||
|
println!("Warning: you should **not** set `output` to the same file as `input`, as this program may very well break your svg!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = std::fs::File::open(&args.input).unwrap_or_else(|err| {
|
||||||
|
panic!("Error while reading {}: {}", args.input.display(), err);
|
||||||
|
});
|
||||||
|
let mut element = Element::parse(file).expect("Couldn't parse SVG!");
|
||||||
|
|
||||||
|
let view_box = get_viewbox(&element);
|
||||||
|
|
||||||
|
let scale = if let Some(width) = args.width {
|
||||||
|
width / view_box.2
|
||||||
|
} else if let Some(height) = args.height {
|
||||||
|
height / view_box.3
|
||||||
|
} else {
|
||||||
|
panic!("Expected either width or height as arguments");
|
||||||
|
};
|
||||||
|
|
||||||
|
rescale(&mut element, scale);
|
||||||
|
|
||||||
|
*element.attributes.get_mut("viewBox").unwrap() = format!(
|
||||||
|
"{} {} {} {}",
|
||||||
|
view_box.0 * scale,
|
||||||
|
view_box.1 * scale,
|
||||||
|
view_box.2 * scale,
|
||||||
|
view_box.3 * scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut s: Vec<u8> = Vec::new();
|
||||||
|
element.write(&mut s).expect("Couldn't export SVG!");
|
||||||
|
std::fs::write(&args.output, s).unwrap_or_else(|err| {
|
||||||
|
panic!("Error while writing {}: {}", args.output.display(), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_viewbox(element: &Element) -> (f64, f64, f64, f64) {
|
||||||
|
let view_box = element
|
||||||
|
.attributes
|
||||||
|
.get("viewBox")
|
||||||
|
.expect("missing viewBox attribute on svg");
|
||||||
|
let view_box: Vec<f64> = view_box
|
||||||
|
.split(" ")
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.parse().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if view_box.len() != 4 {
|
||||||
|
panic!(
|
||||||
|
"Parse error: expected viewBox to be four space-separated numbers, got {} numbers",
|
||||||
|
view_box.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(view_box[0], view_box[1], view_box[2], view_box[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescale(element: &mut Element, scale: f64) {
|
||||||
|
if element.name == "path" {
|
||||||
|
if let Some(path) = element.attributes.get_mut("d") {
|
||||||
|
let mut new_path = String::new();
|
||||||
|
for instruction in path.split_whitespace() {
|
||||||
|
if let [Ok(left), Ok(right)] = instruction
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.parse::<f64>())
|
||||||
|
.collect::<Vec<_>>()[..]
|
||||||
|
{
|
||||||
|
write!(&mut new_path, "{},{} ", left * scale, right * scale).unwrap();
|
||||||
|
} else if let Ok(number) = instruction.parse::<f64>() {
|
||||||
|
write!(&mut new_path, "{} ", number * scale).unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut new_path, "{} ", instruction).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*path = new_path;
|
||||||
|
}
|
||||||
|
} else if element.name == "image" {
|
||||||
|
if let Some(width) = element.attributes.get_mut("width") {
|
||||||
|
if let Ok(parsed) = width.parse::<f64>() {
|
||||||
|
*width = (parsed * scale).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(height) = element.attributes.get_mut("height") {
|
||||||
|
if let Ok(parsed) = height.parse::<f64>() {
|
||||||
|
*height = (parsed * scale).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if element.name == "namedview" {
|
||||||
|
if let Some(units) = element.attributes.get_mut("document-units") {
|
||||||
|
*units = "px".to_string();
|
||||||
|
}
|
||||||
|
} else if element.name == "ellipse" || element.name == "radialGradient" || element.name == "linearGradient" {
|
||||||
|
const PROPS: [&'static str; 11] = [
|
||||||
|
"cx",
|
||||||
|
"cy",
|
||||||
|
"rx",
|
||||||
|
"ry",
|
||||||
|
"fx",
|
||||||
|
"fy",
|
||||||
|
"r",
|
||||||
|
"x1",
|
||||||
|
"x2",
|
||||||
|
"y1",
|
||||||
|
"y2"
|
||||||
|
];
|
||||||
|
|
||||||
|
for prop in PROPS {
|
||||||
|
if let Some(attr) = element.attributes.get_mut(prop) {
|
||||||
|
if let Ok(parsed) = attr.parse::<f64>() {
|
||||||
|
*attr = (parsed * scale).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = element.attributes.get_mut("style") {
|
||||||
|
let mut new_style = String::new();
|
||||||
|
for instruction in style.split(';') {
|
||||||
|
match instruction.split(':').map(|x| x.trim()).collect::<Vec<_>>()[..] {
|
||||||
|
["stroke-width", width] => {
|
||||||
|
let (width, unit): (String, String) =
|
||||||
|
width.chars().partition(|x| !x.is_ascii_alphabetic());
|
||||||
|
let width = width.parse::<f64>().unwrap();
|
||||||
|
|
||||||
|
write!(&mut new_style, "stroke-width: {}{};", width * scale, unit).unwrap();
|
||||||
|
}
|
||||||
|
ref any => write!(&mut new_style, "{};", any.join(":")).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*style = new_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with Option::or if and when Polonius
|
||||||
|
// or another successor to the current borrow-checker gets merged
|
||||||
|
let mut transform = element.attributes.get_mut("transform");
|
||||||
|
if transform.is_none() {
|
||||||
|
transform = element.attributes.get_mut("gradientTransform");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(transform) = transform {
|
||||||
|
let mut new_transform = String::new();
|
||||||
|
|
||||||
|
for (instruction, args) in parse_transform(transform) {
|
||||||
|
match instruction.trim() {
|
||||||
|
"rotate" => {
|
||||||
|
let parsed: Vec<f64> = args
|
||||||
|
.split(|c| matches!(c, ' ' | ','))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|x| x.parse().unwrap())
|
||||||
|
.collect();
|
||||||
|
if parsed.len() == 3 {
|
||||||
|
write!(
|
||||||
|
&mut new_transform,
|
||||||
|
"rotate({} {} {})",
|
||||||
|
parsed[0],
|
||||||
|
parsed[1] * scale,
|
||||||
|
parsed[2] * scale
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut new_transform, "rotate({})", args).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"translate" => {
|
||||||
|
let parsed: Vec<f64> = args
|
||||||
|
.split(|c| matches!(c, ' ' | ','))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|x| x.parse().unwrap())
|
||||||
|
.collect();
|
||||||
|
if parsed.len() == 2 {
|
||||||
|
write!(
|
||||||
|
&mut new_transform,
|
||||||
|
"translate({} {})",
|
||||||
|
parsed[0] * scale,
|
||||||
|
parsed[1] * scale
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut new_transform, "translate({})", args).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"matrix" => {
|
||||||
|
let parsed: Vec<f64> = args
|
||||||
|
.split(|c| matches!(c, ' ' | ','))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|x| x.parse().unwrap())
|
||||||
|
.collect();
|
||||||
|
if parsed.len() == 6 {
|
||||||
|
write!(
|
||||||
|
&mut new_transform,
|
||||||
|
"matrix({} {} {} {} {} {})",
|
||||||
|
parsed[0],
|
||||||
|
parsed[1],
|
||||||
|
parsed[2],
|
||||||
|
parsed[3],
|
||||||
|
parsed[4] * scale,
|
||||||
|
parsed[5] * scale,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
write!(&mut new_transform, "matrix({})", args).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
write!(&mut new_transform, "{}({})", instruction, args).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_transform.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: transform-origin
|
||||||
|
|
||||||
|
*transform = new_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in element.children.iter_mut() {
|
||||||
|
if let XMLNode::Element(ref mut child) = child {
|
||||||
|
rescale(child, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_transform(raw: &str) -> Vec<(String, String)> {
|
||||||
|
let mut res: Vec<(String, String)> = Vec::new();
|
||||||
|
let mut instruction = String::new();
|
||||||
|
let mut args = String::new();
|
||||||
|
let mut depth = 0;
|
||||||
|
|
||||||
|
for c in raw.chars() {
|
||||||
|
if c == '(' {
|
||||||
|
depth += 1;
|
||||||
|
if depth > 1 {
|
||||||
|
args.push(c);
|
||||||
|
}
|
||||||
|
} else if c == ')' {
|
||||||
|
depth -= 1;
|
||||||
|
assert!(depth >= 0);
|
||||||
|
if depth >= 1 {
|
||||||
|
args.push(c);
|
||||||
|
}
|
||||||
|
} else if c == ' ' {
|
||||||
|
if depth == 0 {
|
||||||
|
let instruction = std::mem::take(&mut instruction);
|
||||||
|
let args = std::mem::take(&mut args);
|
||||||
|
res.push((instruction, args));
|
||||||
|
} else {
|
||||||
|
args.push(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if depth == 0 {
|
||||||
|
instruction.push(c);
|
||||||
|
} else {
|
||||||
|
args.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push((instruction, args));
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[clap(value_parser)]
|
||||||
|
input: PathBuf,
|
||||||
|
|
||||||
|
#[clap(value_parser)]
|
||||||
|
output: PathBuf,
|
||||||
|
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
width: Option<f64>,
|
||||||
|
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
height: Option<f64>,
|
||||||
|
}
|
Loading…
Reference in new issue