Skip to content

Commit

Permalink
add Fetcher which automatically expands urls
Browse files Browse the repository at this point in the history
  • Loading branch information
Sharktheone committed Aug 19, 2024
1 parent a9e7869 commit 22cab61
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 61 deletions.
1 change: 1 addition & 0 deletions crates/gosub_net/src/http.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub use ureq;

pub mod fetcher;
pub mod headers;
pub mod request;
pub mod response;
58 changes: 58 additions & 0 deletions crates/gosub_net/src/http/fetcher.rs
Original file line number Diff line number Diff line change
@@ -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<Response> {
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<Response> {
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<Url> {
let mut parsed_url = Url::parse(url);

if parsed_url == Err(ParseError::RelativeUrlWithoutBase) {
parsed_url = self.base_url.join(url);
}

Ok(parsed_url?)
}
}
16 changes: 13 additions & 3 deletions crates/gosub_net/src/http/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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);
}
Expand Down
12 changes: 6 additions & 6 deletions crates/gosub_net/src/http/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "/");
Expand All @@ -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");
Expand Down
60 changes: 59 additions & 1 deletion crates/gosub_net/src/http/response.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,6 +24,63 @@ impl Response {
body: vec![],
}
}

pub fn is_ok(&self) -> bool {
self.status >= 200 && self.status < 300
}
}

impl TryFrom<ureq::Response> for Response {
type Error = anyhow::Error;

fn try_from(value: ureq::Response) -> std::result::Result<Self, Self::Error> {
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<Vec<u8>> for Response {
fn from(body: Vec<u8>) -> 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 {
Expand Down Expand Up @@ -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());
Expand Down
7 changes: 6 additions & 1 deletion crates/gosub_render_backend/src/svg.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -13,4 +13,9 @@ pub trait SvgRenderer<B: RenderBackend> {
fn parse_internal(tree: DocumentHandle, id: NodeId) -> Result<Self::SvgDocument>;

fn render(&mut self, doc: &Self::SvgDocument) -> Result<ImageBuffer<B>>;
fn render_with_size(
&mut self,
doc: &Self::SvgDocument,
size: Size<u32>,
) -> Result<ImageBuffer<B>>;
}
37 changes: 18 additions & 19 deletions crates/gosub_renderer/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -251,7 +252,14 @@ impl<B: RenderBackend, L: Layouter> 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" {
Expand All @@ -260,24 +268,19 @@ impl<B: RenderBackend, L: Layouter> 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::<B>(
img,
self.scene,
*pos,
node.layout.size(),
border_radius,
fit,
)?;
render_image::<B>(img, self.scene, *pos, size, border_radius, fit)?;
}
}

Expand Down Expand Up @@ -305,7 +308,7 @@ fn render_text<B: RenderBackend, L: Layouter>(
}
})
.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);

Expand Down Expand Up @@ -336,6 +339,7 @@ fn render_bg<B: RenderBackend, L: Layouter>(
pos: &Point,
root_url: &Url,
svg: &mut B::SVGRenderer,
fetcher: &Fetcher,
) -> (FP, FP, FP, FP) {
let bg_color = node
.properties
Expand Down Expand Up @@ -448,12 +452,7 @@ fn render_bg<B: RenderBackend, L: Layouter>(
});

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);
Expand Down
39 changes: 17 additions & 22 deletions crates/gosub_renderer/src/draw/img.rs
Original file line number Diff line number Diff line change
@@ -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<B: RenderBackend>(
fetcher: &Fetcher,
svg_renderer: &mut B::SVGRenderer,
url: &Url,
url: &str,
size: SizeU32,
) -> Result<ImageBuffer<B>> {
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::<usize>().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"<?xml") || img.starts_with(b"<svg");

Expand All @@ -38,7 +32,8 @@ pub fn request_img<B: RenderBackend>(

let svg = <B::SVGRenderer as SvgRenderer<B>>::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
Expand Down
Loading

0 comments on commit 22cab61

Please sign in to comment.