mod geometry;
mod pdf;
mod request;
mod sheet;
use std::ffi::CStr;
use std::sync::atomic::{ AtomicU64, Ordering };
use std::sync::mpsc::{ self, Sender };
use std::sync::{ Arc, Mutex, Weak };
use wpe_platform_sys::*;
use wpe_webkit_sys::*;
use super::{ scripting, snapshot, Inner, WebView };
use crate::types::{ Preview, PreviewPage, PrintError, PrintSettings, Snapshot };
use geometry::Geometry;
pub( super ) use request::connect;
const PAGE_JS: &str = include_str!( "page.js" );
const WORLD: &[ u8 ] = b"ltk-print\0";
const MAX_IN_FLIGHT: usize = 2;
static NEXT_JOB: AtomicU64 = AtomicU64::new( 1 );
enum Output
{
Pdf( Box<dyn FnOnce( Result<Vec<u8>, PrintError> )> ),
Preview( u32, Box<dyn FnOnce( Result<Preview, PrintError> )> ),
}
impl Output
{
fn fail( self, error: PrintError )
{
match self
{
Output::Pdf( done ) => done( Err( error ) ),
Output::Preview( _, done ) => done( Err( error ) ),
}
}
fn deliver( self, result: Result<Product, PrintError> )
{
match ( self, result )
{
( Output::Pdf( done ), Ok( Product::Pdf( pdf ) ) ) => done( Ok( pdf ) ),
( Output::Preview( _, done ), Ok( Product::Preview( preview ) ) ) => done( Ok( preview ) ),
( output, Err( error ) ) => output.fail( error ),
( output, Ok( _ ) ) => output.fail( PrintError::Failed( "rendered the wrong output".to_string() ) ),
}
}
}
enum Rendered
{
Jpeg( pdf::Page ),
Sheet( PreviewPage ),
}
enum Product
{
Pdf( Vec<u8> ),
Preview( Preview ),
}
pub( super ) struct Job
{
id: u64,
geometry: Geometry,
backgrounds: bool,
ranges: Vec<( u32, u32 )>,
saved_zoom: f64,
preview: Option<u32>,
done: Option<Output>,
tops: Vec<f64>,
end: f64,
order: Vec<usize>,
next: usize,
pages: Vec<Option<Rendered>>,
in_flight: usize,
stalled: bool,
encoder: Option<Sender<Encode>>,
result: Option<Result<Product, PrintError>>,
finishing: bool,
}
struct Encode
{
job: u64,
slot: usize,
index: usize,
snapshot: Snapshot,
first: u32,
rows: u32,
height: u32,
geometry: Geometry,
preview: Option<u32>,
}
impl WebView
{
pub fn print_to_pdf( &self, settings: &PrintSettings, done: impl FnOnce( Result<Vec<u8>, PrintError> ) + 'static )
{
self.start_print( settings, geometry::PRINT_DPI, Output::Pdf( Box::new( done ) ) );
}
pub fn print_preview( &self, settings: &PrintSettings, width: u32, done: impl FnOnce( Result<Preview, PrintError> ) + 'static )
{
let width = width.max( 1 );
self.start_print( settings, geometry::preview_dpi( settings, width ), Output::Preview( width, Box::new( done ) ) );
}
fn start_print( &self, settings: &PrintSettings, dpi: f64, output: Output )
{
let failed = |reason: &str| PrintError::Failed( reason.to_string() );
let Some( webview ) = self.webview_ptr() else { return output.fail( failed( "no view" ) ) };
if let Err( e ) = geometry::check( &settings.pages ) { return output.fail( e ); }
let saved_zoom = self.zoom();
let Ok( mut g ) = self.inner.lock() else { return output.fail( failed( "the view is poisoned" ) ) };
if g.print_job.is_some()
{
drop( g );
return output.fail( PrintError::Busy );
}
let geometry = match geometry::geometry( settings, g.scale, dpi )
{
Ok( geometry ) => geometry,
Err( e ) =>
{
drop( g );
return output.fail( e );
}
};
let id = NEXT_JOB.fetch_add( 1, Ordering::Relaxed );
g.print_job = Some( Job
{
id,
geometry,
backgrounds: settings.backgrounds,
ranges: settings.pages.clone(),
saved_zoom,
preview: match output { Output::Preview( width, _ ) => Some( width ), Output::Pdf( _ ) => None },
done: Some( output ),
tops: Vec::new(),
end: 0.0,
order: Vec::new(),
next: 0,
pages: Vec::new(),
in_flight: 0,
stalled: false,
encoder: None,
result: None,
finishing: false,
} );
let view = g.view;
drop( g );
self.set_zoom( geometry.zoom );
resize_view( view, geometry.view );
prepare( Arc::downgrade( &self.inner ), webview, id );
}
pub fn take_print_request( &self ) -> bool
{
self.inner.lock().map( |mut g| std::mem::take( &mut g.print_requested ) ).unwrap_or( false )
}
}
fn resize_view( view: *mut WPEView, ( width, height ): ( i32, i32 ) )
{
if view.is_null() { return; }
unsafe
{
let top = wpe_view_get_toplevel( view );
if !top.is_null() { wpe_toplevel_resize( top, width, height ); }
}
}
fn with_job<R>( weak: &Weak<Mutex<Inner>>, id: u64, step: impl FnOnce( &mut Job, *mut WebKitWebView ) -> R ) -> Option<R>
{
let inner = weak.upgrade()?;
let mut g = inner.lock().ok()?;
let webview = g.webview;
let job = g.print_job.as_mut().filter( |job| job.id == id && !job.finishing )?;
Some( step( job, webview ) )
}
fn numbers( json: &str ) -> Vec<f64>
{
json.trim_matches( '"' ).split_whitespace().filter_map( |n| n.parse().ok() ).collect()
}
fn prepare( weak: Weak<Mutex<Inner>>, webview: *mut WebKitWebView, id: u64 )
{
let Some( args ) = with_job( &weak, id, |job, _| format!( "{{ width: {}, pageHeight: {}, backgrounds: {} }}", job.geometry.css.0, job.geometry.css.1, job.backgrounds ) ) else { return };
let body = format!( "{PAGE_JS}\nreturn await ltkPrint.prepare( {args} );" );
scripting::call_async_in_world( webview, &body, WORLD, move |result|
{
let laid_out = result.and_then( |json|
{
let n = numbers( &json );
if n.len() < 4 { Err( format!( "unexpected layout {json}" ) ) } else { Ok( n ) }
} );
let started = with_job( &weak, id, |job, _| -> Result<(), PrintError>
{
let n = laid_out.map_err( PrintError::Failed )?;
job.end = n[ 2 ];
job.tops = n[ 3.. ].to_vec();
job.order = geometry::select( &job.ranges, job.tops.len() )?;
job.pages = job.order.iter().map( |_| None ).collect();
job.encoder = Some( spawn_encoder( weak.clone() ) );
Ok( () )
} );
match started
{
Some( Ok( () ) ) => next_page( weak, id ),
Some( Err( e ) ) => finish( weak, id, Some( e ) ),
None => {}
}
} );
}
fn next_page( weak: Weak<Mutex<Inner>>, id: u64 )
{
enum Next { Shoot( *mut WebKitWebView, usize, f64 ), Wait, Done }
let next = with_job( &weak, id, |job, webview|
{
if job.next >= job.order.len() { return if job.in_flight == 0 { Next::Done } else { Next::Wait }; }
if job.in_flight >= MAX_IN_FLIGHT
{
job.stalled = true;
return Next::Wait;
}
let page = job.order[ job.next ];
Next::Shoot( webview, job.next, job.tops[ page ] )
} );
let ( webview, slot, top ) = match next
{
Some( Next::Shoot( webview, slot, top ) ) => ( webview, slot, top ),
Some( Next::Done ) => return finish( weak, id, None ),
_ => return,
};
let body = format!( "{PAGE_JS}\nreturn await ltkPrint.scroll( {top} );" );
scripting::call_async_in_world( webview, &body, WORLD, move |result|
{
let scrolled = match result.map( |json| numbers( &json ) )
{
Ok( n ) if n.len() == 2 => ( n[ 0 ], n[ 1 ] ),
Ok( _ ) => return finish( weak, id, Some( PrintError::Failed( "unexpected scroll".to_string() ) ) ),
Err( e ) => return finish( weak, id, Some( PrintError::Failed( e ) ) ),
};
snapshot::snapshot( webview, WEBKIT_SNAPSHOT_REGION_VISIBLE, move |shot|
{
let queued = with_job( &weak, id, |job, _| -> Result<(), PrintError>
{
let shot = shot.map_err( PrintError::Failed )?;
let encode = cut( job, slot, top, scrolled, shot );
job.encoder.as_ref().and_then( |tx| tx.send( encode ).ok() ).ok_or_else( || PrintError::Failed( "the encoder stopped".to_string() ) )?;
job.in_flight += 1;
job.next += 1;
Ok( () )
} );
match queued
{
Some( Ok( () ) ) => next_page( weak, id ),
Some( Err( e ) ) => finish( weak, id, Some( e ) ),
None => {}
}
} );
} );
}
fn cut( job: &Job, slot: usize, top: f64, scrolled: ( f64, f64 ), shot: Snapshot ) -> Encode
{
let page = job.order[ slot ];
let bottom = job.tops.get( page + 1 ).copied().unwrap_or( job.end );
let ratio = shot.height as f64 / scrolled.1.max( 1.0 );
let ( area_width, area_height ) = ( job.geometry.area.2, job.geometry.area.3 );
let height = ( shot.width as f64 * area_height / area_width ).round().max( 1.0 ) as u32;
Encode
{
job: job.id,
slot,
index: page,
first: ( ( top - scrolled.0 ).max( 0.0 ) * ratio ).round() as u32,
rows: ( ( bottom - top ).max( 0.0 ) * ratio ).round() as u32,
height,
snapshot: shot,
geometry: job.geometry,
preview: job.preview,
}
}
fn spawn_encoder( weak: Weak<Mutex<Inner>> ) -> Sender<Encode>
{
let ( tx, rx ) = mpsc::channel::<Encode>();
std::thread::spawn( move ||
{
for page in rx
{
let shot = &page.snapshot;
let encoded = match page.preview
{
None => pdf::encode( &shot.rgba, shot.width, page.first, page.rows, page.height ).map( Rendered::Jpeg ),
Some( width ) => sheet::sheet( &shot.rgba, shot.width, page.first, page.rows, page.height, &page.geometry, width )
.map( |( rgba, width, height )| Rendered::Sheet( PreviewPage { index: page.index, rgba, width, height } ) ),
};
let ( weak, id, slot ) = ( weak.clone(), page.job, page.slot );
glib::MainContext::default().invoke( move || encoded_page( weak, id, slot, encoded ) );
}
} );
tx
}
fn encoded_page( weak: Weak<Mutex<Inner>>, id: u64, slot: usize, encoded: Result<Rendered, String> )
{
let resume = with_job( &weak, id, |job, _|
{
job.in_flight -= 1;
match encoded
{
Ok( page ) =>
{
job.pages[ slot ] = Some( page );
Ok( std::mem::take( &mut job.stalled ) || job.next >= job.order.len() )
}
Err( e ) => Err( PrintError::Failed( e ) ),
}
} );
match resume
{
Some( Ok( true ) ) => next_page( weak, id ),
Some( Err( e ) ) => finish( weak, id, Some( e ) ),
_ => {}
}
}
fn finish( weak: Weak<Mutex<Inner>>, id: u64, error: Option<PrintError> )
{
let started = with_job( &weak, id, |job, webview|
{
job.finishing = true;
job.encoder = None;
job.result = Some( match error
{
Some( e ) => Err( e ),
None =>
{
let rendered = job.pages.drain( .. ).flatten();
match job.preview
{
None =>
{
let pages: Vec<pdf::Page> = rendered.filter_map( |r| match r { Rendered::Jpeg( page ) => Some( page ), Rendered::Sheet( _ ) => None } ).collect();
Ok( Product::Pdf( pdf::write( &pages, &job.geometry, &title( webview ) ) ) )
}
Some( _ ) =>
{
let pages = rendered.filter_map( |r| match r { Rendered::Sheet( page ) => Some( page ), Rendered::Jpeg( _ ) => None } ).collect();
Ok( Product::Preview( Preview { total: job.tops.len(), pages } ) )
}
}
}
} );
webview
} );
let Some( webview ) = started else { return };
let body = format!( "{PAGE_JS}\nreturn await ltkPrint.restore();" );
scripting::call_async_in_world( webview, &body, WORLD, move |_| complete( weak, id ) );
}
fn complete( weak: Weak<Mutex<Inner>>, id: u64 )
{
let Some( inner ) = weak.upgrade() else { return };
let Ok( mut g ) = inner.lock() else { return };
if g.print_job.as_ref().is_none_or( |job| job.id != id ) { return; }
let Some( mut job ) = g.print_job.take() else { return };
g.needs_redraw = true;
let ( webview, view, size ) = ( g.webview, g.view, g.logical_size() );
drop( g );
if !webview.is_null()
{
unsafe { webkit_web_view_set_zoom_level( webview, job.saved_zoom ); }
}
resize_view( view, size );
let result = job.result.take().unwrap_or_else( || Err( PrintError::Failed( "printing stopped".to_string() ) ) );
if let Some( done ) = job.done.take() { done.deliver( result ); }
}
fn title( webview: *mut WebKitWebView ) -> String
{
if webview.is_null() { return String::new(); }
unsafe
{
let title = webkit_web_view_get_title( webview );
if title.is_null() { String::new() } else { CStr::from_ptr( title ).to_string_lossy().into_owned() }
}
}