diff --git a/packages/renderer/src/lib.rs b/packages/renderer/src/lib.rs index 71a8cd6f7..54d582163 100644 --- a/packages/renderer/src/lib.rs +++ b/packages/renderer/src/lib.rs @@ -78,25 +78,51 @@ pub fn renderer_build_info() -> JsValue { #[wasm_bindgen] #[derive(Debug, Default)] pub struct RenderPageImageOptions { - pub(crate) page_off: usize, + pub(crate) page_off: Option, + pub(crate) cache_key: Option, + pub(crate) data_selection: Option, } #[wasm_bindgen] impl RenderPageImageOptions { #[wasm_bindgen(constructor)] pub fn new() -> Self { - Self { page_off: 0 } + Self { + page_off: None, + cache_key: None, + data_selection: None, + } } #[wasm_bindgen(getter)] - pub fn page_off(&self) -> usize { + pub fn page_off(&self) -> Option { self.page_off } #[wasm_bindgen(setter)] - pub fn set_page_off(&mut self, page_off: usize) { + pub fn set_page_off(&mut self, page_off: Option) { self.page_off = page_off; } + + #[wasm_bindgen(getter)] + pub fn cache_key(&self) -> Option { + self.cache_key.clone() + } + + #[wasm_bindgen(setter)] + pub fn set_cache_key(&mut self, cache_key: Option) { + self.cache_key = cache_key; + } + + #[wasm_bindgen(getter)] + pub fn data_selection(&self) -> Option { + self.data_selection + } + + #[wasm_bindgen(setter)] + pub fn set_data_selection(&mut self, data_selection: Option) { + self.data_selection = data_selection; + } } #[wasm_bindgen] diff --git a/packages/renderer/src/render/canvas.rs b/packages/renderer/src/render/canvas.rs index b5c4ded6c..fb859457c 100644 --- a/packages/renderer/src/render/canvas.rs +++ b/packages/renderer/src/render/canvas.rs @@ -1,9 +1,14 @@ -use std::{collections::HashMap, ops::Deref}; +use std::{collections::HashMap, hash::Hash, ops::Deref}; use typst_ts_canvas_exporter::{ AnnotationListTask, DefaultExportFeature, ExportFeature, TextContentTask, }; -use typst_ts_core::error::prelude::*; +use typst_ts_core::{ + annotation::AnnotationList, + error::prelude::*, + hash::{Fingerprint, FingerprintHasher, FingerprintSipHasher}, + TextContent, +}; use typst_ts_svg_exporter::{ flat_ir::LayoutRegionNode, ir::{Axes, Rect, Scalar}, @@ -12,6 +17,13 @@ use wasm_bindgen::prelude::*; use crate::{RenderPageImageOptions, RenderSession, TypstRenderer}; +#[derive(Default)] +pub struct CanvasDataSelection { + pub body: bool, + pub text_content: bool, + pub annotation_list: bool, +} + #[wasm_bindgen] impl TypstRenderer { pub async fn render_page_to_canvas( @@ -20,11 +32,14 @@ impl TypstRenderer { canvas: &web_sys::CanvasRenderingContext2d, options: Option, ) -> ZResult { - let (text_content, annotation_list, ..) = self + let (fingerprint, text_content, annotation_list, ..) = self .render_page_to_canvas_internal::(ses, canvas, options) .await?; let res = js_sys::Object::new(); + let err = + js_sys::Reflect::set(&res, &"cacheKey".into(), &fingerprint.as_svg_id("c").into()); + err.map_err(map_into_err::("Renderer.SetCacheKey"))?; let err = js_sys::Reflect::set(&res, &"textContent".into(), &text_content); err.map_err(map_into_err::("Renderer.SetTextContent"))?; let err = js_sys::Reflect::set(&res, &"annotationList".into(), &annotation_list); @@ -40,7 +55,7 @@ impl TypstRenderer { ses: &RenderSession, canvas: &web_sys::CanvasRenderingContext2d, options: Option, - ) -> ZResult<(JsValue, JsValue, Option>)> { + ) -> ZResult<(Fingerprint, JsValue, JsValue, Option>)> { let rect_lo_x: f32 = -1.; let rect_lo_y: f32 = -1.; let rect_hi_x: f32 = 1e30; @@ -54,13 +69,18 @@ impl TypstRenderer { let mut client = ses.canvas_kern.lock().unwrap(); client.set_pixel_per_pt(ses.pixel_per_pt.unwrap_or(3.)); client.set_fill(ses.background_color.as_deref().unwrap_or("ffffff").into()); - console_log!( - "background_color: {:?}", - ses.background_color.as_deref().unwrap_or("ffffff") - ); + // console_log!( + // "background_color: {:?}", + // ses.background_color.as_deref().unwrap_or("ffffff") + // ); + + let data_selection = options + .as_ref() + .and_then(|o| o.data_selection) + .unwrap_or(u32::MAX); - let mut tc = Default::default(); - let mut annotations = Default::default(); + let mut tc = ((data_selection & (1 << 1)) != 0).then(TextContent::default); + let mut annotations = ((data_selection & (1 << 2)) != 0).then(AnnotationList::default); // let def_provider = GlyphProvider::new(FontGlyphProvider::default()); // let partial_providier = @@ -76,25 +96,59 @@ impl TypstRenderer { } else { None }; - // if let Some(perf_events) = perf_events.as_ref() { // worker.set_perf_events(perf_events) // }; - let mut page_num = usize::MAX; - if let Some(options) = options { - page_num = options.page_off; - client - .render_page_in_window(&mut kern, canvas, options.page_off, rect) - .await?; + // todo: reuse + let Some(t) = &kern.layout else { + todo!(); + }; + let pages = t.pages(kern.module()).unwrap().pages(); + + let fingerprint; + let page_off; + + if let Some(RenderPageImageOptions { + page_off: Some(c), .. + }) = options + { + fingerprint = pages[c].content; + page_off = Some(c); } else { - client.render_in_window(&mut kern, canvas, rect).await; + let mut f = FingerprintSipHasher::default(); + for page in pages.iter() { + page.content.hash(&mut f); + } + fingerprint = f.finish_fingerprint().0; + page_off = None; + } + + let cached = options + .and_then(|o| o.cache_key) + .map(|c| c == fingerprint.as_svg_id("c")) + .unwrap_or(false); + + console_log!("cached: {}", cached); + + if !cached { + if let Some(page_off) = page_off { + client + .render_page_in_window(&mut kern, canvas, page_off, rect) + .await?; + } else { + client.render_in_window(&mut kern, canvas, rect).await; + } } // todo: leaking abstraction - let mut worker = TextContentTask::new(&kern.doc.module, &mut tc); - let mut annotation_list_worker = - AnnotationListTask::new(&kern.doc.module, &mut annotations); + let page_num = usize::MAX; + let mut worker = tc + .as_mut() + .map(|tc| TextContentTask::new(&kern.doc.module, tc)); + let mut annotation_list_worker = annotations + .as_mut() + .map(|annotations| AnnotationListTask::new(&kern.doc.module, annotations)); // todo: reuse if let Some(t) = &kern.layout { let pages = match t { @@ -111,21 +165,26 @@ impl TypstRenderer { continue; } let partial_page_off = if page_num != usize::MAX { 0. } else { page_off }; - worker.page_height = partial_page_off + page.size.y.0; - worker.process_flat_item( - tiny_skia::Transform::from_translate(partial_page_off, 0.), - &page.content, - ); - annotation_list_worker.page_num = page_num as u32; - annotation_list_worker.process_flat_item( - tiny_skia::Transform::from_translate(partial_page_off, 0.), - &page.content, - ); + if let Some(worker) = worker.as_mut() { + worker.page_height = partial_page_off + page.size.y.0; + worker.process_flat_item( + tiny_skia::Transform::from_translate(partial_page_off, 0.), + &page.content, + ); + } + if let Some(worker) = annotation_list_worker.as_mut() { + worker.page_num = page_num as u32; + worker.process_flat_item( + tiny_skia::Transform::from_translate(partial_page_off, 0.), + &page.content, + ); + } page_off += page.size.y.0; } } Ok(( + fingerprint, serde_wasm_bindgen::to_value(&tc) .map_err(map_into_err::("Renderer.EncodeTextContent"))?, serde_wasm_bindgen::to_value(&annotations).map_err(map_into_err::( @@ -235,7 +294,7 @@ mod tests { let prepare = performance.now(); - let (res, _, perf_events) = renderer + let (_fingerprint, res, _, perf_events) = renderer .render_page_to_canvas_internal::(&session, &context, None) .await .unwrap(); diff --git a/packages/typst.ts/src/index.mts b/packages/typst.ts/src/index.mts index 601dcc404..505044173 100644 --- a/packages/typst.ts/src/index.mts +++ b/packages/typst.ts/src/index.mts @@ -2,7 +2,7 @@ export type { InitOptions, BeforeBuildFn } from './options.init.mjs'; export type { RenderByContentOptions, RenderInSessionOptions, - RenderPageOptions, + RenderCanvasOptions as RnederPageOptions, RenderOptions, } from './options.render.mjs'; export { preloadRemoteFonts, preloadSystemFonts } from './options.init.mjs'; diff --git a/packages/typst.ts/src/internal.types.mts b/packages/typst.ts/src/internal.types.mts index 2f5a44b78..0a4c648eb 100644 --- a/packages/typst.ts/src/internal.types.mts +++ b/packages/typst.ts/src/internal.types.mts @@ -61,3 +61,47 @@ export interface SemanticTokens { readonly data: Uint32Array; } //#endregion + +export interface AnnotationBox { + height: number; + width: number; + page_ref: number; + transform: TransformMatrix; +} + +export interface UrlLinkAction { + t: 'Url'; + v: { + url: string; + }; +} + +export interface GoToLinkAction { + t: 'GoTo'; + v: { + page_ref: number; + x: number; + y: number; + }; +} + +export type LinkAction = UrlLinkAction | GoToLinkAction; + +export interface LinkAnnotation { + annotation_box: AnnotationBox; + action: LinkAction; +} + +export interface AnnotationList { + links: LinkAnnotation[]; +} + +/** + * The result of rendering a Typst document to a canvas. + */ +export interface RenderCanvasResult { + cacheKey: string; + textContent: any; + // still unstable type + annotationList: AnnotationList; +} diff --git a/packages/typst.ts/src/main.mts b/packages/typst.ts/src/main.mts index 5327ecb3d..bcaf44975 100644 --- a/packages/typst.ts/src/main.mts +++ b/packages/typst.ts/src/main.mts @@ -3,7 +3,7 @@ export type { InitOptions, BeforeBuildFn } from './options.init.mjs'; export type { RenderByContentOptions, RenderInSessionOptions, - RenderPageOptions, + RenderCanvasOptions as RenderPageOptions, RenderOptions, } from './options.render.mjs'; export { preloadRemoteFonts, preloadSystemFonts } from './options.init.mjs'; diff --git a/packages/typst.ts/src/options.render.mts b/packages/typst.ts/src/options.render.mts index 3c531ba87..49293e93a 100644 --- a/packages/typst.ts/src/options.render.mts +++ b/packages/typst.ts/src/options.render.mts @@ -111,9 +111,31 @@ export interface ManipulateDataOptions { } /** - * The options for rendering a page to an image. + * The options for rendering a page to a canvas. * @property {number} page_off - The page offset to render. */ -export class RenderPageOptions { - page_off: number; +export class RenderCanvasOptions { + canvas: CanvasRenderingContext2D; + + /** + * The page offset to render. + */ + pageOffset?: number; + /** + * The previous render state. + */ + cacheKey?: string; + + /** + * The selection of the data to render. + * @description `body`: render the body of the document. + * @description `text`: render the text repr of the document. + * @description `annnotation`: render the annnotation of the document. + * @default: all of fields set to `true` + */ + dataSelection?: { + body?: boolean; + text?: boolean; + annotation?: boolean; + }; } diff --git a/packages/typst.ts/src/renderer.mts b/packages/typst.ts/src/renderer.mts index 9c4f4dcb8..73c18c0c0 100644 --- a/packages/typst.ts/src/renderer.mts +++ b/packages/typst.ts/src/renderer.mts @@ -3,12 +3,18 @@ import type * as typst from '@myriaddreamin/typst-ts-renderer/pkg/wasm-pack-shim import type { InitOptions } from './options.init.mjs'; import { PageViewport } from './render/canvas/viewport.mjs'; -import { PageInfo, TransformMatrix, kObject } from './internal.types.mjs'; +import { + AnnotationList, + PageInfo, + RenderCanvasResult, + TransformMatrix, + kObject, +} from './internal.types.mjs'; import { CreateSessionOptions, RenderToCanvasOptions, RenderOptions, - RenderPageOptions, + RenderCanvasOptions, RenderToSvgOptions, ManipulateDataOptions, RenderSvgOptions, @@ -29,10 +35,7 @@ export interface RenderResult { height: number; } -export interface RenderPageResult { - textContent: any; - annotationList: AnnotationList; -} +type ContextedRenderOptions = T | RenderOptions; /** * The session of a Typst document. @@ -163,7 +166,7 @@ export class RenderSession { /** * See {@link TypstRenderer#renderSvg} for more details. */ - renderSvg(options: RenderOptions): Promise { + renderSvg(options: ContextedRenderOptions): Promise { return this.plugin.renderSvg({ renderSession: this, ...options, @@ -173,13 +176,23 @@ export class RenderSession { /** * See {@link TypstRenderer#renderToSvg} for more details. */ - renderToSvg(options: RenderOptions): Promise { + renderToSvg(options: ContextedRenderOptions): Promise { return this.plugin.renderToSvg({ renderSession: this, ...options, }); } + /** + * See {@link TypstRenderer#renderCanvas} for more details. + */ + renderCanvas(options: ContextedRenderOptions): Promise { + return this.plugin.renderCanvas({ + renderSession: this, + ...options, + }); + } + /** * See {@link TypstRenderer#manipulateData} for more details. */ @@ -257,11 +270,16 @@ export interface TypstRenderer extends TypstSvgRenderer { */ retrievePagesInfoFromSession(session: RenderSession): PageInfo[]; + /** + * Render a Typst document to canvas. + */ + renderCanvas(options: RenderOptions): Promise; + /** * Render a Typst document to canvas. * @param {RenderOptions} options - The options for * rendering a Typst document to specified container. - * @returns {RenderResult} - The result of rendering a Typst document. + * @returns {void} - The result of rendering a Typst document. * @example * ```typescript * let fetchDoc = (path) => fetch(path).then( @@ -456,7 +474,7 @@ export interface TypstSvgRenderer { * Render a Typst document to svg. * @param {RenderOptions} options - The options for * rendering a Typst document to specified container. - * @returns {RenderResult} - The result of rendering a Typst document. + * @returns {void} - The result of rendering a Typst document. * @example * ```typescript * let fetchDoc = (path) => fetch(path).then( @@ -524,19 +542,30 @@ class TypstRendererDriver { return session.retrievePagesInfo(); } - renderCanvasInSession( - session: RenderSession, - canvas: CanvasRenderingContext2D, - options?: RenderPageOptions, - ): Promise { - if (!options) { - return this.renderer.render_page_to_canvas(session[kObject], canvas, options || undefined); - } - - const rustOptions = new this.rendererJs.RenderPageImageOptions(); - rustOptions.page_off = options.page_off; - - return this.renderer.render_page_to_canvas(session[kObject], canvas, rustOptions); + /** + * Render a Typst document to canvas. + */ + renderCanvas(options: RenderOptions): Promise { + return this.withinOptionSession(options, async sessionRef => { + const rustOptions = new this.rendererJs.RenderPageImageOptions(); + if (options.pageOffset !== undefined) { + rustOptions.page_off = options.pageOffset; + } + if (options.dataSelection !== undefined) { + let encoded = 0; + if (options.dataSelection.body) { + encoded |= 1 << 0; + } + if (options.dataSelection.text) { + encoded |= 1 << 1; + } + if (options.dataSelection.annotation) { + encoded |= 1 << 2; + } + rustOptions.data_selection = encoded; + } + return this.renderer.render_page_to_canvas(sessionRef[kObject], options.canvas, rustOptions); + }); } // async renderPdf(artifactContent: string): Promise { @@ -560,7 +589,7 @@ class TypstRendererDriver { container: HTMLElement, canvasList: HTMLCanvasElement[], options: RenderToCanvasOptions, - ): Promise { + ): Promise { const pages_info = session[kObject].pages_info; const page_count = pages_info.page_count; @@ -570,8 +599,10 @@ class TypstRendererDriver { if (!ctx) { throw new Error('canvas context is null'); } - return await this.renderCanvasInSession(session, ctx, { - page_off, + return await this.renderCanvas({ + canvas: ctx, + renderSession: session, + pageOffset: page_off, }); }; @@ -600,7 +631,7 @@ class TypstRendererDriver { /// seq [ (async () => { - const results: RenderPageResult[] = []; + const results: RenderCanvasResult[] = []; for (let i = 0; i < page_count; i++) { results.push(await doRender(i, i)); } @@ -619,19 +650,6 @@ class TypstRendererDriver { }); } - private renderOnePageTextLayer( - container: HTMLElement, - viewport: PageViewport, - textContentSource: any, - ) { - // console.log(viewport); - this.pdf.renderTextLayer({ - textContentSource, - container, - viewport, - }); - } - private renderTextLayer( session: RenderSession, view: RenderView, @@ -733,7 +751,7 @@ class TypstRendererDriver { async renderToCanvas(options: RenderOptions): Promise { let session: RenderSession; - let renderPageResults: RenderPageResult[]; + let renderPageResults: RenderCanvasResult[]; const mountContainer = options.container; mountContainer.style.visibility = 'hidden'; @@ -891,12 +909,10 @@ class TypstRendererDriver { } private withinOptionSession( - options: RenderOptions, + options: RenderOptions, fn: (session: RenderSession) => Promise, ): Promise { - function isRenderByContentOption( - options: RenderSvgOptions | RenderToCanvasOptions | CreateSessionOptions, - ): options is CreateSessionOptions { + function isRenderByContentOption(options: RenderOptions): options is CreateSessionOptions { return 'artifactContent' in options; } @@ -948,37 +964,3 @@ class TypstRendererDriver { } } } - -interface AnnotationBox { - height: number; - width: number; - page_ref: number; - transform: TransformMatrix; -} - -interface UrlLinkAction { - t: 'Url'; - v: { - url: string; - }; -} - -interface GoToLinkAction { - t: 'GoTo'; - v: { - page_ref: number; - x: number; - y: number; - }; -} - -type LinkAction = UrlLinkAction | GoToLinkAction; - -interface LinkAnnotation { - annotation_box: AnnotationBox; - action: LinkAction; -} - -interface AnnotationList { - links: LinkAnnotation[]; -}