Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/graphics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ mod test_data;
pub fn convert_svg_to_png(
svg_bytes: Option<Vec<u8>>,
badge_style: BadgeStyle,
pixel_ratio: u8,
) -> Result<Vec<u8>, SvgToPngConversionError> {
raster::convert_svg_to_png(svg_bytes, badge_style, LetterSpacingSvgProcessor::new())
raster::convert_svg_to_png(
svg_bytes,
badge_style,
LetterSpacingSvgProcessor::new(),
pixel_ratio,
)
}
9 changes: 5 additions & 4 deletions src/graphics/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ pub enum SvgToPngConversionError {
}

// Returns tuple with (width, height)
fn get_dimensions(renderer: &CairoRenderer) -> (f64, f64) {
fn get_dimensions(renderer: &CairoRenderer, pixel_ratio: u8) -> (f64, f64) {
let IntrinsicDimensions {
width: width_dim,
height: height_dim,
..
} = renderer.intrinsic_dimensions();
let width = width_dim.map_or(0f64, |w| w.length);
let height = height_dim.map_or(0f64, |h| h.length);
let width = width_dim.map_or(0f64, |w| w.length * pixel_ratio as f64);
let height = height_dim.map_or(0f64, |h| h.length * pixel_ratio as f64);

(width, height)
}
Expand All @@ -54,6 +54,7 @@ pub(super) fn convert_svg_to_png<S: SvgProcessor>(
svg_bytes: Option<Vec<u8>>,
badge_style: BadgeStyle,
svg_processor: S,
pixel_ratio: u8,
) -> Result<Vec<u8>, SvgToPngConversionError> {
use SvgToPngConversionError::*;
let stream =
Expand All @@ -63,7 +64,7 @@ pub(super) fn convert_svg_to_png<S: SvgProcessor>(
.map_err(|_| SvgHandleCreationFailure)?;

let renderer = CairoRenderer::new(&handle);
let (width, height) = get_dimensions(&renderer);
let (width, height) = get_dimensions(&renderer, pixel_ratio);
let surface = ImageSurface::create(cairo::Format::ARgb32, width as i32, height as i32)
.map_err(|_| ImageSurfaceCreationFailure)?;

Expand Down
12 changes: 6 additions & 6 deletions src/graphics/raster_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,27 @@ mod get_dimensions_tests {
height: None,
vbox: None,
});
let (width, height) = get_dimensions(&mock_renderer);
let (width, height) = get_dimensions(&mock_renderer, 1);
assert_eq!(width, 0f64);
assert_eq!(height, 0f64);
}

#[test]
fn provides_correct_width() {
let exp_width = 70.33f64;
let input_width = 70.33f64;
let mut mock_renderer = MockCairoRenderer::default();
mock_renderer
.expect_intrinsic_dimensions()
.return_const(IntrinsicDimensions {
width: Some(Length {
length: exp_width,
length: input_width,
unit: LengthUnit::Px,
}),
height: None,
vbox: None,
});
let (width, height) = get_dimensions(&mock_renderer);
assert_eq!(width, exp_width);
let (width, height) = get_dimensions(&mock_renderer, 2);
assert_eq!(width, input_width * 2f64);
assert_eq!(height, 0f64);
}

Expand All @@ -56,7 +56,7 @@ mod get_dimensions_tests {
}),
vbox: None,
});
let (width, height) = get_dimensions(&mock_renderer);
let (width, height) = get_dimensions(&mock_renderer, 1);
assert_eq!(width, 0f64);
assert_eq!(height, exp_height);
}
Expand Down
2 changes: 1 addition & 1 deletion src/graphics/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub static INVALID_SVG: &[u8] = br##"<?xml version="1.0" encoding="UTF-8"?>

lazy_static! {
pub static ref INVALID_SVG_BADGE: Vec<u8> = {
match convert_svg_to_png(None, BadgeStyle::Unspecified) {
match convert_svg_to_png(None, BadgeStyle::Unspecified, 1) {
Ok(png) => png,
Err(_) => {
// This happens during server initialization, so if it fails
Expand Down
79 changes: 63 additions & 16 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const FORWARDING_REQUEST_HEADERS: &[&str] =
&["if-modified-since", "if-unmodified-since", "if-none-match"];
const FORWARDING_RESPONSE_HEADERS: &[&str] = &["date", "cache-control", "expires", "last-modified"];

const MIN_PIXEL_RATIO: u8 = 2;
const MAX_PIXEL_RATIO: u8 = 4;

fn get_badge_style(req: &Request<Body>) -> BadgeStyle {
req.uri().query().map_or(BadgeStyle::Unspecified, |q| {
form_urlencoded::parse(q.as_bytes())
Expand All @@ -41,16 +44,47 @@ fn get_badge_style(req: &Request<Body>) -> BadgeStyle {
})
}

fn extract_pixel_ratio(query: Option<&str>) -> (u8, Option<String>) {
// If a valid `pixel_ratio` param is present, return it along with the query
// string with `pixel_ratio` removed (so it's not forwarded upstream).
// Otherwise return the default pixel ratio of 1 along with the remaining
// query string. The returned query is `None` when nothing is left to forward.
query.map_or((1, None), |q| {
let mut pixel_ratio = 1;
let mut serializer = form_urlencoded::Serializer::new(String::new());
let mut has_other = false;
for (k, v) in form_urlencoded::parse(q.as_bytes()) {
if k == "pixel_ratio" {
if let Ok(pr) = v.parse::<u8>() {
if (MIN_PIXEL_RATIO..=MAX_PIXEL_RATIO).contains(&pr) {
pixel_ratio = pr;
}
}
} else {
serializer.append_pair(&k, &v);
has_other = true;
}
}
let new_query = if has_other {
Some(serializer.finish())
} else {
None
};
(pixel_ratio, new_query)
})
}

async fn get_svg(
mut req: Request<Body>,
http_client: Client,
svg_base_url: &'static str,
) -> Result<(HeaderMap, u16, Vec<u8>, BadgeStyle), ()> {
let svg_url = if let Some(path_and_query) = req.uri().path_and_query() {
let suffix = path_and_query.as_str().replace(".png", ".svg");
Cow::Owned(format!("{}{}", svg_base_url, suffix))
} else {
Cow::Borrowed(svg_base_url)
) -> Result<(HeaderMap, u16, Vec<u8>, BadgeStyle, u8), ()> {
let (pixel_ratio, forwarded_query) = extract_pixel_ratio(req.uri().query());

let svg_path = req.uri().path().replace(".png", ".svg");
let svg_url = match forwarded_query {
Some(query) => format!("{}{}?{}", svg_base_url, svg_path, query),
None => format!("{}{}", svg_base_url, svg_path),
};

let mut headers = HeaderMap::new();
Expand Down Expand Up @@ -83,7 +117,13 @@ async fn get_svg(
return Err(());
}
};
Ok((headers, status, bytes.to_vec(), get_badge_style(&req)))
Ok((
headers,
status,
bytes.to_vec(),
get_badge_style(&req),
pixel_ratio,
))
}
Err(e) => {
eprintln!("Failed to fetch SVG data. Details: {:?}", e);
Expand All @@ -99,9 +139,11 @@ async fn rasterize(
invalid_svg_badge: &'static [u8],
include_body: bool,
) -> Result<Response<Body>, hyper::http::Error> {
let (mut svg_res_headers, svg_status, svg_bytes, badge_style) =
let (mut svg_res_headers, svg_status, svg_bytes, badge_style, pixel_ratio) =
match get_svg(req, http_client, svg_base_url).await {
Ok((headers, status, data, style)) => (headers, status, data, style),
Ok((headers, status, data, style, pixel_ratio)) => {
(headers, status, data, style, pixel_ratio)
}
Err(_) => {
return Response::builder()
.status(502)
Expand All @@ -124,13 +166,14 @@ async fn rasterize(
return res.status(304).body(Body::empty());
}

let (png_stream, res_status) = match convert_svg_to_png(Some(svg_bytes), badge_style) {
Ok(png_stream) => (Cow::Owned(png_stream), 200),
Err(e) => {
eprintln!("Failed to convert SVG to PNG. Details: {:?}", e);
(Cow::Borrowed(invalid_svg_badge), 502)
}
};
let (png_stream, res_status) =
match convert_svg_to_png(Some(svg_bytes), badge_style, pixel_ratio) {
Ok(png_stream) => (Cow::Owned(png_stream), 200),
Err(e) => {
eprintln!("Failed to convert SVG to PNG. Details: {:?}", e);
(Cow::Borrowed(invalid_svg_badge), 502)
}
};

let body = Body::from(png_stream);

Expand Down Expand Up @@ -191,3 +234,7 @@ pub(crate) async fn start_server(
.await?;
Ok(())
}

#[cfg(test)]
#[path = "server_test.rs"]
mod tests;
72 changes: 72 additions & 0 deletions src/server_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#[cfg(test)]
mod extract_pixel_ratio_tests {
use crate::server::extract_pixel_ratio;

#[test]
fn none() {
assert_eq!(extract_pixel_ratio(None), (1, None));
}

#[test]
fn empty() {
assert_eq!(extract_pixel_ratio(Some("")), (1, None));
}

#[test]
fn different_param() {
assert_eq!(
extract_pixel_ratio(Some("foo=bar")),
(1, Some("foo=bar".to_string()))
);
}

#[test]
fn empty_param() {
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=")), (1, None));
}

#[test]
fn not_an_int() {
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=foo")), (1, None));
}

#[test]
fn not_a_whole_number() {
// A decimal parses neither as u8 nor to a valid ratio, so it falls back
// to 1 (and is still stripped from the forwarded query).
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=2.5")), (1, None));
}

#[test]
fn normal_case() {
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=2")), (2, None));
}

#[test]
fn over_max() {
// Out-of-range ratios fall back to 1 rather than clamping to MAX_PIXEL_RATIO.
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=5")), (1, None));
}

#[test]
fn under_min() {
// Out-of-range ratios fall back to 1 rather than clamping to MIN_PIXEL_RATIO.
assert_eq!(extract_pixel_ratio(Some("pixel_ratio=0")), (1, None));
}

#[test]
fn strips_pixel_ratio_keeps_others() {
assert_eq!(
extract_pixel_ratio(Some("style=flat&pixel_ratio=3&label=hi")),
(3, Some("style=flat&label=hi".to_string()))
);
}

#[test]
fn invalid_pixel_ratio_is_still_stripped() {
assert_eq!(
extract_pixel_ratio(Some("pixel_ratio=99&style=flat")),
(1, Some("style=flat".to_string()))
);
}
}