use std::io::Write;
use image::codecs::jpeg::JpegEncoder;
use image::ExtendedColorType;
use super::geometry::Geometry;
const QUALITY: u8 = 88;
pub( super ) struct Page
{
jpeg: Vec<u8>,
width: u32,
height: u32,
}
pub( super ) fn encode( rgba: &[ u8 ], width: u32, first: u32, rows: u32, height: u32 ) -> Result<Page, String>
{
if width == 0 || height == 0 { return Err( "empty page".to_string() ); }
let rgb = flatten( rgba, width, first, rows, height );
let mut jpeg = Vec::new();
JpegEncoder::new_with_quality( &mut jpeg, QUALITY ).encode( &rgb, width, height, ExtendedColorType::Rgb8 ).map_err( |e| e.to_string() )?;
Ok( Page { jpeg, width, height } )
}
pub( super ) fn flatten( rgba: &[ u8 ], width: u32, first: u32, rows: u32, height: u32 ) -> Vec<u8>
{
let ( w, h ) = ( width as usize, height as usize );
let mut rgb = vec![ 255u8; w * h * 3 ];
if w == 0 { return rgb; }
let available = rgba.len() / ( w * 4 );
let first = ( first as usize ).min( available );
let rows = ( rows as usize ).min( available - first ).min( h );
for ( px, out ) in rgba[ first * w * 4..( first + rows ) * w * 4 ].chunks_exact( 4 ).zip( rgb.chunks_exact_mut( 3 ) )
{
let rest = 255 - px[ 3 ];
out.copy_from_slice( &[ px[ 0 ].saturating_add( rest ), px[ 1 ].saturating_add( rest ), px[ 2 ].saturating_add( rest ) ] );
}
rgb
}
pub( super ) fn write( pages: &[ Page ], geometry: &Geometry, title: &str ) -> Vec<u8>
{
let mut out: Vec<u8> = b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n".to_vec();
let mut offsets: Vec<usize> = Vec::new();
let object = |out: &mut Vec<u8>, offsets: &mut Vec<usize>, body: &[ u8 ]|
{
offsets.push( out.len() );
let _ = write!( out, "{} 0 obj\n", offsets.len() );
out.extend_from_slice( body );
out.extend_from_slice( b"\nendobj\n" );
};
let ( paper_width, paper_height ) = geometry.paper;
let ( left, bottom, area_width, area_height ) = geometry.area;
object( &mut out, &mut offsets, b"<< /Type /Catalog /Pages 2 0 R >>" );
let kids: Vec<String> = ( 0..pages.len() ).map( |i| format!( "{} 0 R", 4 + i * 3 ) ).collect();
object( &mut out, &mut offsets, format!( "<< /Type /Pages /Kids [ {} ] /Count {} >>", kids.join( " " ), pages.len() ).as_bytes() );
object( &mut out, &mut offsets, format!( "<< /Title {} /Producer (ltk-webkit) >>", text_string( title ) ).as_bytes() );
for ( i, page ) in pages.iter().enumerate()
{
let ( contents, image ) = ( 5 + i * 3, 6 + i * 3 );
object( &mut out, &mut offsets, format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [ 0 0 {paper_width:.2} {paper_height:.2} ] /Resources << /XObject << /Im0 {image} 0 R >> >> /Contents {contents} 0 R >>" ).as_bytes() );
let draw = format!( "q {area_width:.2} 0 0 {area_height:.2} {left:.2} {bottom:.2} cm /Im0 Do Q" );
object( &mut out, &mut offsets, format!( "<< /Length {} >>\nstream\n{draw}\nendstream", draw.len() ).as_bytes() );
let mut body = format!(
"<< /Type /XObject /Subtype /Image /Width {} /Height {} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length {} >>\nstream\n",
page.width, page.height, page.jpeg.len() ).into_bytes();
body.extend_from_slice( &page.jpeg );
body.extend_from_slice( b"\nendstream" );
object( &mut out, &mut offsets, &body );
}
let xref = out.len();
let _ = write!( out, "xref\n0 {}\n0000000000 65535 f \n", offsets.len() + 1 );
for offset in &offsets { let _ = write!( out, "{offset:010} 00000 n \n" ); }
let _ = write!( out, "trailer\n<< /Size {} /Root 1 0 R /Info 3 0 R >>\nstartxref\n{xref}\n%%EOF\n", offsets.len() + 1 );
out
}
fn text_string( text: &str ) -> String
{
let mut out = String::from( "<FEFF" );
for unit in text.encode_utf16() { out.push_str( &format!( "{unit:04X}" ) ); }
out.push( '>' );
out
}
#[ cfg( test ) ]
mod tests
{
use super::*;
use crate::types::PrintSettings;
fn geometry() -> Geometry
{
super::super::geometry::geometry( &PrintSettings::default(), 1.0 ).unwrap()
}
fn black( width: u32, rows: u32 ) -> Vec<u8>
{
vec![ 0, 0, 0, 255 ].repeat( ( width * rows ) as usize )
}
#[ test ]
fn one_object_per_page_part()
{
let pages: Vec<Page> = ( 0..3 ).map( |_| encode( &black( 10, 10 ), 10, 0, 10, 14 ).unwrap() ).collect();
let pdf = String::from_utf8_lossy( &write( &pages, &geometry(), "TÃtulo" ) ).into_owned();
assert!( pdf.starts_with( "%PDF-1.4" ) && pdf.ends_with( "%%EOF\n" ) );
assert!( pdf.contains( "/Count 3" ) );
assert!( pdf.contains( "/Title <FEFF005400ED00740075006C006F>" ) );
}
#[ test ]
fn the_xref_points_at_each_object()
{
let pages = vec![ encode( &black( 10, 10 ), 10, 0, 10, 10 ).unwrap() ];
let bytes = write( &pages, &geometry(), "" );
let text = String::from_utf8_lossy( &bytes ).into_owned();
let xref = text.rfind( "xref\n" ).unwrap();
for ( n, line ) in text[ xref.. ].lines().skip( 3 ).take_while( |l| l.ends_with( " n " ) ).enumerate()
{
let offset: usize = line[ ..10 ].parse().unwrap();
assert!( text[ offset.. ].starts_with( &format!( "{} 0 obj", n + 1 ) ) );
}
}
#[ test ]
fn rows_past_the_image_are_white()
{
let page = encode( &black( 16, 16 ), 16, 8, 8, 32 ).unwrap();
assert_eq!( ( page.width, page.height ), ( 16, 32 ) );
let decoded = image::load_from_memory( &page.jpeg ).unwrap().to_rgb8();
assert!( decoded.get_pixel( 8, 2 ).0.iter().all( |&c| c < 40 ) );
assert!( decoded.get_pixel( 8, 28 ).0.iter().all( |&c| c > 215 ) );
}
#[ test ]
fn an_empty_page_is_refused()
{
assert!( encode( &[], 0, 0, 0, 0 ).is_err() );
}
}