From 22cab6103ef249c87989f75a3522a5f93ab2ac26 Mon Sep 17 00:00:00 2001 From: Shark Date: Mon, 19 Aug 2024 12:55:06 +0200 Subject: [PATCH] add `Fetcher` which automatically expands urls --- crates/gosub_net/src/http.rs | 1 + crates/gosub_net/src/http/fetcher.rs | 58 +++++++++++++++++++++++ crates/gosub_net/src/http/headers.rs | 16 +++++-- crates/gosub_net/src/http/request.rs | 12 ++--- crates/gosub_net/src/http/response.rs | 60 +++++++++++++++++++++++- crates/gosub_render_backend/src/svg.rs | 7 ++- crates/gosub_renderer/src/draw.rs | 37 +++++++-------- crates/gosub_renderer/src/draw/img.rs | 39 +++++++-------- crates/gosub_renderer/src/render_tree.rs | 5 +- crates/gosub_svg/src/resvg.rs | 25 +++++++--- crates/gosub_vello/src/vello_svg.rs | 10 +++- src/engine.rs | 2 +- 12 files changed, 211 insertions(+), 61 deletions(-) create mode 100644 crates/gosub_net/src/http/fetcher.rs diff --git a/crates/gosub_net/src/http.rs b/crates/gosub_net/src/http.rs index 5d53d779a..acad616b3 100644 --- a/crates/gosub_net/src/http.rs +++ b/crates/gosub_net/src/http.rs @@ -1,5 +1,6 @@ pub use ureq; +pub mod fetcher; pub mod headers; pub mod request; pub mod response; diff --git a/crates/gosub_net/src/http/fetcher.rs b/crates/gosub_net/src/http/fetcher.rs new file mode 100644 index 000000000..ffc76baa1 --- /dev/null +++ b/crates/gosub_net/src/http/fetcher.rs @@ -0,0 +1,58 @@ +use super::response::Response; +use crate::http::request::Request; +use anyhow::bail; +use gosub_shared::types::Result; +use url::{ParseError, Url}; + +pub struct Fetcher { + base_url: Url, + client: ureq::Agent, +} + +impl Fetcher { + pub fn new(base: Url) -> Self { + Self { + base_url: base, + client: ureq::Agent::new(), + } + } + + pub fn get_url(&self, url: &Url) -> Result { + let scheme = url.scheme(); + + let resp = if scheme == "http" || scheme == "https" { + let response = self.client.get(url.as_str()).call()?; + + response.try_into()? + } else if scheme == "file" { + let path = url.path(); + let body = std::fs::read(path)?; + + Response::from(body) + } else { + bail!("Unsupported scheme") + }; + + Ok(resp) + } + + pub fn get(&self, url: &str) -> Result { + let url = self.parse_url(url)?; + + self.get_url(&url) + } + + pub fn get_req(&self, _url: &Request) { + todo!() + } + + fn parse_url(&self, url: &str) -> Result { + let mut parsed_url = Url::parse(url); + + if parsed_url == Err(ParseError::RelativeUrlWithoutBase) { + parsed_url = self.base_url.join(url); + } + + Ok(parsed_url?) + } +} diff --git a/crates/gosub_net/src/http/headers.rs b/crates/gosub_net/src/http/headers.rs index c209731f7..fa5a36228 100644 --- a/crates/gosub_net/src/http/headers.rs +++ b/crates/gosub_net/src/http/headers.rs @@ -12,10 +12,20 @@ impl Headers { } } - pub fn set(&mut self, key: &str, value: &str) { + pub fn with_capacity(capacity: usize) -> Headers { + Headers { + headers: HashMap::with_capacity(capacity), + } + } + + pub fn set_str(&mut self, key: &str, value: &str) { self.headers.insert(key.to_string(), value.to_string()); } + pub fn set(&mut self, key: String, value: String) { + self.headers.insert(key, value); + } + pub fn get(&self, key: &str) -> Option<&String> { self.headers.get(key) } @@ -40,10 +50,10 @@ mod tests { fn test_headers() { let mut headers = Headers::new(); - headers.set("Content-Type", "application/json"); + headers.set_str("Content-Type", "application/json"); assert_eq!(headers.get("Content-Type").unwrap(), "application/json"); - headers.set("Content-Type", "text/html"); + headers.set_str("Content-Type", "text/html"); assert_eq!(headers.get("Content-Type").unwrap(), "text/html"); assert_eq!(headers.all().len(), 1); } diff --git a/crates/gosub_net/src/http/request.rs b/crates/gosub_net/src/http/request.rs index a516419d1..b25ac4b72 100644 --- a/crates/gosub_net/src/http/request.rs +++ b/crates/gosub_net/src/http/request.rs @@ -62,11 +62,11 @@ mod tests { req.headers(Headers::new()); req.cookies(CookieJar::new()); - req.headers.set("Content-Type", "application/json"); + req.headers.set_str("Content-Type", "application/json"); req.cookies.add(Cookie::new("qux", "wok")); req.cookies.add(Cookie::new("foo", "bar")); - req.headers.set("Accept", "text/html"); - req.headers.set("Accept-Encoding", "gzip, deflate, br"); + req.headers.set_str("Accept", "text/html"); + req.headers.set_str("Accept-Encoding", "gzip, deflate, br"); assert_eq!(req.method, "GET"); assert_eq!(req.uri, "/"); @@ -83,9 +83,9 @@ mod tests { req.cookies.add(Cookie::new("foo", "bar")); req.cookies.add(Cookie::new("qux", "wok")); - req.headers.set("Content-Type", "application/json"); - req.headers.set("Accept", "text/html"); - req.headers.set("Accept-Encoding", "gzip, deflate, br"); + req.headers.set_str("Content-Type", "application/json"); + req.headers.set_str("Accept", "text/html"); + req.headers.set_str("Accept-Encoding", "gzip, deflate, br"); let s = format!("{}", req); assert_eq!(s, "GET / HTTP/1.1\nHeaders:\n Accept: text/html\n Accept-Encoding: gzip, deflate, br\n Content-Type: application/json\nCookies:\n foo=bar\n qux=wok\nBody: 0 bytes\n"); diff --git a/crates/gosub_net/src/http/response.rs b/crates/gosub_net/src/http/response.rs index ee327a434..f1ec46251 100644 --- a/crates/gosub_net/src/http/response.rs +++ b/crates/gosub_net/src/http/response.rs @@ -1,6 +1,7 @@ use crate::http::headers::Headers; use core::fmt::{Display, Formatter}; use std::collections::HashMap; +use std::io::Read; #[derive(Debug)] pub struct Response { @@ -23,6 +24,63 @@ impl Response { body: vec![], } } + + pub fn is_ok(&self) -> bool { + self.status >= 200 && self.status < 300 + } +} + +impl TryFrom for Response { + type Error = anyhow::Error; + + fn try_from(value: ureq::Response) -> std::result::Result { + let mut body = Vec::with_capacity( + value + .header("Content-Length") + .map(|s| s.parse().unwrap_or(0)) + .unwrap_or(0), + ); + + let mut this = Self { + status: value.status(), + status_text: value.status_text().to_string(), + version: value.http_version().to_string(), + headers: get_headers(&value), + body, + cookies: Default::default(), + }; + + value.into_reader().read_to_end(&mut this.body)?; + + Ok(this) + } +} + +impl From> for Response { + fn from(body: Vec) -> Self { + Self { + status: 200, + status_text: "OK".to_string(), + version: "HTTP/1.1".to_string(), + headers: Default::default(), + cookies: Default::default(), + body, + } + } +} + +fn get_headers(response: &ureq::Response) -> Headers { + let names = response.headers_names(); + + let mut headers = Headers::with_capacity(names.len()); + + for name in names { + let header = response.header(&name).unwrap_or_default().to_string(); + + headers.set(name, header); + } + + headers } impl Default for Response { @@ -60,7 +118,7 @@ mod tests { assert_eq!(s, "HTTP/1.1 0\nHeaders:\nCookies:\nBody: 0 bytes\n"); response.status = 200; - response.headers.set("Content-Type", "application/json"); + response.headers.set_str("Content-Type", "application/json"); response .cookies .insert("session".to_string(), "1234567890".to_string()); diff --git a/crates/gosub_render_backend/src/svg.rs b/crates/gosub_render_backend/src/svg.rs index 59ff05973..02e194583 100644 --- a/crates/gosub_render_backend/src/svg.rs +++ b/crates/gosub_render_backend/src/svg.rs @@ -1,6 +1,6 @@ use gosub_html5::node::NodeId; use gosub_html5::parser::document::DocumentHandle; -use gosub_shared::types::Result; +use gosub_shared::types::{Result, Size}; use crate::{ImageBuffer, RenderBackend}; @@ -13,4 +13,9 @@ pub trait SvgRenderer { fn parse_internal(tree: DocumentHandle, id: NodeId) -> Result; fn render(&mut self, doc: &Self::SvgDocument) -> Result>; + fn render_with_size( + &mut self, + doc: &Self::SvgDocument, + size: Size, + ) -> Result>; } diff --git a/crates/gosub_renderer/src/draw.rs b/crates/gosub_renderer/src/draw.rs index 514364fb9..e58b4d2bb 100644 --- a/crates/gosub_renderer/src/draw.rs +++ b/crates/gosub_renderer/src/draw.rs @@ -6,6 +6,7 @@ use url::Url; use gosub_css3::colors::RgbColor; use gosub_css3::stylesheet::CssValue; use gosub_html5::node::NodeId; +use gosub_net::http::fetcher::Fetcher; use gosub_render_backend::geo::{Size, SizeU32, FP}; use gosub_render_backend::layout::{Layout, LayoutTree, Layouter}; use gosub_render_backend::svg::SvgRenderer; @@ -251,7 +252,14 @@ impl Drawer<'_, '_, B, L> { pos.x += p.x as FP; pos.y += p.y as FP; - let border_radius = render_bg(node, self.scene, pos, &self.drawer.url, &mut self.svg); + let border_radius = render_bg( + node, + self.scene, + pos, + &self.drawer.url, + &mut self.svg, + &self.drawer.fetcher, + ); if let RenderNodeData::Element(element) = &node.data { if element.name() == "img" { @@ -260,24 +268,19 @@ impl Drawer<'_, '_, B, L> { .get("src") .ok_or(anyhow!("Image element has no src attribute"))?; - let url = - Url::parse(src.as_str()).or_else(|_| self.drawer.url.join(src.as_str()))?; + let url = src.as_str(); + + let size = node.layout.size(); + + let img = request_img(&self.drawer.fetcher, &mut self.svg, url, size.u32())?; - let img = request_img(&mut self.svg, &url)?; let fit = element .attributes .get("object-fit") .map(|prop| prop.as_str()) .unwrap_or("contain"); - render_image::( - img, - self.scene, - *pos, - node.layout.size(), - border_radius, - fit, - )?; + render_image::(img, self.scene, *pos, size, border_radius, fit)?; } } @@ -305,7 +308,7 @@ fn render_text( } }) .map(|color| Color::rgba(color.r as u8, color.g as u8, color.b as u8, color.a as u8)) - .unwrap_or(Color::BLACK); + .unwrap_or(Color::WHITE); let text = Text::new(&mut text.prerender); @@ -336,6 +339,7 @@ fn render_bg( pos: &Point, root_url: &Url, svg: &mut B::SVGRenderer, + fetcher: &Fetcher, ) -> (FP, FP, FP, FP) { let bg_color = node .properties @@ -448,12 +452,7 @@ fn render_bg( }); if let Some(url) = background_image { - let Ok(url) = Url::parse(url).or_else(|_| root_url.join(url)) else { - eprintln!("TODO: Add Image not found Image"); - return border_radius; - }; - - let img = match request_img(svg, &url) { + let img = match request_img(fetcher, svg, &url, node.layout.size().u32()) { Ok(img) => img, Err(e) => { eprintln!("Error loading image: {:?}", e); diff --git a/crates/gosub_renderer/src/draw/img.rs b/crates/gosub_renderer/src/draw/img.rs index 5a5194fe2..4d73e1004 100644 --- a/crates/gosub_renderer/src/draw/img.rs +++ b/crates/gosub_renderer/src/draw/img.rs @@ -1,35 +1,29 @@ +use anyhow::anyhow; +use gosub_net::http::fetcher::Fetcher; +use gosub_render_backend::svg::SvgRenderer; +use gosub_render_backend::{Image as _, ImageBuffer, RenderBackend, SizeU32}; +use gosub_shared::types::Result; use std::fs; use std::io::Cursor; - use url::Url; -use gosub_render_backend::svg::SvgRenderer; -use gosub_render_backend::{Image as _, ImageBuffer, RenderBackend}; -use gosub_shared::types::Result; - pub fn request_img( + fetcher: &Fetcher, svg_renderer: &mut B::SVGRenderer, - url: &Url, + url: &str, + size: SizeU32, ) -> Result> { - let img = if url.scheme() == "file" { - let path = url.as_str().trim_start_matches("file://"); + println!("getting image from url: {}", url); - println!("Loading image from: {:?}", path); - - fs::read(path)? - } else { - let res = gosub_net::http::ureq::get(url.as_str()).call()?; + let res = fetcher.get(url)?; - let mut img = Vec::with_capacity( - res.header("Content-Length") - .and_then(|x| x.parse::().ok()) - .unwrap_or(1024), - ); + println!("got response from url: {}", res.status); - res.into_reader().read_to_end(&mut img)?; + if !res.is_ok() { + return Err(anyhow!("Could not get url. Status code {}", res.status)); + } - img - }; + let img = res.body; let is_svg = img.starts_with(b"( let svg = >::parse_external(svg)?; - svg_renderer.render(&svg)? + let svg = svg_renderer.render_with_size(&svg, size)?; + svg } else { let format = image::guess_format(&img)?; let img = image::load(Cursor::new(img), format)?; //In that way we don't need to copy the image data diff --git a/crates/gosub_renderer/src/render_tree.rs b/crates/gosub_renderer/src/render_tree.rs index ca5e7c9c3..6e9eaecd7 100644 --- a/crates/gosub_renderer/src/render_tree.rs +++ b/crates/gosub_renderer/src/render_tree.rs @@ -6,6 +6,7 @@ use url::Url; use gosub_html5::node::NodeId; use gosub_html5::parser::document::{Document, DocumentBuilder}; use gosub_html5::parser::Html5Parser; +use gosub_net::http::fetcher::Fetcher; use gosub_net::http::ureq; use gosub_render_backend::geo::SizeU32; use gosub_render_backend::layout::Layouter; @@ -16,6 +17,7 @@ use gosub_styling::render_tree::{generate_render_tree, RenderNodeData, RenderTre use gosub_styling::styling::CssProperties; pub struct TreeDrawer { + pub(crate) fetcher: Fetcher, pub(crate) tree: RenderTree, pub(crate) layouter: L, pub(crate) size: Option, @@ -36,7 +38,7 @@ impl TreeDrawer { tree, layouter, size: None, - url, + url: url.clone(), position: PositionTree::default(), last_hover: None, debug, @@ -45,6 +47,7 @@ impl TreeDrawer { tree_scene: None, selected_element: None, scene_transform: None, + fetcher: Fetcher::new(url), } } } diff --git a/crates/gosub_svg/src/resvg.rs b/crates/gosub_svg/src/resvg.rs index 28a2ee434..d5b316944 100644 --- a/crates/gosub_svg/src/resvg.rs +++ b/crates/gosub_svg/src/resvg.rs @@ -6,7 +6,7 @@ use gosub_html5::parser::document::DocumentHandle; use gosub_render_backend::geo::FP; use gosub_render_backend::svg::SvgRenderer; use gosub_render_backend::{Image, ImageBuffer, RenderBackend}; -use gosub_shared::types::Result; +use gosub_shared::types::{Result, Size}; use crate::SVGDocument; @@ -28,17 +28,30 @@ impl SvgRenderer for Resvg { } fn render(&mut self, doc: &SVGDocument) -> Result> { - let img: B::Image = Self::render_to_image::(self, doc)?; + let size = doc.tree.size().to_int_size(); + let size = Size::new(size.width(), size.height()); + + self.render_with_size(doc, size) + } + + fn render_with_size( + &mut self, + doc: &Self::SvgDocument, + size: Size, + ) -> Result> { + let img: B::Image = Self::render_to_image::(self, doc, size)?; Ok(ImageBuffer::Image(img)) } } impl Resvg { - pub fn render_to_image(&mut self, doc: &SVGDocument) -> Result { - let size = doc.tree.size().to_int_size(); - - let mut pixmap = Pixmap::new(size.width(), size.height()) + pub fn render_to_image( + &mut self, + doc: &SVGDocument, + size: Size, + ) -> Result { + let mut pixmap = Pixmap::new(size.width, size.height) .ok_or_else(|| anyhow!("Failed to create pixmap"))?; resvg::render( diff --git a/crates/gosub_vello/src/vello_svg.rs b/crates/gosub_vello/src/vello_svg.rs index 83eb96253..3e73e4b7d 100644 --- a/crates/gosub_vello/src/vello_svg.rs +++ b/crates/gosub_vello/src/vello_svg.rs @@ -2,7 +2,7 @@ use gosub_html5::node::NodeId; use gosub_html5::parser::document::DocumentHandle; use gosub_render_backend::svg::SvgRenderer; use gosub_render_backend::ImageBuffer; -use gosub_shared::types::Result; +use gosub_shared::types::{Result, Size}; use crate::render::window::WindowData; use crate::VelloBackend; @@ -30,4 +30,12 @@ impl SvgRenderer for VelloSVG { todo!(); } + + fn render_with_size( + &mut self, + doc: &Self::SvgDocument, + size: Size, + ) -> gosub_shared::types::Result> { + todo!() + } } diff --git a/src/engine.rs b/src/engine.rs index bc90589ee..3d9ef9b12 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -110,7 +110,7 @@ fn fetch_url( fetch_response.response.version = format!("{:?}", resp.http_version()); for key in &resp.headers_names() { for value in resp.all(key) { - fetch_response.response.headers.set(key.as_str(), value); + fetch_response.response.headers.set_str(key.as_str(), value); } } // TODO: cookies