| 1 | use canvas::gpu::{Paper, Viewport, page::PageScene}; |
| 2 | use canvas::layout::TextEngine; |
| 3 | use draw::Renderer; |
| 4 | use onestore::page::Page; |
| 5 | use onestore::{RevisionIndex, Store, document::Document}; |
| 6 | use std::{env, fs, sync::Arc, time::Duration}; |
| 7 | |
| 8 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 9 | let mut args = env::args().skip(1).peekable(); |
| 10 | let path = args |
| 11 | .next() |
| 12 | .ok_or("Expected a section snapshot, page title and new PNG path")?; |
| 13 | let title = args.next().ok_or("Expected a page title")?; |
| 14 | let destination = args.next().ok_or("Expected a new PNG path")?; |
| 15 | let origin_y: f32 = if args |
| 16 | .peek() |
| 17 | .is_some_and(|value| value != "--substitute-font") |
| 18 | { |
| 19 | args.next().unwrap().parse()? |
| 20 | } else { |
| 21 | 0.0 |
| 22 | }; |
| 23 | let mut engine = TextEngine::default(); |
| 24 | while let Some(option) = args.next() { |
| 25 | if option != "--substitute-font" { |
| 26 | return Err(format!("Unknown option: {option}").into()); |
| 27 | } |
| 28 | let path = args |
| 29 | .next() |
| 30 | .ok_or("Provide a font file after --substitute-font.")?; |
| 31 | engine.register_substitute(parley::fontique::Blob::new(Arc::new(fs::read(path)?)))?; |
| 32 | } |
| 33 | let page = { |
| 34 | let bytes = fs::read(path)?; |
| 35 | let store = Store::parse(&bytes)?; |
| 36 | let index = RevisionIndex::parse(&store)?; |
| 37 | Page::from_document(&Document::parse(&index)?, &title)? |
| 38 | }; |
| 39 | let margin_origin = page.margin_origin; |
| 40 | let object_count = page.objects.len(); |
| 41 | let mut scene = PageScene::new(page, &mut engine)?; |
| 42 | scene.settle(None, 96.0 / 72.0, Paper::WHITE); |
| 43 | let mut primitives = Vec::new(); |
| 44 | scene.append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)?; |
| 45 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); |
| 46 | let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?; |
| 47 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?; |
| 48 | let size = [1386_u32, 759]; |
| 49 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; |
| 50 | let target = device.create_texture(&wgpu::TextureDescriptor { |
| 51 | label: Some("Native page comparison"), |
| 52 | size: wgpu::Extent3d { |
| 53 | width: size[0], |
| 54 | height: size[1], |
| 55 | depth_or_array_layers: 1, |
| 56 | }, |
| 57 | mip_level_count: 1, |
| 58 | sample_count: 1, |
| 59 | dimension: wgpu::TextureDimension::D2, |
| 60 | format, |
| 61 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, |
| 62 | view_formats: &[], |
| 63 | }); |
| 64 | let row_bytes = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); |
| 65 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { |
| 66 | label: Some("Native page comparison readback"), |
| 67 | size: u64::from(row_bytes) * u64::from(size[1]), |
| 68 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, |
| 69 | mapped_at_creation: false, |
| 70 | }); |
| 71 | let mut renderer = Renderer::new(device, queue, format); |
| 72 | let viewport = Viewport { |
| 73 | size, |
| 74 | scale: 96.0 / 72.0, |
| 75 | origin: [ |
| 76 | (36.0 - margin_origin[0]) * (96.0 / 72.0), |
| 77 | (14.4 - margin_origin[1]) * (96.0 / 72.0) + origin_y, |
| 78 | ], |
| 79 | }; |
| 80 | renderer |
| 81 | .draw( |
| 82 | &target.create_view(&Default::default()).into(), |
| 83 | size, |
| 84 | [1.0; 4], |
| 85 | &[viewport.layer(&primitives)], |
| 86 | ) |
| 87 | .map_err(|e| format!("Page comparison rendering failed: {e:?}"))?; |
| 88 | let mut encoder = renderer |
| 89 | .device() |
| 90 | .create_command_encoder(&Default::default()); |
| 91 | encoder.copy_texture_to_buffer( |
| 92 | target.as_image_copy(), |
| 93 | wgpu::TexelCopyBufferInfo { |
| 94 | buffer: &buffer, |
| 95 | layout: wgpu::TexelCopyBufferLayout { |
| 96 | offset: 0, |
| 97 | bytes_per_row: Some(row_bytes), |
| 98 | rows_per_image: Some(size[1]), |
| 99 | }, |
| 100 | }, |
| 101 | wgpu::Extent3d { |
| 102 | width: size[0], |
| 103 | height: size[1], |
| 104 | depth_or_array_layers: 1, |
| 105 | }, |
| 106 | ); |
| 107 | renderer.queue().submit([encoder.finish()]); |
| 108 | let (sender, receiver) = std::sync::mpsc::channel(); |
| 109 | buffer.map_async(wgpu::MapMode::Read, .., move |result| { |
| 110 | let _ = sender.send(result); |
| 111 | }); |
| 112 | renderer.device().poll(wgpu::PollType::Wait { |
| 113 | submission_index: None, |
| 114 | timeout: Some(Duration::from_secs(10)), |
| 115 | })?; |
| 116 | receiver.recv_timeout(Duration::from_secs(10))??; |
| 117 | let mapped = buffer.get_mapped_range(..)?; |
| 118 | let pixels: Vec<_> = mapped |
| 119 | .chunks_exact(row_bytes as usize) |
| 120 | .flat_map(|row| row[..size[0] as usize * 4].iter().copied()) |
| 121 | .collect(); |
| 122 | let mut encoder = png::Encoder::new(fs::File::create_new(destination)?, size[0], size[1]); |
| 123 | encoder.set_color(png::ColorType::Rgba); |
| 124 | encoder.set_depth(png::BitDepth::Eight); |
| 125 | encoder.write_header()?.write_image_data(&pixels)?; |
| 126 | eprintln!( |
| 127 | "Rendered {} page objects at 100% / 96 DPI, origin Y {origin_y} px using {:?}", |
| 128 | object_count, |
| 129 | adapter.get_info() |
| 130 | ); |
| 131 | Ok(()) |
| 132 | } |