mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 06:28:14 +00:00
✨ Add vector render path for PDF export
This commit is contained in:
parent
2d4a24bf97
commit
a1299e1797
@ -179,6 +179,15 @@
|
||||
(and (wasm-export-enabled? state)
|
||||
(contains? wasm-export-types (:type export))))
|
||||
|
||||
(defn- request-simple-export-wasm
|
||||
[export]
|
||||
(ptk/reify ::request-simple-export-wasm
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(case (:type export)
|
||||
:pdf (wasm.exports/export-pdf export)
|
||||
(wasm.exports/export-image export)))))
|
||||
|
||||
(defn request-simple-export
|
||||
[{:keys [export]}]
|
||||
(ptk/reify ::request-simple-export
|
||||
@ -191,11 +200,7 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(if (use-wasm-export? state export)
|
||||
(do
|
||||
(case (:type export)
|
||||
:pdf (wasm.exports/export-pdf export)
|
||||
(wasm.exports/export-image export))
|
||||
(rx/empty))
|
||||
(rx/of (request-simple-export-wasm export))
|
||||
(let [profile-id (:profile-id state)
|
||||
params {:exports [export]
|
||||
:profile-id profile-id
|
||||
|
||||
@ -4,7 +4,7 @@ use macros::wasm_error;
|
||||
use crate::emscripten::init_gl;
|
||||
|
||||
use crate::mem;
|
||||
use crate::render::{gpu_state::GpuState, RenderState};
|
||||
use crate::render::{gpu_state::GpuState, RenderResources, RenderState};
|
||||
use crate::state::{State, TextEditorState, UIState};
|
||||
|
||||
static mut DESIGN_STATE: *mut State = std::ptr::null_mut();
|
||||
@ -23,7 +23,12 @@ static mut GPU_STATE: *mut GpuState = std::ptr::null_mut();
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_gpu_state() -> &'static mut GpuState {
|
||||
unsafe {
|
||||
debug_assert!(!GPU_STATE.is_null(), "GPU State is null");
|
||||
// `assert!` (not `debug_assert!`): a headless instance never inits GPU
|
||||
// state, so an interactive call must fail-fast rather than deref null.
|
||||
assert!(
|
||||
!GPU_STATE.is_null(),
|
||||
"GPU State is null (headless instance?)"
|
||||
);
|
||||
&mut *GPU_STATE
|
||||
}
|
||||
}
|
||||
@ -39,6 +44,17 @@ pub(crate) fn get_render_state() -> &'static mut RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
/// GPU-free resources for the headless export path
|
||||
static mut RENDER_RESOURCES: *mut RenderResources = std::ptr::null_mut();
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_resources() -> &'static mut RenderResources {
|
||||
unsafe {
|
||||
debug_assert!(!RENDER_RESOURCES.is_null(), "Render Resources is null");
|
||||
&mut *RENDER_RESOURCES
|
||||
}
|
||||
}
|
||||
|
||||
/// Text Editor State
|
||||
static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut();
|
||||
|
||||
@ -113,6 +129,14 @@ fn render_init(width: i32, height: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the interactive RenderResources (GPU image store).
|
||||
fn resources_init() {
|
||||
unsafe {
|
||||
let resources = RenderResources::try_new().expect("Cannot initialize RenderResources");
|
||||
RENDER_RESOURCES = Box::into_raw(Box::new(resources));
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes DesignState.
|
||||
fn design_init() {
|
||||
unsafe {
|
||||
@ -143,6 +167,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
gpu_init();
|
||||
resources_init();
|
||||
render_init(width, height);
|
||||
text_editor_init();
|
||||
design_init();
|
||||
|
||||
@ -22,7 +22,7 @@ use std::collections::HashMap;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::{FrameType, RenderFlag};
|
||||
|
||||
use globals::{get_design_state, get_gpu_state, get_render_state};
|
||||
use globals::{get_design_state, get_gpu_state, get_render_state, get_resources};
|
||||
|
||||
use macros::wasm_error;
|
||||
use math::{Bounds, Matrix};
|
||||
@ -720,7 +720,7 @@ pub extern "C" fn is_image_cached(
|
||||
is_thumbnail: bool,
|
||||
) -> Result<bool> {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
let result = get_render_state().has_image(&id, is_thumbnail);
|
||||
let result = get_resources().images.contains(&id, is_thumbnail);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ pub mod grid_layout;
|
||||
mod images;
|
||||
mod options;
|
||||
pub mod pdf;
|
||||
mod resources;
|
||||
pub mod rulers;
|
||||
mod shadows;
|
||||
pub mod shape_renderer;
|
||||
@ -35,10 +36,11 @@ use crate::tiles::{self, PendingTiles, TileRect};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
use crate::wapi;
|
||||
use crate::{get_gpu_state, performance};
|
||||
use crate::{get_gpu_state, get_resources, performance};
|
||||
|
||||
pub use fonts::*;
|
||||
pub use images::*;
|
||||
pub(crate) use resources::RenderResources;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
@ -349,15 +351,12 @@ pub(crate) struct RenderState {
|
||||
pub options: RenderOptions,
|
||||
stats: RenderStats,
|
||||
pub surfaces: Surfaces,
|
||||
pub fonts: FontStore,
|
||||
pub viewbox: Viewbox,
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub images: ImageStore,
|
||||
pub background_color: skia::Color,
|
||||
// Stack of nodes pending to be rendered.
|
||||
pending_nodes: Vec<NodeRenderState>,
|
||||
pub current_tile: Option<tiles::Tile>,
|
||||
pub sampling_options: skia::SamplingOptions,
|
||||
pub render_area: Rect,
|
||||
// render_area expanded by surface margins — used for visibility checks so that
|
||||
// shapes in the margin zone are rendered (needed for background blur sampling).
|
||||
@ -538,19 +537,18 @@ impl RenderState {
|
||||
|
||||
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
|
||||
// This needs to be done once per WebGL context.
|
||||
let sampling_options =
|
||||
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
|
||||
let sampling_options = get_resources().sampling_options;
|
||||
|
||||
let fonts = FontStore::try_new()?;
|
||||
let surfaces = Surfaces::try_new(
|
||||
(width, height),
|
||||
sampling_options,
|
||||
tiles::get_tile_dimensions(),
|
||||
)?;
|
||||
|
||||
// This is used multiple times everywhere so instead of creating new instances every
|
||||
// time we reuse this one.
|
||||
Self::assemble(width, height, surfaces)
|
||||
}
|
||||
|
||||
fn assemble(width: i32, height: i32, surfaces: Surfaces) -> Result<RenderState> {
|
||||
let viewbox = Viewbox::new(width as f32, height as f32);
|
||||
let tiles = tiles::TileHashMap::new();
|
||||
let options = RenderOptions::default();
|
||||
@ -559,14 +557,11 @@ impl RenderState {
|
||||
options,
|
||||
stats: RenderStats::new(),
|
||||
surfaces,
|
||||
fonts,
|
||||
viewbox,
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
images: ImageStore::new(),
|
||||
background_color: skia::Color::TRANSPARENT,
|
||||
pending_nodes: vec![],
|
||||
current_tile: None,
|
||||
sampling_options,
|
||||
render_area: Rect::new_empty(),
|
||||
render_area_with_margins: Rect::new_empty(),
|
||||
tiles,
|
||||
@ -770,35 +765,6 @@ impl RenderState {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn fonts(&self) -> &FontStore {
|
||||
&self.fonts
|
||||
}
|
||||
|
||||
pub fn fonts_mut(&mut self) -> &mut FontStore {
|
||||
&mut self.fonts
|
||||
}
|
||||
|
||||
pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> {
|
||||
self.images.add(id, is_thumbnail, image_data)
|
||||
}
|
||||
|
||||
/// Adds an image from an existing WebGL texture, avoiding re-decoding
|
||||
pub fn add_image_from_gl_texture(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
is_thumbnail: bool,
|
||||
texture_id: u32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<()> {
|
||||
self.images
|
||||
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
|
||||
}
|
||||
|
||||
pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool {
|
||||
self.images.contains(id, is_thumbnail)
|
||||
}
|
||||
|
||||
pub fn set_debug_flags(&mut self, debug: u32) {
|
||||
self.options.flags = debug;
|
||||
}
|
||||
@ -813,7 +779,7 @@ impl RenderState {
|
||||
self.viewbox.width().floor() as i32,
|
||||
self.viewbox.height().floor() as i32,
|
||||
)?;
|
||||
self.fonts.set_scale_debug_font(dpr);
|
||||
get_resources().fonts.set_scale_debug_font(dpr);
|
||||
self.viewbox.set_dpr(dpr);
|
||||
self.surfaces.set_dpr(dpr);
|
||||
}
|
||||
@ -961,7 +927,7 @@ impl RenderState {
|
||||
text_paint.set_color(skia::Color::GRAY);
|
||||
text_paint.set_anti_alias(true);
|
||||
|
||||
let font = self.fonts.debug_font();
|
||||
let font = get_resources().fonts.debug_font();
|
||||
// FIXME
|
||||
let text = "Loading…";
|
||||
let (text_width, _) = font.measure_str(text, None);
|
||||
@ -1398,7 +1364,8 @@ impl RenderState {
|
||||
if let Some(svg) = shape.svg.as_ref() {
|
||||
svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
|
||||
} else {
|
||||
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
|
||||
let font_manager =
|
||||
skia::FontMgr::from(get_resources().fonts.font_provider().clone());
|
||||
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
|
||||
match dom_result {
|
||||
Ok(dom) => {
|
||||
@ -2938,7 +2905,7 @@ impl RenderState {
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
get_resources().sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
@ -2948,7 +2915,7 @@ impl RenderState {
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
get_resources().sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
|
||||
@ -6,6 +6,7 @@ use macros::wasm_error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::get_render_state;
|
||||
use crate::get_resources;
|
||||
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
@ -62,16 +63,16 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
"WebGL rendering"
|
||||
};
|
||||
let dpr = render_state.options.dpr;
|
||||
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
|
||||
let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None);
|
||||
let mut p = skia::Point::new(canvas_width - (20.0 * dpr) - scalar, 50.0 * dpr);
|
||||
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
let debug_font = get_resources().fonts.debug_font();
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
|
||||
if render_state.options.is_text_editor_v3() {
|
||||
str = "Text Editor v3";
|
||||
|
||||
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
|
||||
let (scalar, _) = get_resources().fonts.debug_font().measure_str(str, None);
|
||||
p.x = canvas_width - (20.0 * dpr) - scalar;
|
||||
p.y += 15.0 * dpr;
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
@ -85,7 +86,7 @@ pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
|
||||
paint.set_color(skia::Color::RED);
|
||||
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
|
||||
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
let debug_font = get_resources().fonts.debug_font();
|
||||
canvas.draw_str(str_rect, skia::Point::new(100.0, 150.0), debug_font, &paint);
|
||||
}
|
||||
|
||||
@ -104,7 +105,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
|
||||
|
||||
let str_rect = format!("{} {} {} {}", sx, sy, ex, ey);
|
||||
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
let debug_font = get_resources().fonts.debug_font();
|
||||
canvas.draw_str(str_rect, skia::Point::new(100.0, 100.0), debug_font, &paint);
|
||||
|
||||
let tile_size = tiles::get_tile_size(scale);
|
||||
@ -114,7 +115,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) {
|
||||
let debug_rect = get_debug_rect(rect);
|
||||
let p = skia::Point::new(debug_rect.x(), debug_rect.y() - 1.);
|
||||
let str = format!("{}:{}", x, y);
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
let debug_font = get_resources().fonts.debug_font();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
canvas.draw_rect(debug_rect, &paint);
|
||||
@ -153,7 +154,7 @@ pub fn render_workspace_current_tile(
|
||||
let tile_position_origin = skia::Point::new(rect.x() + 10., rect.y() + 20.);
|
||||
p.set_style(skia::PaintStyle::Fill);
|
||||
let str = format!("{prefix} {}:{}", tile.x(), tile.y());
|
||||
let mut debug_font = render_state.fonts.debug_font().clone();
|
||||
let mut debug_font = get_resources().fonts.debug_font().clone();
|
||||
debug_font.set_size(16.);
|
||||
canvas.draw_str(str, tile_position_origin, &debug_font, &p);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use skia_safe::{self as skia, Paint, RRect};
|
||||
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
use crate::get_resources;
|
||||
use crate::render::get_source_rect;
|
||||
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
||||
|
||||
@ -13,7 +14,7 @@ fn draw_image_fill(
|
||||
antialias: bool,
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
let Some(image) = render_state.images.get(&image_fill.id()) else {
|
||||
let Some(image) = get_resources().images.get(&image_fill.id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@ -83,7 +84,7 @@ fn draw_image_fill(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
dest_rect,
|
||||
render_state.sampling_options,
|
||||
get_resources().sampling_options,
|
||||
paint,
|
||||
);
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ use skia_safe::{self as skia, ImageFilter, Rect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
use crate::get_resources;
|
||||
|
||||
/// Composes two image filters, returning a combined filter if both are present,
|
||||
/// or the individual filter if only one is present, or None if neither is present.
|
||||
@ -51,12 +52,12 @@ where
|
||||
canvas.save();
|
||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||
canvas.translate((bounds.left * scale, bounds.top * scale));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
|
||||
canvas.restore();
|
||||
}
|
||||
Ok(true)
|
||||
|
||||
@ -61,7 +61,8 @@ enum StoredImage {
|
||||
|
||||
pub struct ImageStore {
|
||||
images: HashMap<(Uuid, bool), StoredImage>,
|
||||
context: Box<DirectContext>,
|
||||
/// gpu-only
|
||||
context: Option<Box<DirectContext>>,
|
||||
}
|
||||
|
||||
/// Creates a Skia image from an existing WebGL texture.
|
||||
@ -149,7 +150,7 @@ impl ImageStore {
|
||||
let context = &gpu_state.context;
|
||||
Self {
|
||||
images: HashMap::with_capacity(2048),
|
||||
context: Box::new(context.clone()),
|
||||
context: Some(Box::new(context.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,10 +168,18 @@ impl ImageStore {
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
|
||||
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
|
||||
self.images.insert(key, StoredImage::Gpu(gpu_image));
|
||||
} else {
|
||||
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||
match self.context.as_mut() {
|
||||
Some(context) => {
|
||||
if let Some(gpu_image) = decode_image(context, &raw_data) {
|
||||
self.images.insert(key, StoredImage::Gpu(gpu_image));
|
||||
} else {
|
||||
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||
}
|
||||
}
|
||||
// GPU-free: keep the encoded bytes; decoded on the CPU at draw time.
|
||||
None => {
|
||||
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -193,7 +202,12 @@ impl ImageStore {
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
|
||||
let Some(context) = self.context.as_mut() else {
|
||||
return Err(crate::error::Error::CriticalError(
|
||||
"Cannot register a GL texture without a GPU context".to_string(),
|
||||
));
|
||||
};
|
||||
let image = create_image_from_gl_texture(context, texture_id, width, height)?;
|
||||
self.images.insert(key, StoredImage::Gpu(image));
|
||||
|
||||
Ok(())
|
||||
@ -214,8 +228,27 @@ impl ImageStore {
|
||||
}
|
||||
|
||||
pub fn get_cpu_image(&mut self, id: &Uuid) -> Option<Image> {
|
||||
let gpu_image = self.get(id)?.clone();
|
||||
gpu_image.make_non_texture_image(self.context.as_mut())
|
||||
// GPU path: promote to a texture, then copy to a CPU image.
|
||||
if self.context.is_some() {
|
||||
let gpu_image = self.get(id)?.clone();
|
||||
let context = self.context.as_mut()?;
|
||||
return gpu_image.make_non_texture_image(context.as_mut());
|
||||
}
|
||||
// Headless (no GPU context): decode the stored encoded bytes directly to
|
||||
// a CPU image, which draws fine on a raster/PDF canvas. Try full first,
|
||||
// then thumbnail.
|
||||
self.decode_raw_cpu_image(id, false)
|
||||
.or_else(|| self.decode_raw_cpu_image(id, true))
|
||||
}
|
||||
|
||||
fn decode_raw_cpu_image(&self, id: &Uuid, is_thumbnail: bool) -> Option<Image> {
|
||||
match self.images.get(&(*id, is_thumbnail))? {
|
||||
StoredImage::Raw(raw_data) => {
|
||||
let data = unsafe { skia::Data::new_bytes(raw_data) };
|
||||
Image::from_encoded(&data)
|
||||
}
|
||||
StoredImage::Gpu(img) => Some(img.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
|
||||
@ -225,7 +258,8 @@ impl ImageStore {
|
||||
match entry {
|
||||
StoredImage::Gpu(ref img) => Some(img),
|
||||
StoredImage::Raw(raw_data) => {
|
||||
let gpu_image = decode_image(&mut self.context, raw_data)?;
|
||||
let context = self.context.as_mut()?;
|
||||
let gpu_image = decode_image(context, raw_data)?;
|
||||
*entry = StoredImage::Gpu(gpu_image);
|
||||
|
||||
if let StoredImage::Gpu(ref img) = entry {
|
||||
|
||||
@ -69,6 +69,7 @@ impl RenderOptions {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn set_capture_frames(&mut self, capture_frames: i32) {
|
||||
self.capture_frames = capture_frames;
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use crate::error::Result;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use super::vector::{self, VectorTarget};
|
||||
use super::RenderState;
|
||||
use super::vector;
|
||||
use super::RenderResources;
|
||||
|
||||
/// Renders a shape tree to a PDF document and returns the raw PDF bytes.
|
||||
///
|
||||
@ -16,7 +16,7 @@ use super::RenderState;
|
||||
/// pixel-based (blur, shadows with blur) are rasterised internally by Skia's
|
||||
/// PDF backend
|
||||
pub fn render_to_pdf(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
@ -45,7 +45,7 @@ pub fn render_to_pdf(
|
||||
let page_canvas = on_page.canvas();
|
||||
page_canvas.scale((scale, scale));
|
||||
page_canvas.translate((-bounds.left(), -bounds.top()));
|
||||
vector::render_tree(shared, page_canvas, id, tree, scale, VectorTarget::Pdf)?;
|
||||
vector::render_tree(shared, page_canvas, id, tree, scale)?;
|
||||
}
|
||||
|
||||
let document = on_page.end_page();
|
||||
|
||||
31
render-wasm/src/render/resources.rs
Normal file
31
render-wasm/src/render/resources.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use skia_safe as skia;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
use super::{FontStore, ImageStore};
|
||||
|
||||
/// Render dependencies that the export (PDF) path needs independently of the
|
||||
/// surface/tile/GPU machinery in [`RenderState`]: the font store, the
|
||||
/// decoded-image store, and the default sampling options.
|
||||
///
|
||||
/// Splitting these out keeps the vector/PDF export path (which takes
|
||||
/// `&mut RenderResources`) decoupled from the interactive renderer, and gives a
|
||||
/// single source of truth reached through `get_resources()`.
|
||||
pub(crate) struct RenderResources {
|
||||
pub fonts: FontStore,
|
||||
pub images: ImageStore,
|
||||
pub sampling_options: skia::SamplingOptions,
|
||||
}
|
||||
|
||||
impl RenderResources {
|
||||
pub fn try_new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
fonts: FontStore::try_new()?,
|
||||
images: ImageStore::new(),
|
||||
sampling_options: skia::SamplingOptions::new(
|
||||
skia::FilterMode::Linear,
|
||||
skia::MipmapMode::Nearest,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ use skia_safe::{self as skia, ImageFilter, RRect};
|
||||
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::get_resources;
|
||||
use crate::render::filters::compose_filters;
|
||||
use crate::render::{get_dest_rect, get_source_rect};
|
||||
|
||||
@ -467,7 +468,7 @@ fn draw_image_stroke_in_container(
|
||||
surface_id: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let scale = render_state.get_scale();
|
||||
let Some(image) = render_state.images.get(&image_fill.id()) else {
|
||||
let Some(image) = get_resources().images.get(&image_fill.id()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -575,7 +576,7 @@ fn draw_image_stroke_in_container(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
dest_rect,
|
||||
render_state.sampling_options,
|
||||
get_resources().sampling_options,
|
||||
&image_paint,
|
||||
);
|
||||
|
||||
|
||||
@ -106,6 +106,7 @@ impl DocAtlas {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: delete one docatlas is optional
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.size.width <= 0 || self.size.height <= 0
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use skia_safe::{self as skia, Color4f};
|
||||
|
||||
use super::{RenderState, ShapesPoolRef, SurfaceId};
|
||||
use crate::get_resources;
|
||||
use crate::globals::get_ui_state;
|
||||
use crate::render::{grid_layout, rulers};
|
||||
use crate::shapes::{Layout, Type};
|
||||
@ -64,7 +65,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
|
||||
let viewbox = render_state.viewbox;
|
||||
let ruler_state = render_state.rulers;
|
||||
rulers::render(canvas, viewbox, &render_state.fonts, &ruler_state);
|
||||
rulers::render(canvas, viewbox, &get_resources().fonts, &ruler_state);
|
||||
|
||||
// Width of the ruler bars in document coordinates.
|
||||
// Note that when rulers are hidden, guides are not shown either, so we
|
||||
|
||||
@ -9,19 +9,9 @@ use crate::uuid::Uuid;
|
||||
|
||||
use super::shape_renderer::ShapeRenderer;
|
||||
use super::text;
|
||||
use super::RenderState;
|
||||
use super::RenderResources;
|
||||
use super::{get_dest_rect, get_source_rect};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VectorTarget — vector export backend selector
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Vector export backend selector (PDF today; SVG could be added as a variant).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VectorTarget {
|
||||
Pdf,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VectorRenderer — implements ShapeRenderer for canvas-based vector export
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -29,23 +19,16 @@ pub(super) enum VectorTarget {
|
||||
/// Canvas-based vector render backend (CPU Skia canvas, no GPU surfaces).
|
||||
pub(super) struct VectorRenderer<'a> {
|
||||
canvas: &'a Canvas,
|
||||
shared: &'a mut RenderState,
|
||||
shared: &'a mut RenderResources,
|
||||
scale: f32,
|
||||
_target: VectorTarget,
|
||||
}
|
||||
|
||||
impl<'a> VectorRenderer<'a> {
|
||||
pub fn new(
|
||||
canvas: &'a Canvas,
|
||||
shared: &'a mut RenderState,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
) -> Self {
|
||||
pub fn new(canvas: &'a Canvas, shared: &'a mut RenderResources, scale: f32) -> Self {
|
||||
Self {
|
||||
canvas,
|
||||
shared,
|
||||
scale,
|
||||
_target: target,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -348,12 +331,11 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
||||
|
||||
/// Depth-first render of the shape tree rooted at `id`.
|
||||
pub(super) fn render_tree(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
) -> Result<()> {
|
||||
let Some(element) = tree.get(id) else {
|
||||
return Ok(());
|
||||
@ -365,10 +347,10 @@ pub(super) fn render_tree(
|
||||
|
||||
match &element.shape_type {
|
||||
Type::Group(group) => {
|
||||
render_group(shared, canvas, element, group.masked, tree, scale, target)?;
|
||||
render_group(shared, canvas, element, group.masked, tree, scale)?;
|
||||
}
|
||||
Type::Frame(_) => {
|
||||
render_frame(shared, canvas, element, tree, scale, target)?;
|
||||
render_frame(shared, canvas, element, tree, scale)?;
|
||||
}
|
||||
// Leaf types listed explicitly (no `_`) so a new Type must be handled.
|
||||
Type::Rect(_)
|
||||
@ -377,7 +359,7 @@ pub(super) fn render_tree(
|
||||
| Type::Bool(_)
|
||||
| Type::Text(_)
|
||||
| Type::SVGRaw(_) => {
|
||||
render_leaf(shared, canvas, element, scale, target)?;
|
||||
render_leaf(shared, canvas, element, scale)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,13 +371,12 @@ pub(super) fn render_tree(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn render_group(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
element: &Shape,
|
||||
masked: bool,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
) -> Result<()> {
|
||||
// A group has no geometry of its own and does NOT propagate a transform to
|
||||
// its children: child shapes are stored in absolute coordinates and each
|
||||
@ -404,7 +385,7 @@ fn render_group(
|
||||
canvas.save();
|
||||
|
||||
// Group drop shadow: subtree silhouette, below the opacity/clip layer.
|
||||
render_container_drop_shadows(shared, canvas, element, tree, scale, target, false)?;
|
||||
render_container_drop_shadows(shared, canvas, element, tree, scale, false)?;
|
||||
|
||||
// Layer for opacity / blend mode (and group-level layer blur)
|
||||
let needs_layer = element.needs_layer();
|
||||
@ -437,21 +418,21 @@ fn render_group(
|
||||
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint));
|
||||
|
||||
for child_id in &children {
|
||||
render_tree(shared, canvas, child_id, tree, scale, target)?;
|
||||
render_tree(shared, canvas, child_id, tree, scale)?;
|
||||
}
|
||||
|
||||
if let Some(mask_id) = element.mask_id() {
|
||||
let mut mask_paint = Paint::default();
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&mask_paint));
|
||||
render_tree(shared, canvas, mask_id, tree, scale, target)?;
|
||||
render_tree(shared, canvas, mask_id, tree, scale)?;
|
||||
canvas.restore(); // mask layer
|
||||
}
|
||||
|
||||
canvas.restore(); // composition layer
|
||||
} else {
|
||||
for child_id in &children {
|
||||
render_tree(shared, canvas, child_id, tree, scale, target)?;
|
||||
render_tree(shared, canvas, child_id, tree, scale)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,12 +448,11 @@ fn render_group(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn render_frame(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
element: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
) -> Result<()> {
|
||||
// A frame's own geometry (background, clip, strokes) is placed by its
|
||||
// `centered_transform`, but — like groups — it does NOT propagate that
|
||||
@ -485,7 +465,7 @@ fn render_frame(
|
||||
|
||||
// Frame drop shadow: background + subtree silhouette, below the clip layer
|
||||
// so it extends outside the frame bounds.
|
||||
render_container_drop_shadows(shared, canvas, element, tree, scale, target, true)?;
|
||||
render_container_drop_shadows(shared, canvas, element, tree, scale, true)?;
|
||||
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
@ -524,7 +504,7 @@ fn render_frame(
|
||||
if !element.fills.is_empty() {
|
||||
canvas.save();
|
||||
canvas.concat(&matrix);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale);
|
||||
renderer.draw_fills(element, &element.fills)?;
|
||||
renderer.draw_fill_inner_shadows(element)?;
|
||||
canvas.restore();
|
||||
@ -533,7 +513,7 @@ fn render_frame(
|
||||
// Children (absolute coords, no frame transform).
|
||||
let children: Vec<Uuid> = element.children_ids_iter_forward(false).copied().collect();
|
||||
for child_id in &children {
|
||||
render_tree(shared, canvas, child_id, tree, scale, target)?;
|
||||
render_tree(shared, canvas, child_id, tree, scale)?;
|
||||
}
|
||||
|
||||
// Strokes over children (clipped frames), in the frame's space.
|
||||
@ -541,7 +521,7 @@ fn render_frame(
|
||||
if !visible_strokes.is_empty() {
|
||||
canvas.save();
|
||||
canvas.concat(&matrix);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale);
|
||||
renderer.draw_strokes(element, &visible_strokes)?;
|
||||
canvas.restore();
|
||||
}
|
||||
@ -557,12 +537,11 @@ fn render_frame(
|
||||
/// layer (its alpha becomes the shadow). `draw_fills` includes the frame
|
||||
/// background in the silhouette.
|
||||
fn render_container_drop_shadows(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
element: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
draw_fills: bool,
|
||||
) -> Result<()> {
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
@ -574,13 +553,13 @@ fn render_container_drop_shadows(
|
||||
canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint));
|
||||
|
||||
if draw_fills && !element.fills.is_empty() {
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale);
|
||||
renderer.draw_fills(element, &element.fills)?;
|
||||
}
|
||||
|
||||
let children: Vec<Uuid> = element.children_ids_iter_forward(false).copied().collect();
|
||||
for child_id in &children {
|
||||
render_tree(shared, canvas, child_id, tree, scale, target)?;
|
||||
render_tree(shared, canvas, child_id, tree, scale)?;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
@ -593,11 +572,10 @@ fn render_container_drop_shadows(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn render_leaf(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
element: &Shape,
|
||||
scale: f32,
|
||||
target: VectorTarget,
|
||||
) -> Result<()> {
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
@ -615,7 +593,7 @@ fn render_leaf(
|
||||
canvas.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale, target);
|
||||
let mut renderer = VectorRenderer::new(canvas, shared, scale);
|
||||
|
||||
// Layer blur (non-text shapes)
|
||||
let blur_layer = if !matches!(element.shape_type, Type::Text(_)) {
|
||||
@ -677,7 +655,7 @@ fn render_leaf_content<R: ShapeRenderer + ?Sized>(renderer: &mut R, shape: &Shap
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn draw_image_fill(
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
image_fill: &crate::shapes::ImageFill,
|
||||
@ -719,7 +697,7 @@ fn draw_image_fill(
|
||||
|
||||
fn draw_single_stroke(
|
||||
canvas: &Canvas,
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
scale: f32,
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
@ -830,7 +808,7 @@ fn draw_stroke_kind_aware(canvas: &Canvas, shape: &Shape, stroke: &Stroke, paint
|
||||
/// CPU image over it with `SrcIn` so only the stroke area shows the image.
|
||||
fn draw_image_stroke(
|
||||
canvas: &Canvas,
|
||||
shared: &mut RenderState,
|
||||
shared: &mut RenderResources,
|
||||
scale: f32,
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::get_render_state;
|
||||
use crate::get_resources;
|
||||
use crate::shapes::text::TextContent;
|
||||
use skia_safe::{
|
||||
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
|
||||
@ -174,7 +174,7 @@ impl TextPaths {
|
||||
) -> Option<(skia::Path, skia::Rect)> {
|
||||
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
|
||||
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
||||
let emoji_font = get_render_state().fonts().get_emoji_font(font.size());
|
||||
let emoji_font = get_resources().fonts.get_emoji_font(font.size());
|
||||
let use_font = emoji_font.as_ref().unwrap_or(font);
|
||||
|
||||
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::error::{Error, Result};
|
||||
use crate::render::FrameType;
|
||||
use crate::shapes::{grid_layout::grid_cell_data, Shape};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::{get_render_state, tiles};
|
||||
use crate::{get_render_state, get_resources, tiles};
|
||||
|
||||
/// This struct holds the state of the Rust application between JS calls.
|
||||
///
|
||||
@ -97,7 +97,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn render_shape_pdf(&mut self, id: &Uuid, scale: f32) -> Result<Vec<u8>> {
|
||||
crate::render::pdf::render_to_pdf(get_render_state(), id, &self.shapes, scale)
|
||||
crate::render::pdf::render_to_pdf(get_resources(), id, &self.shapes, scale)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
@ -266,7 +266,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn font_collection(&self) -> &FontCollection {
|
||||
get_render_state().fonts().font_collection()
|
||||
get_resources().fonts.font_collection()
|
||||
}
|
||||
|
||||
pub fn get_grid_coords(&self, pos_x: f32, pos_y: f32) -> Option<(i32, i32)> {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::get_render_state;
|
||||
use crate::get_resources;
|
||||
use crate::skia::textlayout::FontCollection;
|
||||
use crate::skia::Image;
|
||||
use crate::uuid::Uuid;
|
||||
@ -25,12 +25,12 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid {
|
||||
}
|
||||
|
||||
pub fn get_image(image_id: &Uuid) -> Option<&Image> {
|
||||
get_render_state().images.get(image_id)
|
||||
get_resources().images.get(image_id)
|
||||
}
|
||||
|
||||
// FIXME: move to a different place ?
|
||||
pub fn get_fallback_fonts() -> &'static HashSet<String> {
|
||||
get_render_state().fonts().get_fallback()
|
||||
get_resources().fonts.get_fallback()
|
||||
}
|
||||
|
||||
pub fn get_font_collection() -> &'static FontCollection {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::get_render_state;
|
||||
use crate::get_resources;
|
||||
use crate::mem;
|
||||
use crate::shapes::Fill;
|
||||
use crate::state::State;
|
||||
@ -104,7 +104,10 @@ pub extern "C" fn store_image() -> Result<()> {
|
||||
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
|
||||
|
||||
with_state!(state, {
|
||||
if let Err(msg) = get_render_state().add_image(ids.image_id, is_thumbnail, image_bytes) {
|
||||
if let Err(msg) = get_resources()
|
||||
.images
|
||||
.add(ids.image_id, is_thumbnail, image_bytes)
|
||||
{
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
touch_shapes_with_image(state, ids.image_id);
|
||||
@ -174,7 +177,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> {
|
||||
);
|
||||
|
||||
with_state!(state, {
|
||||
if let Err(msg) = get_render_state().add_image_from_gl_texture(
|
||||
if let Err(msg) = get_resources().images.add_image_from_gl_texture(
|
||||
ids.image_id,
|
||||
is_thumbnail,
|
||||
texture_id,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::get_render_state;
|
||||
use crate::get_resources;
|
||||
use crate::mem;
|
||||
use crate::shapes::{FontFamily, FontStyle};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
@ -45,8 +45,8 @@ pub extern "C" fn store_font(
|
||||
let font_style = RawFontStyle::from(style);
|
||||
|
||||
let family = FontFamily::new(id, weight, font_style.into());
|
||||
let _ = get_render_state()
|
||||
.fonts_mut()
|
||||
let _ = get_resources()
|
||||
.fonts
|
||||
.add(family, &font_bytes, is_emoji, is_fallback);
|
||||
|
||||
mem::free_bytes()?;
|
||||
@ -66,7 +66,5 @@ pub extern "C" fn is_font_uploaded(
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
let font_style = RawFontStyle::from(style);
|
||||
let family = FontFamily::new(id, weight, font_style.into());
|
||||
let res = get_render_state().fonts().has_family(&family, is_emoji);
|
||||
|
||||
res
|
||||
get_resources().fonts.has_family(&family, is_emoji)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user