194c44c
use axum::body::Body;
use axum::extract::{Path, Query, State};
use axum::http::{StatusCode, header};
use axum::response::Response;
use crate::handler::html::error::html_err;
use crate::handler::html::{HtmlRepoPath, resolve_view};
use crate::service::GitService;
use crate::service::pdf;
use crate::share::access::BasicAuth;
#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
pub struct BlobPagesQuery {
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub path: Option<String>,
/// Rasterisation scale: 1.0 is 72 dpi. Clamped to 0.5..=3.0.
#[serde(default)]
pub scale: Option<f32>,
}
/// Rasterised pages of a PDF, streamed one page at a time.
///
/// Framing: repeating `u32 header_len | header | u32 payload_len | payload`,
/// all big-endian. The header is JSON — `{page,total,width,height}` per page,
/// `{page,total,error}` for a page that failed, `{total,done}` to close — and
/// the payload of a page frame is its PNG bytes. Streaming (rather than one
/// request per page) is what keeps the first page instant while the rest of a
/// long document is still being rasterised.
#[utoipa::path(
get,
path = "/repos/{space_name}/{repo_name}/html/blob-pages",
tag = "html",
description = "按页栅格化 PDF 并以分帧流式返回 PNG,供文件页阅读器使用。",
operation_id = "render_blob_pages",
params(HtmlRepoPath, BlobPagesQuery),
responses(
(status = 200, description = "framed png page stream"),
(status = 404, description = "repo, ref or blob not found"),
(status = 400, description = "blob is not a readable pdf")
)
)]
pub async fn render_blob_pages(
State(service): State<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<BlobPagesQuery>,
auth: BasicAuth,
) -> Response {
let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
Ok(v) => v,
Err(resp) => return resp,
};
let ctx = view.ctx;
let scale = pdf::clamp_scale(q.scale);
let receiver = match pdf::render_stream(
&service,
&ctx.space_name,
&ctx.repo_name,
&view.revision,
&view.clean_path,
scale,
)
.await
{
Ok(rx) => rx,
Err(crate::share::errors::GitError::NotFound) => return html_err(404, "blob not found"),
Err(e) => {
tracing::warn!(error = %e, "pdf page stream rejected");
return html_err(400, "blob is not a readable pdf");
}
};
let stream = futures::stream::unfold(receiver, |mut rx| async move {
rx.recv()
.await
.map(|chunk| (Ok::<Vec<u8>, std::io::Error>(chunk), rx))
});
let mut resp = Response::new(Body::from_stream(stream));
*resp.status_mut() = StatusCode::OK;
let headers = resp.headers_mut();
headers.insert(
header::CONTENT_TYPE,
"application/octet-stream".parse().unwrap(),
);
headers.insert(header::CACHE_CONTROL, "private, no-store".parse().unwrap());
headers.insert(header::VARY, "Cookie".parse().unwrap());
// Reverse proxies must hand each frame through as it is produced, otherwise
// the streaming reader degrades into a spinner until the document ends.
headers.insert("x-accel-buffering", "no".parse().unwrap());
resp
}