diff --git a/Cargo.toml b/Cargo.toml index 61ccc8c483..c83476e34b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ tempfile = "3.2.0" tokio = { version = "1.17.0", features = ["rt-multi-thread"] } tokio-stream = "0.1.9" tokio-util = {version = "0.7.3", features = ["compat"] } -tower-http = { version = "0.3.3", features = ["set-header"] } +tower-http = { version = "0.3.3", features = ["cors", "set-header"] } [dev-dependencies] executable-path = "1.0.0" diff --git a/src/media.rs b/src/media.rs index 0dfd9e78c2..df622fb5b2 100644 --- a/src/media.rs +++ b/src/media.rs @@ -5,6 +5,7 @@ pub(crate) enum Media { Audio, Iframe, Image, + Pdf, Text, Unknown, } @@ -48,6 +49,7 @@ impl FromStr for Media { } const TABLE: &[(&str, Media, &[&str])] = &[ + ("application/pdf", Media::Pdf, &["pdf"]), ("audio/flac", Media::Audio, &["flac"]), ("audio/mpeg", Media::Audio, &["mp3"]), ("audio/wav", Media::Audio, &["wav"]), diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 2293f3165d..9ade61c47f 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -6,8 +6,8 @@ use { super::*, crate::templates::{ BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionsHtml, OutputHtml, - PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewTextHtml, PreviewUnknownHtml, - RangeHtml, RareTxt, SatHtml, TransactionHtml, + PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, + PreviewUnknownHtml, RangeHtml, RareTxt, SatHtml, TransactionHtml, }, axum::{ body, @@ -28,7 +28,10 @@ use { }, std::{cmp::Ordering, str}, tokio_stream::StreamExt, - tower_http::set_header::SetResponseHeaderLayer, + tower_http::{ + cors::{Any, CorsLayer}, + set_header::SetResponseHeaderLayer, + }, }; mod error; @@ -163,7 +166,12 @@ impl Server { .layer(SetResponseHeaderLayer::overriding( header::STRICT_TRANSPORT_SECURITY, HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"), - )); + )) + .layer( + CorsLayer::new() + .allow_methods([http::Method::GET]) + .allow_origin(Any), + ); match (self.http_port(), self.https_port()) { (Some(http_port), None) => { @@ -751,6 +759,16 @@ impl Server { .ok_or_not_found(|| format!("inscription {inscription_id} content"))? .into_response(), ), + Media::Pdf => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://cdn.jsdelivr.net", + )], + PreviewPdfHtml { inscription_id }, + ) + .into_response(), + ), Media::Text => { let content = inscription .body() @@ -1995,6 +2013,27 @@ mod tests { ); } + #[test] + fn pdf_preview() { + let server = TestServer::new(); + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("application/pdf", "hello").to_witness(), + ..Default::default() + }); + let inscription_id = InscriptionId::from(txid); + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/preview/{inscription_id}"), + StatusCode::OK, + format!(r".*.*"), + ); + } + #[test] fn image_preview() { let server = TestServer::new(); diff --git a/src/templates.rs b/src/templates.rs index 8ad882af8c..8283e2916b 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -9,7 +9,9 @@ pub(crate) use { inscription::InscriptionHtml, inscriptions::InscriptionsHtml, output::OutputHtml, - preview::{PreviewAudioHtml, PreviewImageHtml, PreviewTextHtml, PreviewUnknownHtml}, + preview::{ + PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, + }, range::RangeHtml, rare::RareTxt, sat::SatHtml, diff --git a/src/templates/preview.rs b/src/templates/preview.rs index ba06b70516..f0bdb16998 100644 --- a/src/templates/preview.rs +++ b/src/templates/preview.rs @@ -10,6 +10,11 @@ pub(crate) struct PreviewImageHtml { pub(crate) inscription_id: InscriptionId, } +#[derive(boilerplate::Boilerplate)] +pub(crate) struct PreviewPdfHtml { + pub(crate) inscription_id: InscriptionId, +} + #[derive(boilerplate::Boilerplate)] pub(crate) struct PreviewTextHtml<'a> { pub(crate) text: &'a str, diff --git a/static/preview-pdf.css b/static/preview-pdf.css new file mode 100644 index 0000000000..4944cb4128 --- /dev/null +++ b/static/preview-pdf.css @@ -0,0 +1,14 @@ +html { + height: 100%; +} + +body { + height: 100%; + margin: 0; +} + +canvas { + height: 100%; + width: 100%; + object-fit: contain; +} diff --git a/static/preview-pdf.js b/static/preview-pdf.js new file mode 100644 index 0000000000..a76ff3abd1 --- /dev/null +++ b/static/preview-pdf.js @@ -0,0 +1,23 @@ +import pdfjs from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.2.146/+esm'; + +pdfjs.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.2.146/build/pdf.worker.min.js'; + +let canvas = document.querySelector('canvas'); + +let pdf = await pdfjs.getDocument(`/content/${canvas.dataset.inscription}`).promise; + +let page = await pdf.getPage(1); + +let scale = window.devicePixelRatio || 1; + +let viewport = page.getViewport({ scale }); + +canvas.width = Math.ceil(viewport.width * scale); + +canvas.height = Math.ceil(viewport.height * scale); + +page.render({ + canvasContext: canvas.getContext('2d'), + transform: [scale, 0, 0, scale, 0, 0], + viewport, +}); diff --git a/templates/preview-pdf.html b/templates/preview-pdf.html new file mode 100644 index 0000000000..e6fb528747 --- /dev/null +++ b/templates/preview-pdf.html @@ -0,0 +1,11 @@ + + + + + + + + + + +