1use super::ServerState;
2use axum::body::Body;
3use flams_math_archives::{
4 LocallyBuilt, MathArchive,
5 backend::{GlobalBackend, LocalBackend},
6};
7use flams_system::settings::Settings;
8use ftml_ontology::utils::time::Timestamp;
9use ftml_uris::{
10 ArchiveId, DocumentUri, IsNarrativeUri, Language, UriWithArchive, UriWithPath,
11 components::{DocumentUriComponentTuple, DocumentUriComponents},
12};
13use http::{Request, Response};
14use leptos::server_fn::{codec::IntoRes, response::Res};
15use std::{borrow::Cow, ops::DerefMut, path::PathBuf, sync::atomic::AtomicU64};
16use tower::ServiceExt;
17use tower_http::services::{ServeFile, fs::ServeFileSystemResponseBody};
18
19#[derive(Clone, Default)]
20pub struct ImageStore(ImageStoreI);
21
22#[derive(Default, Copy, Clone)]
23struct ImageStoreI {
24 }
27impl ImageStoreI {
28 async fn get(&self, spec: ImageSpec) -> Option<Box<[u8]>> {
30 let path = match spec {
31 ImageSpec::Kpse(p) => tex_engine::engine::filesystem::kpathsea::KPATHSEA.which(p)?,
32 ImageSpec::ARp(a, p) => {
33 GlobalBackend.with_local_archive(&a, |a| a.map(|a| a.path().join(&*p)))?
34 }
35 ImageSpec::File(p) => std::path::PathBuf::from(p.to_string()),
36 };
37 let img =
38 tokio::task::spawn_blocking(|| image::ImageReader::open(path).ok()?.decode().ok())
39 .await
40 .ok()??;
41 let mut v = Vec::<u8>::new();
42 img.write_with_encoder(image::codecs::webp::WebPEncoder::new_lossless(&mut v))
43 .ok()?;
44 Some(v.into_boxed_slice())
45 }
46}
47
48#[derive(Clone, Debug, Hash, PartialEq, Eq)]
49pub enum ImageSpec {
50 Kpse(Box<str>),
51 ARp(ArchiveId, Box<str>),
52 File(Box<str>),
53}
54impl ImageSpec {
55 pub fn path(&self) -> Option<PathBuf> {
56 match self {
57 Self::Kpse(p) => tex_engine::engine::filesystem::kpathsea::KPATHSEA.which(p),
58 Self::ARp(a, p) => {
59 GlobalBackend.with_local_archive(a, |a| a.map(|a| a.path().join(&**p)))
60 }
61 Self::File(p) => Some(std::path::PathBuf::from(p.to_string())),
62 }
63 }
64}
65
66pub struct ImageData {
67 img: Box<[u8]>,
68 timestamp: AtomicU64,
69}
70impl ImageData {
71 pub fn update(&self) {
72 let now = Timestamp::now();
73 self.timestamp
74 .store(now.0.get() as _, std::sync::atomic::Ordering::SeqCst);
75 }
76 #[must_use]
77 pub fn new(data: &[u8]) -> Self {
78 Self {
79 img: data.into(),
80 timestamp: AtomicU64::new(Timestamp::now().0.get()),
81 }
82 }
83}
84
85pub(crate) struct Img(Box<[u8]>);
86impl axum::response::IntoResponse for Img {
87 fn into_response(self) -> axum::response::Response {
88 ([(axum::http::header::CONTENT_TYPE, "image/webp")], self.0).into_response()
89 }
90}
91
92#[axum::debug_handler]
93pub(crate) async fn img_handler(
94 uri: http::Uri,
95 axum::extract::State(ServerState { images, .. }): axum::extract::State<ServerState>,
96 ) -> Result<Img, axum::http::StatusCode> {
98 let Some(s) = uri.query() else {
99 return Err(http::StatusCode::NOT_FOUND);
100 };
101
102 let spec = if let Some(s) = s.strip_prefix("kpse=") {
103 ImageSpec::Kpse(s.into())
104 } else if let Some(f) = s.strip_prefix("file=")
105 && Settings::get().lsp
106 {
107 ImageSpec::File(f.into())
108 } else if let Some(s) = s.strip_prefix("a=")
109 && let Some((a, rp)) = s.split_once("&rp=")
110 {
111 let a = a.parse().unwrap_or_else(|_| unreachable!());
112 let rp = rp.into();
113 ImageSpec::ARp(a, rp)
114 } else {
115 return Err(http::StatusCode::NOT_FOUND);
116 };
117
118 if let Some(img) = images.0.get(spec).await {
119 Ok(Img(img))
120 }
121 else {
133 Err(http::StatusCode::NOT_FOUND)
134 }
135}
136
137pub(crate) async fn doc_handler(
138 uri: http::Uri,
139) -> axum::response::Response<ServeFileSystemResponseBody> {
140 let req_uri = uri;
141 let default = || {
142 let mut resp = axum::response::Response::new(ServeFileSystemResponseBody::default());
143 *resp.status_mut() = http::StatusCode::NOT_FOUND;
144 resp
145 };
146 let err = |s: &str| {
147 let mut resp = axum::response::Response::new(ServeFileSystemResponseBody::default());
148 tracing::info!("pdf download error: {s}");
149 *resp.status_mut() = http::StatusCode::BAD_REQUEST;
150 resp
151 };
152
153 let Some(params) = Params::new(&req_uri) else {
154 return err("Invalid URI");
155 };
156
157 macro_rules! parse {
158 ($id:literal) => {
159 if let Some(s) = params.get_str($id) {
160 let Ok(r) = s.parse() else {
161 return err("malformed uri");
162 };
163 Some(r)
164 } else {
165 None
166 }
167 };
168 }
169 let Some(format) = params.get_str("format") else {
170 return err("Missing format");
171 };
172
173 let uri: Option<DocumentUri> = parse!("uri");
174 let rp = params.get("rp");
175 let a: Option<ArchiveId> = parse!("a");
176 let p = params.get("p");
177 let l: Option<Language> = parse!("l");
178 let d = params.get("d");
179
180 let comps = DocumentUriComponentTuple {
181 uri,
182 rp,
183 a,
184 p,
185 d,
186 l,
187 };
188
189 let comps: Result<DocumentUriComponents, _> = comps.try_into();
190 let uri = if let Ok(comps) = comps {
191 let Ok(uri) =
192 comps.parse(|a| GlobalBackend.with_archive(a, |a| a.map(|a| a.uri().clone())))
193 else {
194 return err("Malformed URI components");
195 };
196 uri
197 } else {
198 return err("Malformed URI components");
199 };
200 let uri2 = uri.clone();
201 let formatstr = format.to_string();
202 let Ok(Some(path)) = tokio::task::spawn_blocking(move || {
203 GlobalBackend.with_local_archive(uri.archive_id(), |a| {
204 a.map(|a| {
205 a.out_path_of(uri.path(), uri.document_name(), None, uri.language)
206 .join(&formatstr)
207 })
208 })
209 })
210 .await
211 else {
212 return default();
213 };
214
215 let pandq = format!("/{}.{format}", uri2.document_name());
216 let mime = mime_guess::from_ext(&format).first_or_octet_stream();
217 let req_uri = http::Uri::builder()
218 .path_and_query(pandq)
219 .build()
220 .unwrap_or(req_uri);
221 let req = Request::builder()
222 .uri(req_uri)
223 .body(Body::empty())
224 .expect("this is a bug");
225 ServeFile::new_with_mime(path, &mime)
226 .oneshot(req)
227 .await
228 .unwrap_or_else(|_| default())
229}
230
231struct Params<'a>(&'a str);
232impl<'a> Params<'a> {
233 fn new(uri: &'a http::Uri) -> Option<Self> {
234 uri.query().map(Self)
235 }
236 fn get_str(&self, name: &str) -> Option<Cow<'_, str>> {
237 self.0
238 .split('&')
239 .find(|s| s.starts_with(name) && s.as_bytes().get(name.len()) == Some(&b'='))?
240 .split('=')
241 .nth(1)
242 .and_then(|s| urlencoding::decode(s).ok())
243 }
244 fn get(&self, name: &str) -> Option<String> {
245 self.get_str(name).map(Cow::into_owned)
246 }
247}