From 428a8d021ede4df22a60fc73420c6f8c7cc27151 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Sun, 14 Jun 2026 21:03:52 -0400 Subject: [PATCH] Support remote image URLs in Typst compiler --- README.md | 6 ++++++ server/Cargo.toml | 1 + server/src/world.rs | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/README.md b/README.md index e2385b4..9527f03 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,12 @@ Uploaded images can be referenced natively using the `#image` function in Typst. #image("logo.png", width: 50%) ``` +You can also reference remote images directly by their `http://` or `https://` URL — TypstDrive fetches them at compile time. + +```typst +#image("https://example.com/logo.png", width: 50%) +``` + ## Self-Hosting TypstDrive is completely self-hostable. A Docker image packages both the Rust backend and the SvelteKit frontend into a single container. diff --git a/server/Cargo.toml b/server/Cargo.toml index 07e011c..ef23ebf 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -37,3 +37,4 @@ tempfile = "3.27.0" sha2 = "0.10" base64 = "0.22" toml = "0.8" +ureq = "2.12" diff --git a/server/src/world.rs b/server/src/world.rs index 704f1e6..db369aa 100644 --- a/server/src/world.rs +++ b/server/src/world.rs @@ -1,5 +1,6 @@ use chrono::Datelike; use std::collections::HashMap; +use std::io::Read; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Duration}; @@ -22,10 +23,44 @@ pub struct MemoryWorld { const LOCAL_NAMESPACE: &str = "typstdrive"; +const REMOTE_FETCH_TIMEOUT_SECS: u64 = 15; +const REMOTE_MAX_BYTES: u64 = 50 * 1024 * 1024; + fn normalize_path(path: &str) -> String { path.trim_start_matches('/').replace('\\', "/") } +fn remote_url(path: &str) -> Option { + for scheme in ["https", "http"] { + let prefix = format!("{scheme}:/"); + if let Some(rest) = path.strip_prefix(&prefix) { + let host_and_path = rest.trim_start_matches('/'); + return Some(format!("{scheme}://{host_and_path}")); + } + } + None +} + +fn fetch_remote(url: &str) -> FileResult> { + let agent = ureq::AgentBuilder::new() + .timeout(std::time::Duration::from_secs(REMOTE_FETCH_TIMEOUT_SECS)) + .build(); + + let response = agent + .get(url) + .call() + .map_err(|e| FileError::Other(Some(format!("failed to fetch {url}: {e}").into())))?; + + let mut bytes = Vec::new(); + response + .into_reader() + .take(REMOTE_MAX_BYTES) + .read_to_end(&mut bytes) + .map_err(|e| FileError::Other(Some(format!("failed to read {url}: {e}").into())))?; + + Ok(bytes) +} + impl MemoryWorld { pub fn new_project( entrypoint: String, @@ -103,6 +138,10 @@ impl MemoryWorld { return root.load(id.vpath()).map(|bytes| bytes.to_vec()); } + if let Some(url) = remote_url(&path) { + return fetch_remote(&url); + } + self.files .get(&path) .cloned()