diff --git a/Cargo.lock b/Cargo.lock index 49635a72c..c5c129bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4601,6 +4601,7 @@ dependencies = [ "base64", "flate2", "getrandom 0.3.4", + "image", "lopdf", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 1467496a5..f7d03a426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ meshopt = "0.6.2" i18n-embed = { version = "0.16", features = ["desktop-requester"] } rayon = "1" open = "5" -printpdf = { version = "0.9.1", default-features = false } +printpdf = { version = "0.9.1", default-features = false, features = ["images"] } ureq = { version = "3", default-features = false, features = ["rustls", "platform-verifier", "win-system-proxy"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 1bad3d3ce..c6e699748 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -92,33 +92,7 @@ fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool { } } -type LayoutPlotParams = ( - std::sync::Arc>, - Vec, - Vec, - crate::io::pdf_export::PlotGroupSplits, - f64, - f64, - f64, - f64, - i32, - f32, - Option<(f32, f32, f32, f32)>, -); - -type ClippedPlotParams = ( - Vec, - Vec, - Vec, - crate::io::pdf_export::PlotGroupSplits, - f64, - f64, - f64, - f64, - i32, - f32, - Option<(f32, f32, f32, f32)>, -); +use crate::io::pdf_export::{PdfPageInput, PlotContent}; fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f64) { use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize}; @@ -149,11 +123,8 @@ fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f } } -fn plot_content_extents( - wires: &[crate::io::pdf_export::PlotWire], - hatches: &[crate::scene::model::hatch_model::HatchModel], - wipeouts: &[crate::scene::model::hatch_model::HatchModel], -) -> Option<(f64, f64, f64, f64)> { +fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> { + let PlotContent { wires, hatches, wipeouts, images, .. } = content; let mut bounds = ( f64::INFINITY, f64::INFINITY, @@ -168,7 +139,7 @@ fn plot_content_extents( bounds.3 = bounds.3.max(y); } }; - for wire in wires { + for wire in wires.iter().filter(|wire| wire.name != "paper_printable_area") { let [x0, y0, x1, y1] = wire.aabb; include(x0 as f64, y0 as f64); include(x1 as f64, y1 as f64); @@ -178,6 +149,29 @@ fn plot_content_extents( include(hatch.world_origin[0] + x as f64, hatch.world_origin[1] + y as f64); } } + for plot in images { + let mut image_bounds = [f64::INFINITY, f64::INFINITY, + f64::NEG_INFINITY, f64::NEG_INFINITY]; + for vertex in &plot.image.verts { + for axis in 0..2 { + let value = vertex.pos[axis] as f64 + vertex.pos_low[axis] as f64; + image_bounds[axis] = image_bounds[axis].min(value); + image_bounds[axis + 2] = image_bounds[axis + 2].max(value); + } + } + for clip in &plot.clips { + for axis in 0..2 { + let min = clip.iter().map(|p| p[axis]).fold(f64::INFINITY, f64::min); + let max = clip.iter().map(|p| p[axis]).fold(f64::NEG_INFINITY, f64::max); + image_bounds[axis] = image_bounds[axis].max(min); + image_bounds[axis + 2] = image_bounds[axis + 2].min(max); + } + } + if image_bounds[0] < image_bounds[2] && image_bounds[1] < image_bounds[3] { + include(image_bounds[0], image_bounds[1]); + include(image_bounds[2], image_bounds[3]); + } + } (bounds.0.is_finite() && bounds.1.is_finite() && bounds.2.is_finite() @@ -191,12 +185,7 @@ fn plot_scene_content( scene: &crate::scene::Scene, paper_space_last: bool, render_mode_override: Option, -) -> ( - std::sync::Arc>, - Vec, - Vec, - crate::io::pdf_export::PlotGroupSplits, -) { +) -> PlotContent { let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override); let plot_viewport_borders = scene .effective_plot_settings() @@ -230,20 +219,23 @@ fn plot_scene_content( let model_wires = with_depth(model_wires); let paper_hatches = scene.paper_plot_hatches().as_ref().clone(); let paper_wipeouts = scene.paper_plot_wipeouts().as_ref().clone(); + let paper_images = scene.paper_plot_images(); if scene.current_layout == "Model" { let splits = crate::io::pdf_export::PlotGroupSplits { wires: paper_wires.len(), hatches: paper_hatches.len(), wipeouts: paper_wipeouts.len(), + images: paper_images.len(), + }; + return PlotContent { + wires: std::sync::Arc::new(paper_wires), + hatches: paper_hatches, + wipeouts: paper_wipeouts, + images: paper_images, + group_splits: splits, }; - return ( - std::sync::Arc::new(paper_wires), - paper_hatches, - paper_wipeouts, - splits, - ); } - let (mut model_pattern_wires, model_hatches, model_wipeouts) = + let (mut model_pattern_wires, model_hatches, model_wipeouts, model_images) = scene.viewport_plot_fills(); model_pattern_wires.retain(|(wire, _)| wire.plot_visible); let model_pattern_wires = model_pattern_wires @@ -251,36 +243,42 @@ fn plot_scene_content( .map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth }) .collect::>(); - let (wires, hatches, wipeouts, splits) = if paper_space_last { + let (wires, hatches, wipeouts, images, splits) = if paper_space_last { let splits = crate::io::pdf_export::PlotGroupSplits { wires: model_wires.len() + model_pattern_wires.len(), hatches: model_hatches.len(), wipeouts: model_wipeouts.len(), + images: model_images.len(), }; let mut wires = model_wires; let mut hatches = model_hatches; let mut wipeouts = model_wipeouts; + let mut images = model_images; wires.extend(model_pattern_wires); wires.extend(paper_wires); hatches.extend(paper_hatches); wipeouts.extend(paper_wipeouts); - (wires, hatches, wipeouts, splits) + images.extend(paper_images); + (wires, hatches, wipeouts, images, splits) } else { let splits = crate::io::pdf_export::PlotGroupSplits { wires: paper_wires.len(), hatches: paper_hatches.len(), wipeouts: paper_wipeouts.len(), + images: paper_images.len(), }; let mut wires = paper_wires; let mut hatches = paper_hatches; let mut wipeouts = paper_wipeouts; + let mut images = paper_images; wires.extend(model_wires); wires.extend(model_pattern_wires); hatches.extend(model_hatches); wipeouts.extend(model_wipeouts); - (wires, hatches, wipeouts, splits) + images.extend(model_images); + (wires, hatches, wipeouts, images, splits) }; - (std::sync::Arc::new(wires), hatches, wipeouts, splits) + PlotContent { wires: std::sync::Arc::new(wires), hatches, wipeouts, images, group_splits: splits } } impl OpenCADStudio { @@ -2986,32 +2984,16 @@ pub(super) fn on_open_file(&mut self) -> Task { self.load_plotsettings_into_dialog(&settings); } } - let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) = - self.direct_plot_params() + let Some(page) = + self.direct_plot_page() else { self.command_line .push_error(crate::t!("Nothing to plot: model space contains no printable geometry.").as_ref()); return Task::none(); }; - let plot_style = self.dialog_plot_style(&self.plot_dialog); - let render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits); let worker_path = path.clone(); let work = move || { - crate::io::pdf_export::export_pdf( - &wires, - &hatches, - &wipeouts, - page_w, - page_h, - ox, - oy, - rotation, - scale, - clip, - &worker_path, - plot_style.as_ref(), - render_options, - ) + crate::io::pdf_export::export_pdf(&page, &worker_path) .map(|_| crate::tf!("Exported: {}", worker_path.display()).into_owned()) .map_err(|e| crate::tf!("Export failed: {e}").into_owned()) }; @@ -3029,33 +3011,17 @@ pub(super) fn on_open_file(&mut self) -> Task { "Extents" => self.extents_plot_job(), _ => self.window_plot_job(), }; - let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) = + let Some(page) = job else { self.command_line .push_error(crate::t!("Plot area is empty. Pick a larger window.").as_ref()); return Task::none(); }; - let plot_style = self.dialog_plot_style(&self.plot_dialog); - let render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits); let worker_path = path.clone(); self.close_active_modal(); let work = move || { - crate::io::pdf_export::export_pdf( - &wires, - &hatches, - &wipeouts, - page_w, - page_h, - ox, - oy, - rotation, - scale, - clip, - &worker_path, - plot_style.as_ref(), - render_options, - ) + crate::io::pdf_export::export_pdf(&page, &worker_path) .map(|_| {crate::tf!( "Plotted window to {}", worker_path @@ -3192,8 +3158,7 @@ pub(super) fn on_open_file(&mut self) -> Task { dialog.style_name ).into_owned()); } - let plot_style = self.dialog_plot_style(&dialog); - let params = match dialog.area.as_str() { + let area_page = match dialog.area.as_str() { "Display" => self.display_plot_job(), "Extents" => self.extents_plot_job(), "Limits" => self.limits_plot_job(), @@ -3203,62 +3168,12 @@ pub(super) fn on_open_file(&mut self) -> Task { } _ => None, }; - let ( - wires, - hatches, - wipeouts, - group_splits, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - ) = if dialog.area == "Layout" { - self.layout_plot_params_for("Layout") + let page = if dialog.area == "Layout" { + self.layout_plot_page_for("Layout") } else { - let ( - wires, - hatches, - wipeouts, - group_splits, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - ) = params.ok_or_else(|| crate::tf!("Layout '{name}' plot area is empty.").into_owned())?; - ( - std::sync::Arc::new(wires), - hatches, - wipeouts, - group_splits, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - ) + area_page.ok_or_else(|| crate::tf!("Layout '{name}' plot area is empty.").into_owned())? }; - pages.push(crate::io::pdf_export::PdfPageInput { - wires, - hatches, - wipeouts, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - options: Self::pdf_plot_options(&dialog, group_splits), - plot_style, - }); + pages.push(page); } Ok(pages) })(); @@ -3348,7 +3263,7 @@ pub(super) fn on_open_file(&mut self) -> Task { return Task::none(); } }; - let options = self.plot_print_options(&dialog, Default::default()); + let options = self.plot_print_options(&dialog); let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all"); self.save_config(); self.close_active_modal(); @@ -3402,42 +3317,31 @@ pub(super) fn on_open_file(&mut self) -> Task { } } - /// Build the render inputs and page geometry for a full-layout plot: wires - /// plus hatch / wipeout fills, the effective (rotation-swapped) sheet size, - /// draw-origin offset, rotation, unit scale, and optional clip. Shared by - /// PDF export, preview, and printer output so all three render identically. - pub(super) fn layout_plot_params(&self) -> LayoutPlotParams { - self.layout_plot_params_for(&self.plot_dialog.area) + /// Assemble a layout page for PDF export, preview, or printing. + pub(super) fn layout_plot_page(&self) -> PdfPageInput { + self.layout_plot_page_for(&self.plot_dialog.area) } - fn layout_plot_params_for(&self, plot_area: &str) -> LayoutPlotParams { + fn layout_plot_page_for(&self, plot_area: &str) -> PdfPageInput { let i = self.active_tab; let scene = &self.tabs[i].scene; let paper_space = scene.current_layout != "Model"; let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog); - let (source_wires, hatches, wipeouts, mut group_splits) = - plot_scene_content( + let mut content = plot_scene_content( scene, self.plot_dialog.paperspace_last, plot_render_mode_override(&self.plot_dialog), ); // The printable-area rectangle is an on-screen guide, not drawing // content. It used to leak into every paper-space PDF/preview/print. - let wires = if paper_space { - group_splits.wires = source_wires[..group_splits.wires.min(source_wires.len())] + if paper_space { + content.group_splits.wires = content.wires[..content.group_splits.wires.min(content.wires.len())] .iter() .filter(|wire| wire.name != "paper_printable_area") .count(); - std::sync::Arc::new( - source_wires - .iter() - .filter(|wire| wire.name != "paper_printable_area") - .cloned() - .collect(), - ) - } else { - source_wires - }; + std::sync::Arc::make_mut(&mut content.wires) + .retain(|wire| wire.name != "paper_printable_area"); + } if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() { // Paper geometry is stored in layout units; PDF pages use mm. // Scale both geometry and offsets by the same unit conversion. @@ -3470,7 +3374,7 @@ pub(super) fn on_open_file(&mut self) -> Task { let (scale, offset_x, offset_y) = if plot_extents { let (min_x, min_y, max_x, max_y) = - plot_content_extents(&wires, &hatches, &wipeouts) + plot_content_extents(&content) .unwrap_or((x0, y0, x1, y1)); let content_w = (max_x - min_x).max(1e-9); let content_h = (max_y - min_y).max(1e-9); @@ -3556,19 +3460,18 @@ pub(super) fn on_open_file(&mut self) -> Task { } else { None }; - return ( - wires, - hatches, - wipeouts, - group_splits, - page_w, - page_h, + return PdfPageInput { + content, + paper_w: page_w, + paper_h: page_h, offset_x, offset_y, - rotation, - scale as f32, + rotation_deg: rotation, + scale: scale as f32, clip, - ); + options: Self::pdf_plot_options(&self.plot_dialog), + plot_style: self.dialog_plot_style(&self.plot_dialog), + }; } // Model space keeps its established extents + 5% margin behaviour. @@ -3588,65 +3491,50 @@ pub(super) fn on_open_file(&mut self) -> Task { } else { (297.0, 210.0, 0.0, 0.0) }; - ( - wires, - hatches, - wipeouts, - group_splits, - page_w, - page_h, + PdfPageInput { + content, + paper_w: page_w, + paper_h: page_h, offset_x, offset_y, - 0, - 1.0, - None, - ) + rotation_deg: 0, + scale: 1.0, + clip: None, + options: Self::pdf_plot_options(&self.plot_dialog), + plot_style: self.dialog_plot_style(&self.plot_dialog), + } } /// Build direct PDF/printer output using the physical layout sheet in /// paper space and the selected ISO sheet/scale around Model extents. - fn direct_plot_params(&self) -> Option { + fn direct_plot_page(&self) -> Option { if self.tabs[self.active_tab].scene.current_layout != "Model" { - return Some(self.layout_plot_params_for("Layout")); + return Some(self.layout_plot_page_for("Layout")); } - let (wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip) = - self.extents_plot_job()?; - if wires.is_empty() && hatches.is_empty() && wipeouts.is_empty() { + let page = self.extents_plot_job()?; + let content = &page.content; + if content.wires.is_empty() && content.hatches.is_empty() + && content.wipeouts.is_empty() && content.images.is_empty() + { return None; } - Some(( - std::sync::Arc::new(wires), - hatches, - wipeouts, - group_splits, - page_w, - page_h, - ox, - oy, - rotation, - scale, - clip, - )) + Some(page) } pub(super) fn on_print_to_printer(&mut self) -> Task { - let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) = - self.direct_plot_params() + let Some(page) = + self.direct_plot_page() else { self.command_line .push_error(crate::t!("Nothing to plot: model space contains no printable geometry.").as_ref()); return Task::none(); }; - let plot_style = self.dialog_plot_style(&self.plot_dialog); - let options = self.plot_print_options(&self.plot_dialog, group_splits); + let options = self.plot_print_options(&self.plot_dialog); self.command_line.push_info(crate::t!("Sending to system printer…").as_ref()); background_task( move || { iced::futures::executor::block_on( - crate::io::print_to_printer::print_wires_with( - wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip, - plot_style, options, - )) + crate::io::print_to_printer::print_wires_with(page, options)) }, Message::PrintResult, ) @@ -4537,7 +4425,6 @@ pub(super) fn on_open_file(&mut self) -> Task { self.active_modal = None; self.reset_modal_geometry(); - let plot_style = self.dialog_plot_style(&d); // Extents, Window and Display use one plot path in both spaces. Only // Paper-space Layout is special: it uses the physical sheet bounds. if d.area != "Layout" { @@ -4551,19 +4438,7 @@ pub(super) fn on_open_file(&mut self) -> Task { } _ => None, }; - let Some(( - w_wires, - w_hatches, - w_wipeouts, - wgroup_splits, - sw, - sh, - wox, - woy, - wrotation, - wscale, - wclip, - )) = job + let Some(page) = job else { self.command_line .push_error(crate::t!("Plot area is empty. Pick a larger window.").as_ref()); @@ -4574,13 +4449,8 @@ pub(super) fn on_open_file(&mut self) -> Task { // whatever the output target is. if preview { let tmp = crate::io::print_to_printer::temp_pdf_path("preview"); - let render_options = Self::pdf_plot_options(&d, wgroup_splits); let work = move || { - crate::io::pdf_export::export_pdf( - &w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, wrotation, wscale, wclip, &tmp, - plot_style.as_ref(), - render_options, - ).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) + crate::io::pdf_export::export_pdf(&page, &tmp).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) .map(|_| crate::t!("Opened plot preview.").into_owned()).map_err(|e| crate::tf!("Preview failed: {e}").into_owned()) }; return self.run_plot_work(d.background, true, work); @@ -4590,42 +4460,21 @@ pub(super) fn on_open_file(&mut self) -> Task { return Task::done(Message::PlotWindowExport); } let tmp = crate::io::print_to_printer::temp_pdf_path("print"); - let render_options = Self::pdf_plot_options(&d, wgroup_splits); - let opts = self.plot_print_options(&d, wgroup_splits); + let opts = self.plot_print_options(&d); let work = move || { - crate::io::pdf_export::export_pdf( - &w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, wrotation, wscale, wclip, &tmp, - plot_style.as_ref(), - render_options, - ).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) + crate::io::pdf_export::export_pdf(&page, &tmp).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) .map(|printer| crate::tf!("Sent to printer: {printer}").into_owned()) .map_err(|e| crate::tf!("Print failed: {e}").into_owned()) }; return self.run_plot_work(true, false, work); } - let (wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip) = - self.layout_plot_params(); + let page = self.layout_plot_page(); if preview { let tmp = crate::io::print_to_printer::temp_pdf_path("preview"); - let render_options = Self::pdf_plot_options(&d, group_splits); let work = move || { - crate::io::pdf_export::export_pdf( - &wires, - &hatches, - &wipeouts, - page_w, - page_h, - ox, - oy, - rotation, - scale, - clip, - &tmp, - plot_style.as_ref(), - render_options, - ).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) + crate::io::pdf_export::export_pdf(&page, &tmp).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) .map(|_| crate::t!("Opened plot preview.").into_owned()).map_err(|e| crate::tf!("Preview failed: {e}").into_owned()) }; return self.run_plot_work(d.background, true, work); @@ -4636,14 +4485,11 @@ pub(super) fn on_open_file(&mut self) -> Task { return Task::done(Message::PlotExport); } - let opts = self.plot_print_options(&d, group_splits); + let opts = self.plot_print_options(&d); self.command_line.push_info(crate::t!("Sending to system printer…").as_ref()); let work = move || { iced::futures::executor::block_on( - crate::io::print_to_printer::print_wires_with( - wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip, - plot_style, opts, - )) + crate::io::print_to_printer::print_wires_with(page, opts)) .map(|printer| crate::tf!("Sent to printer: {printer}").into_owned()) .map_err(|error| crate::tf!("Print failed: {error}").into_owned()) }; @@ -4655,19 +4501,16 @@ pub(super) fn on_open_file(&mut self) -> Task { fn plot_print_options( &self, d: &crate::ui::window::plot::PlotDialogState, - group_splits: crate::io::pdf_export::PlotGroupSplits, ) -> crate::io::print_to_printer::PrintOptions { crate::io::print_to_printer::PrintOptions { printer: d.printer.clone(), copies: d.copies.trim().parse::().unwrap_or(1).max(1), quality: Some(d.quality.clone()), - render: Self::pdf_plot_options(d, group_splits), } } fn pdf_plot_options( d: &crate::ui::window::plot::PlotDialogState, - group_splits: crate::io::pdf_export::PlotGroupSplits, ) -> crate::io::pdf_export::PdfPlotOptions { crate::io::pdf_export::PdfPlotOptions { object_lineweights: d.lineweights, @@ -4675,7 +4518,6 @@ pub(super) fn on_open_file(&mut self) -> Task { transparency: d.transparency, stamp: d.stamp, merge_lines: d.merge_lines, - group_splits, } } @@ -4692,22 +4534,22 @@ pub(super) fn on_open_file(&mut self) -> Task { .cloned() } - fn window_plot_job(&self) -> Option { + fn window_plot_job(&self) -> Option { self.area_plot_job(self.plot_window?) } - fn display_plot_job(&self) -> Option { + fn display_plot_job(&self) -> Option { self.area_plot_job(self.display_plot_window()?) } - fn limits_plot_job(&self) -> Option { + fn limits_plot_job(&self) -> Option { let (min, max) = self.tabs[self.active_tab] .scene .current_drawing_limits()?; self.area_plot_job((min.x, min.y, max.x, max.y)) } - fn named_view_plot_job(&self, name: &str) -> Option { + fn named_view_plot_job(&self, name: &str) -> Option { let view = self.tabs[self.active_tab] .scene .document @@ -4728,7 +4570,7 @@ pub(super) fn on_open_file(&mut self) -> Task { )) } - fn extents_plot_job(&self) -> Option { + fn extents_plot_job(&self) -> Option { let scene = &self.tabs[self.active_tab].scene; if scene.current_layout == "Model" { let (min, max) = scene.model_space_extents()?; @@ -4739,21 +4581,12 @@ pub(super) fn on_open_file(&mut self) -> Task { max.y as f64, )); } - let (wires, hatches, wipeouts, _) = - plot_scene_content( - scene, - self.plot_dialog.paperspace_last, - plot_render_mode_override(&self.plot_dialog), - ); - let extents = plot_content_extents( - &wires - .iter() - .filter(|wire| wire.name != "paper_printable_area") - .cloned() - .collect::>(), - &hatches, - &wipeouts, - )?; + let content = plot_scene_content( + scene, + self.plot_dialog.paperspace_last, + plot_render_mode_override(&self.plot_dialog), + ); + let extents = plot_content_extents(&content)?; self.area_plot_job(extents) } @@ -4815,7 +4648,7 @@ pub(super) fn on_open_file(&mut self) -> Task { /// Render a selected rectangle through one shared Model/Paper path. The /// window may lie partly or wholly outside a paper sheet. - fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option { + fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option { use crate::io::paper_sizes::{window_to_sheet, PlotScale}; let i = self.active_tab; let (x0, y0, x1, y1) = window; @@ -4854,24 +4687,19 @@ pub(super) fn on_open_file(&mut self) -> Task { }; let scene = &self.tabs[i].scene; let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); - let (all_wires, hatches, wipeouts, mut group_splits) = - plot_scene_content( + let mut content = plot_scene_content( scene, self.plot_dialog.paperspace_last, plot_render_mode_override(&self.plot_dialog), ); - let first_wire_count = all_wires[..group_splits.wires.min(all_wires.len())] - .iter() - .filter(|w| w.name != "paper_printable_area") - .filter(|w| w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0) - .count(); - let wires: Vec<_> = all_wires - .iter() - .filter(|w| w.name != "paper_printable_area") - .filter(|w| w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0) - .cloned() - .collect(); - group_splits.wires = first_wire_count; + let in_window = |wire: &crate::io::pdf_export::PlotWire| { + wire.name != "paper_printable_area" + && wire.aabb[0] <= wx1 && wire.aabb[2] >= wx0 + && wire.aabb[1] <= wy1 && wire.aabb[3] >= wy0 + }; + content.group_splits.wires = content.wires[..content.group_splits.wires.min(content.wires.len())] + .iter().filter(|wire| in_window(wire)).count(); + std::sync::Arc::make_mut(&mut content.wires).retain(in_window); let offset_x = (target_x / scale) - x0; let offset_y = (target_y / scale) - y0; let clip = Some(( @@ -4885,19 +4713,18 @@ pub(super) fn on_open_file(&mut self) -> Task { 90 | 270 => (sheet_h, sheet_w), _ => (sheet_w, sheet_h), }; - Some(( - wires, - hatches, - wipeouts, - group_splits, - page_w, - page_h, + Some(PdfPageInput { + content, + paper_w: page_w, + paper_h: page_h, offset_x, offset_y, - rotation, - scale as f32, + rotation_deg: rotation, + scale: scale as f32, clip, - )) + options: Self::pdf_plot_options(&self.plot_dialog), + plot_style: self.dialog_plot_style(&self.plot_dialog), + }) } pub(super) fn on_plot_style_panel_apply(&mut self) -> Task { diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 5dc8161e1..840555a8e 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -13,6 +13,7 @@ use crate::scene::model::hatch_model::HatchModel; #[cfg(not(target_arch = "wasm32"))] use crate::scene::model::hatch_model::HatchPattern; use crate::scene::WireModel; +use crate::scene::model::image_model::ImageModel; #[cfg(not(target_arch = "wasm32"))] use printpdf::{ BlendMode, BuiltinFont, Color, ExtendedGraphicsState, ExtendedGraphicsStateId, Line, @@ -37,25 +38,18 @@ impl std::ops::Deref for PlotWire { } } +/// Decoded image geometry plus inherited block/viewport clip boundaries. +#[derive(Clone, Debug)] +pub struct PlotImage { + pub image: ImageModel, + pub clips: Vec>, +} + // The web build has no `printpdf` (it pulls a wasm-incompatible `memchr` via // lopdf → nom_locate) and no filesystem, so PDF export is native-only; the web // build gets these stubs so the call sites still compile. #[cfg(target_arch = "wasm32")] -pub fn export_pdf( - _wires: &[PlotWire], - _hatches: &[HatchModel], - _wipeouts: &[HatchModel], - _paper_w: f64, - _paper_h: f64, - _offset_x: f64, - _offset_y: f64, - _rotation_deg: i32, - _scale: f32, - _clip: Option<(f32, f32, f32, f32)>, - _path: &Path, - _plot_style: Option<&PlotStyleTable>, - _options: PdfPlotOptions, -) -> Result<(), String> { +pub fn export_pdf(_page: &PdfPageInput, _path: &Path) -> Result<(), String> { Err("PDF export is not available in the web version.".into()) } @@ -94,7 +88,6 @@ pub struct PdfPlotOptions { pub transparency: bool, pub stamp: bool, pub merge_lines: bool, - pub group_splits: PlotGroupSplits, } /// End indexes of the first paper/model render group in each flat input list. @@ -103,16 +96,26 @@ pub struct PlotGroupSplits { pub wires: usize, pub hatches: usize, pub wipeouts: usize, + pub images: usize, } -/// Owned render data for one page in a multi-page PDF. -#[cfg_attr(target_arch = "wasm32", allow(dead_code))] -pub struct PdfPageInput { +#[derive(Default)] +pub struct PlotContent { pub wires: std::sync::Arc>, pub hatches: Vec, pub wipeouts: Vec, + pub images: Vec, + pub group_splits: PlotGroupSplits, +} + +/// Owned geometry and settings for one PDF page. +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] +pub struct PdfPageInput { + pub content: PlotContent, + /// Page dimensions in mm, after any 90/270-degree rotation. pub paper_w: f64, pub paper_h: f64, + /// Absolute-world offsets stay f64 to preserve local detail at UTM coordinates. pub offset_x: f64, pub offset_y: f64, pub rotation_deg: i32, @@ -130,50 +133,16 @@ impl Default for PdfPlotOptions { transparency: false, stamp: false, merge_lines: false, - group_splits: PlotGroupSplits::default(), } } } // ── Public entry point ──────────────────────────────────────────────────── -/// Export `wires` to a PDF file. -/// -/// - `paper_w` / `paper_h`: page dimensions in mm (already swapped for 90°/270° by caller). -/// - `offset_x` / `offset_y`: added to every wire coordinate so the drawing -/// origin maps to the bottom-left corner of the page. -/// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page. +/// Export one page to a PDF file. #[cfg(not(target_arch = "wasm32"))] -pub fn export_pdf( - wires: &[PlotWire], - hatches: &[HatchModel], - wipeouts: &[HatchModel], - paper_w: f64, - paper_h: f64, - offset_x: f64, - offset_y: f64, - rotation_deg: i32, - scale: f32, - clip: Option<(f32, f32, f32, f32)>, - path: &Path, - plot_style: Option<&PlotStyleTable>, - options: PdfPlotOptions, -) -> Result<(), String> { - let bytes = build_pdf( - wires, - hatches, - wipeouts, - paper_w as f32, - paper_h as f32, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - plot_style, - options, - ); - write_pdf_atomically(path, &bytes) +pub fn export_pdf(page: &PdfPageInput, path: &Path) -> Result<(), String> { + export_pdf_pages(std::slice::from_ref(page), path, None) } /// Export several independently sized pages into one PDF file. @@ -186,7 +155,7 @@ pub fn export_pdf_pages( if pages.is_empty() { return Err("No pages were selected.".into()); } - let bytes = build_pdf_pages(pages, plot_style); + let bytes = build_pdf_pages(pages, plot_style)?; write_pdf_atomically(path, &bytes) } @@ -205,6 +174,202 @@ fn write_pdf_atomically(path: &Path, bytes: &[u8]) -> Result<(), String> { Ok(()) } +#[cfg(not(target_arch = "wasm32"))] +fn image_clip(ops: &mut Vec, rings: Vec>) { + ops.push(Op::DrawPolygon { + polygon: Polygon { + rings: rings + .into_iter() + .map(|ring| PolygonRing { + points: ring + .into_iter() + .map(|[x, y]| LinePoint { + p: Point { x: Pt(x), y: Pt(y) }, + bezier: false, + }) + .collect(), + }) + .collect(), + mode: PaintMode::Clip, + winding_order: WindingOrder::EvenOdd, + }, + }); +} + +#[cfg(not(target_arch = "wasm32"))] +fn emit_image( + doc: &mut PdfDocument, + resources: &mut std::collections::HashMap<(usize, u32, u32), printpdf::XObjectId>, + ops: &mut Vec, + plot: &PlotImage, + ox: f64, + oy: f64, + options: PdfPlotOptions, +) -> Result<(), String> { + let image = &plot.image; + let expected_byte_count = (image.width as usize) + .checked_mul(image.height as usize) + .and_then(|n| n.checked_mul(4)); + if image.width == 0 || image.height == 0 || expected_byte_count != Some(image.pixels.len()) { + return Err("Cannot plot bitmap: dimensions do not match its RGBA pixels.".into()); + } + if image.verts.len() < 3 || image.verts.len() % 3 != 0 { + return Err("Cannot plot bitmap: incomplete triangle geometry.".into()); + } + if options.transparency && image.opacity <= 0.0 { + return Ok(()); + } + let to_page = |high: [f32; 3], low: [f32; 3]| { + [ + ((high[0] as f64 + low[0] as f64 + ox) * MM_TO_PT as f64) as f32, + ((high[1] as f64 + low[1] as f64 + oy) * MM_TO_PT as f64) as f32, + ] + }; + let corners: [[f32; 2]; 4] = + std::array::from_fn(|i| to_page(image.corners[i], image.corners_low[i])); + if !corners.iter().flatten().all(|v| v.is_finite()) + || !image.verts.iter().all(|v| { + to_page(v.pos, v.pos_low) + .iter() + .chain(&v.uv) + .all(|c| c.is_finite()) + }) + || !plot + .clips + .iter() + .all(|ring| ring.len() >= 3 && ring.iter().flatten().all(|v| v.is_finite())) + || !image.opacity.is_finite() + { + return Err("Cannot plot bitmap: invalid coordinates, clip boundary, or opacity.".into()); + } + let key = ( + std::sync::Arc::as_ptr(&image.pixels) as usize, + image.width, + image.height, + ); + let id = resources + .entry(key) + .or_insert_with(|| { + let id = printpdf::XObjectId::new(); + // add_image clones its argument; the public map accepts owned pixels. + doc.resources.xobjects.map.insert( + id.clone(), + printpdf::XObject::Image(printpdf::RawImage { + pixels: printpdf::RawImageData::U8(image.pixels.as_ref().clone()), + width: image.width as usize, + height: image.height as usize, + data_format: printpdf::RawImageFormat::RGBA8, + tag: Vec::new(), + }), + ); + id + }) + .clone(); + ops.push(Op::SaveGraphicsState); + if options.transparency && image.opacity < 1.0 { + // printpdf 0.9.1 swaps the serialized CA/ca keys. Set both inside + // this image's saved state so the nonstroking alpha is correct. + let alpha = image.opacity.clamp(0.0, 1.0); + let gs = doc.add_graphics_state( + ExtendedGraphicsState::default() + .with_current_fill_alpha(alpha) + .with_current_stroke_alpha(alpha), + ); + ops.push(Op::LoadGraphicsState { gs }); + } + for clip in &plot.clips { + image_clip( + ops, + vec![clip + .iter() + .map(|p| { + [ + ((p[0] + ox) * MM_TO_PT as f64) as f32, + ((p[1] + oy) * MM_TO_PT as f64) as f32, + ] + }) + .collect()], + ); + } + let draw = |ops: &mut Vec, matrix: [f32; 6]| { + ops.push(Op::SetTransformationMatrix { + matrix: printpdf::CurTransMat::Raw(matrix), + }); + ops.push(Op::UseXobject { + id: id.clone(), + // Cancel printpdf's pixel-size transform: matrix maps the PDF + // image's unit square directly onto the CAD quad in page points. + transform: printpdf::XObjectTransform { + dpi: Some(72.0), + scale_x: Some(1.0 / image.width as f32), + scale_y: Some(1.0 / image.height as f32), + ..Default::default() + }, + }); + }; + let u = [corners[1][0] - corners[0][0], corners[1][1] - corners[0][1]]; + let v = [corners[3][0] - corners[0][0], corners[3][1] - corners[0][1]]; + let affine = (0..2).all(|axis| { + (corners[2][axis] - corners[0][axis] - u[axis] - v[axis]).abs() + <= 1e-5 * (u[axis].abs() + v[axis].abs()).max(1.0) + }); + if affine { + // A single draw preserves intrinsic IMAGE clipping without overlapping image draws. + image_clip( + ops, + image + .verts + .chunks_exact(3) + .map(|tri| { + tri.iter() + .map(|vertex| to_page(vertex.pos, vertex.pos_low)) + .collect() + }) + .collect(), + ); + draw(ops, [u[0], u[1], v[0], v[1], corners[0][0], corners[0][1]]); + } else { + // PDF transforms are affine; perspective sampling is approximate. + for triangle in image.verts.chunks_exact(3) { + let page = std::array::from_fn(|i| to_page(triangle[i].pos, triangle[i].pos_low)); + let uv = std::array::from_fn(|i| [triangle[i].uv[0], 1.0 - triangle[i].uv[1]]); + let matrix = image_triangle_matrix(page, uv) + .filter(|matrix| matrix.iter().all(|v| v.is_finite())) + .ok_or("Cannot plot bitmap: degenerate texture mapping.")?; + ops.push(Op::SaveGraphicsState); + image_clip(ops, vec![page.to_vec()]); + draw(ops, matrix); + ops.push(Op::RestoreGraphicsState); + } + } + ops.push(Op::RestoreGraphicsState); + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn image_triangle_matrix(p: [[f32; 2]; 3], uv: [[f32; 2]; 3]) -> Option<[f32; 6]> { + let u = [uv[1][0] - uv[0][0], uv[1][1] - uv[0][1]]; + let v = [uv[2][0] - uv[0][0], uv[2][1] - uv[0][1]]; + let determinant = u[0] * v[1] - v[0] * u[1]; + if determinant.abs() < 1e-12 { + return None; + } + let axes: [[f32; 2]; 2] = std::array::from_fn(|i| { + [ + ((p[1][i] - p[0][i]) * v[1] - (p[2][i] - p[0][i]) * u[1]) / determinant, + (u[0] * (p[2][i] - p[0][i]) - v[0] * (p[1][i] - p[0][i])) / determinant, + ] + }); + Some([ + axes[0][0], + axes[1][0], + axes[0][1], + axes[1][1], + p[0][0] - axes[0][0] * uv[0][0] - axes[0][1] * uv[0][1], + p[0][1] - axes[1][0] * uv[0][0] - axes[1][1] * uv[0][1], + ]) +} + /// Show a parented PDF save-file dialog and return the chosen path. /// /// The parent comes from `iced::window::run`, keeping the portal request tied @@ -230,83 +395,32 @@ pub fn pick_pdf_path_owned( // ── PDF builder ─────────────────────────────────────────────────────────── #[cfg(not(target_arch = "wasm32"))] -fn build_pdf( - wires: &[PlotWire], - hatches: &[HatchModel], - wipeouts: &[HatchModel], - paper_w: f32, - paper_h: f32, - // Absolute-world offsets, kept in f64: at UTM the drawing sits at ~5e5/4.5e6 - // where an f32 has ~0.03 m / ~0.5 m of resolution, so an f32 offset is itself - // already quantised before it can cancel the coordinate it is meant to cancel. - ox: f64, - oy: f64, - rotation_deg: i32, - scale: f32, - clip: Option<(f32, f32, f32, f32)>, - plot_style: Option<&PlotStyleTable>, - options: PdfPlotOptions, -) -> Vec { +fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) -> Result, String> { let mut doc = PdfDocument::new("Open CAD Studio Export"); - append_pdf_page( - &mut doc, - wires, - hatches, - wipeouts, - paper_w, - paper_h, - ox, - oy, - rotation_deg, - scale, - clip, - plot_style, - options, - ); - let mut warnings = Vec::new(); - doc.save(&PdfSaveOptions::default(), &mut warnings) -} - -#[cfg(not(target_arch = "wasm32"))] -fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) -> Vec { - let mut doc = PdfDocument::new("Open CAD Studio Export"); - for page in pages { - append_pdf_page( - &mut doc, - &page.wires, - &page.hatches, - &page.wipeouts, - page.paper_w as f32, - page.paper_h as f32, - page.offset_x, - page.offset_y, - page.rotation_deg, - page.scale, - page.clip, - page.plot_style.as_ref().or(plot_style), - page.options, - ); + // Borrowing all pages keeps their pixel Arcs alive until this cache is dropped. + // Allocation addresses cannot be reused by another source during this export. + let mut image_resources = std::collections::HashMap::new(); + for (index, page) in pages.iter().enumerate() { + append_pdf_page(&mut doc, &mut image_resources, page, plot_style) + .map_err(|error| format!("Page {}: {error}", index + 1))?; } let mut warnings = Vec::new(); - doc.save(&PdfSaveOptions::default(), &mut warnings) + Ok(doc.save(&PdfSaveOptions::default(), &mut warnings)) } #[cfg(not(target_arch = "wasm32"))] fn append_pdf_page( doc: &mut PdfDocument, - wires: &[PlotWire], - hatches: &[HatchModel], - wipeouts: &[HatchModel], - paper_w: f32, - paper_h: f32, - ox: f64, - oy: f64, - rotation_deg: i32, - scale: f32, - clip: Option<(f32, f32, f32, f32)>, - plot_style: Option<&PlotStyleTable>, - options: PdfPlotOptions, -) { + image_resources: &mut std::collections::HashMap<(usize, u32, u32), printpdf::XObjectId>, + page: &PdfPageInput, + fallback_plot_style: Option<&PlotStyleTable>, +) -> Result<(), String> { + let PlotContent { wires, hatches, wipeouts, images, group_splits } = &page.content; + let (paper_w, paper_h) = (page.paper_w as f32, page.paper_h as f32); + let (ox, oy) = (page.offset_x, page.offset_y); + let (rotation_deg, scale, clip) = (page.rotation_deg, page.scale, page.clip); + let plot_style = page.plot_style.as_ref().or(fallback_plot_style); + let options = page.options; let mut ops: Vec = Vec::new(); // White page background. @@ -411,23 +525,26 @@ fn append_pdf_page( let (first_wires, second_wires) = - wires.split_at(options.group_splits.wires.min(wires.len())); + wires.split_at(group_splits.wires.min(wires.len())); let (first_hatches, second_hatches) = - hatches.split_at(options.group_splits.hatches.min(hatches.len())); + hatches.split_at(group_splits.hatches.min(hatches.len())); let (first_wipeouts, second_wipeouts) = - wipeouts.split_at(options.group_splits.wipeouts.min(wipeouts.len())); - for (wires, hatches, wipeouts) in [ - (first_wires, first_hatches, first_wipeouts), - (second_wires, second_hatches, second_wipeouts), + wipeouts.split_at(group_splits.wipeouts.min(wipeouts.len())); + let (first_images, second_images) = + images.split_at(group_splits.images.min(images.len())); + for (wires, hatches, wipeouts, images) in [ + (first_wires, first_hatches, first_wipeouts, first_images), + (second_wires, second_hatches, second_wipeouts, second_images), ] { enum DrawItem<'a> { WireFill(&'a PlotWire), Hatch(&'a HatchModel), + Image(&'a PlotImage), Wire(&'a PlotWire), Text(&'a PlotWire), } - let mut draw_items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len()); + let mut draw_items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len()); let mut sequence = 0usize; for wire in wires { if !wire.fill_tris.is_empty() { @@ -445,6 +562,10 @@ fn append_pdf_page( draw_items.push((hatch.draw_depth, 1u8, sequence, DrawItem::Hatch(hatch))); sequence += 1; } + for image in images { + draw_items.push((image.image.draw_depth, 1u8, sequence, DrawItem::Image(image))); + sequence += 1; + } draw_items.sort_by(|a, b| { a.0.total_cmp(&b.0) .then_with(|| a.1.cmp(&b.1)) @@ -508,6 +629,10 @@ fn append_pdf_page( last_dash = None; continue; } + DrawItem::Image(image) => { + emit_image(doc, image_resources, &mut ops, image, ox, oy, options)?; + continue; + } DrawItem::Wire(wire) => wire, }; let [mut r, mut g, mut b, a] = wire.color; @@ -722,6 +847,7 @@ fn append_pdf_page( let page = PdfPage::new(Mm(paper_w), Mm(paper_h), ops); doc.pages.push(page); + Ok(()) } /// Build a PDF dash array (in points) from a WireModel linetype pattern. @@ -1517,6 +1643,21 @@ fn emit_text( mod tests { use super::*; + fn test_page(wires: Vec) -> PdfPageInput { + PdfPageInput { + content: PlotContent { wires: std::sync::Arc::new(wires), ..Default::default() }, + paper_w: 210.0, + paper_h: 297.0, + offset_x: 0.0, + offset_y: 0.0, + rotation_deg: 0, + scale: 1.0, + clip: None, + options: PdfPlotOptions::default(), + plot_style: None, + } + } + #[test] fn atomic_write_replaces_an_existing_pdf() { let path = std::env::temp_dir().join(format!( @@ -1546,20 +1687,10 @@ mod tests { ), draw_depth: 0.0, }; - let bytes = build_pdf( - &[w], - &[], - &[], - 210.0, - 297.0, - 0.0, - 0.0, - 0, - 2.0, - Some((10.0, 10.0, 100.0, 100.0)), - None, - PdfPlotOptions::default(), - ); + let mut page = test_page(vec![w]); + page.scale = 2.0; + page.clip = Some((10.0, 10.0, 100.0, 100.0)); + let bytes = build_pdf_pages(&[page], None).unwrap(); // A valid PDF is produced (starts with the PDF header) and is non-trivial. assert!(bytes.starts_with(b"%PDF"), "not a PDF"); assert!(bytes.len() > 200, "suspiciously small: {}", bytes.len()); @@ -1594,34 +1725,8 @@ mod tests { let mut blank = wire.clone(); blank.wire.text_verts.clear(); - let with_text = build_pdf( - &[wire], - &[], - &[], - 210.0, - 297.0, - 0.0, - 0.0, - 0, - 1.0, - None, - None, - PdfPlotOptions::default(), - ); - let no_text = build_pdf( - &[blank], - &[], - &[], - 210.0, - 297.0, - 0.0, - 0.0, - 0, - 1.0, - None, - None, - PdfPlotOptions::default(), - ); + let with_text = build_pdf_pages(&[test_page(vec![wire])], None).unwrap(); + let no_text = build_pdf_pages(&[test_page(vec![blank])], None).unwrap(); assert!(with_text.starts_with(b"%PDF")); assert!( with_text.len() > no_text.len(), diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 6f4d3a680..425b06e9f 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -9,9 +9,6 @@ #[cfg(not(target_arch = "wasm32"))] use crate::io::pdf_export; -use crate::io::plot_style::PlotStyleTable; -use crate::scene::model::hatch_model::HatchModel; -use crate::io::pdf_export::PlotWire; #[cfg(not(target_arch = "wasm32"))] pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf { @@ -50,8 +47,6 @@ pub struct PrintOptions { /// setting wins, so the field is legitimately unread there. #[cfg_attr(target_os = "windows", allow(dead_code))] pub quality: Option, - /// Controls applied while building the intermediate PDF. - pub render: crate::io::pdf_export::PdfPlotOptions, } #[cfg(target_arch = "wasm32")] @@ -60,19 +55,8 @@ pub fn list_printers() -> Vec { } #[cfg(target_arch = "wasm32")] -#[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - _wires: std::sync::Arc>, - _hatches: Vec, - _wipeouts: Vec, - _paper_w: f64, - _paper_h: f64, - _offset_x: f64, - _offset_y: f64, - _rotation_deg: i32, - _scale: f32, - _clip: Option<(f32, f32, f32, f32)>, - _plot_style: Option, + _page: crate::io::pdf_export::PdfPageInput, _opts: PrintOptions, ) -> Result { Err("Printing is not available in the web version.".into()) @@ -172,46 +156,18 @@ pub fn open_printer_properties(_printer: Option<&str>) -> Result<(), String> { Err("Printer properties are not available in the web version.".into()) } -/// Like [`print_wires`] but honours a [`PrintOptions`] bundle (printer, copies, -/// grayscale, quality, DPI). +/// Render a page and send it to the selected printer. #[cfg(not(target_arch = "wasm32"))] -#[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - wires: std::sync::Arc>, - hatches: Vec, - wipeouts: Vec, - paper_w: f64, - paper_h: f64, - offset_x: f64, - offset_y: f64, - rotation_deg: i32, - scale: f32, - clip: Option<(f32, f32, f32, f32)>, - plot_style: Option, + page: crate::io::pdf_export::PdfPageInput, opts: PrintOptions, ) -> Result { let tmp_path = temp_pdf_path("print"); - pdf_export::export_pdf( - &wires, - &hatches, - &wipeouts, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - &tmp_path, - plot_style.as_ref(), - opts.render, - )?; + pdf_export::export_pdf(&page, &tmp_path)?; dispatch_to_printer_opts(&tmp_path, &opts) } -/// Send an already-rendered PDF to a printer with [`PrintOptions`]. Used for -/// clipped window plots, whose PDF is built with a scale + clip the plain -/// `print_wires_with` path doesn't expose. +/// Send an already-rendered PDF to the selected printer. #[cfg(not(target_arch = "wasm32"))] pub fn print_existing_pdf(path: &std::path::Path, opts: &PrintOptions) -> Result { dispatch_to_printer_opts(path, opts) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index d36fef59f..e5844afb0 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -6648,6 +6648,49 @@ impl Scene { all_visible: bool, viewport: Option, ) -> Vec { + self.collect_images(target_block, frozen, annotation_scale_handle, all_visible, + viewport, false, |image, _| image) + } + + pub fn paper_plot_images(&self) -> Vec { + let scale = if self.current_layout == "Model" { + self.displayed_annotation_scale_handle() + } else { + self.paper_annotation_scale_handle() + }; + self.placed_images(self.current_layout_block_handle(), None, + scale, self.annotation_all_visible(), None, true) + } + + fn placed_images( + &self, + target_block: Handle, + frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, + viewport: Option, + plotting: bool, + ) -> Vec { + self.collect_images(target_block, frozen, annotation_scale_handle, all_visible, + viewport, plotting, |image, context| crate::io::pdf_export::PlotImage { + image, + clips: context.clips.clone(), + }) + } + + fn collect_images( + &self, + target_block: Handle, + frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, + viewport: Option, + plotting: bool, + mut collect: impl FnMut(ImageModel, &render_graph::InstanceContext) -> T, + ) -> Vec { + if self.images.is_empty() { + return Vec::new(); + } let depth_map = self.draw_depth_map(); let graph = render_graph::RenderSceneGraph::new( &self.document, @@ -6673,8 +6716,10 @@ impl Scene { graph.walk_root( root, |entity, context| { - context.is_instanced() - || !self.entity_temporarily_hidden(entity.common().handle) + (!plotting || self.document.layers.get(&entity.common().layer) + .is_none_or(|layer| layer.is_plottable)) + && (context.is_instanced() + || !self.entity_temporarily_hidden(entity.common().handle)) }, |entity, context| { let handle = entity.common().handle; @@ -6734,7 +6779,7 @@ impl Scene { ); } placed.draw_depth = context.draw_depth(handle, depth_map.as_ref()); - models.push(placed); + models.push(collect(placed, context)); }, ); models diff --git a/src/scene/project.rs b/src/scene/project.rs index 29ef0735d..f20c10e38 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -386,12 +386,12 @@ impl Scene { /// needs the equivalent paper-space geometry explicitly. pub fn viewport_plot_fills( &self, - ) -> (Vec<(WireModel, f32)>, Vec, Vec) { + ) -> (Vec<(WireModel, f32)>, Vec, Vec, Vec) { use acadrust::entities::Viewport; use model::hatch_model::HatchPattern; if self.current_layout == "Model" { - return (Vec::new(), Vec::new(), Vec::new()); + return (Vec::new(), Vec::new(), Vec::new(), Vec::new()); } let paper_block = self.current_layout_block_handle(); let model_block = self.model_space_block_handle(); @@ -411,6 +411,7 @@ impl Scene { let mut pattern_wires = Vec::new(); let mut projected_hatches = Vec::new(); let mut projected_wipeouts = Vec::new(); + let mut projected_images = Vec::new(); for viewport in viewports { let Some(camera) = self.camera_for_viewport(viewport.common.handle) else { @@ -463,6 +464,58 @@ impl Scene { let frozen: rustc_hash::FxHashSet = viewport.frozen_layers.iter().copied().collect(); + let mut viewport_clips = vec![vec![ + [xmin as f64, ymin as f64], [xmax as f64, ymin as f64], + [xmax as f64, ymax as f64], [xmin as f64, ymax as f64], + ]]; + if !self.images.is_empty() && !viewport.clip_boundary_handle.is_null() { + let boundary = self.clip_boundary_polygon( + viewport.clip_boundary_handle, viewport.center.z as f32); + if boundary.len() >= 3 { + viewport_clips.push(boundary.iter().map(|p| [p[0] as f64, p[1] as f64]).collect()); + } + } + for mut plot in self.placed_images(model_block, Some(&frozen), + self.viewport_scale_handle(viewport.common.handle), + self.annotation_all_visible(), Some(viewport.common.handle), true) + { + let mut valid = true; + let mut project_point = |high: &mut [f32; 3], low: &mut [f32; 3]| { + let point = std::array::from_fn(|i| high[i] as f64 + low[i] as f64); + if let Some([x, y]) = project_3d(point) { + *high = [x, y, 0.0]; + *low = [0.0; 3]; + } else { + valid = false; + } + }; + for (high, low) in plot.image.corners.iter_mut().zip(&mut plot.image.corners_low) { + project_point(high, low); + } + for vertex in &mut plot.image.verts { + project_point(&mut vertex.pos, &mut vertex.pos_low); + } + for clip in &mut plot.clips { + for point in clip { + if let Some([x, y]) = project(point[0], point[1]) { + *point = [x as f64, y as f64]; + } else { + valid = false; + } + } + } + if !valid { + continue; + } + if [(0, xmin, xmax), (1, ymin, ymax)].iter().any(|&(axis, min, max)| { + plot.image.verts.iter().all(|v| v.pos[axis] < min) + || plot.image.verts.iter().all(|v| v.pos[axis] > max) + }) { + continue; + } + plot.clips.extend(viewport_clips.iter().cloned()); + projected_images.push(plot); + } let hatches = self.plot_hatches_for_block( model_block, Some(&frozen), @@ -536,7 +589,7 @@ impl Scene { } } - (pattern_wires, projected_hatches, projected_wipeouts) + (pattern_wires, projected_hatches, projected_wipeouts, projected_images) } /// A content viewport's clip boundary, projected into that viewport's diff --git a/tests/fixtures/embedded-image-print.dxf b/tests/fixtures/embedded-image-print.dxf new file mode 100644 index 000000000..6fa5972f6 --- /dev/null +++ b/tests/fixtures/embedded-image-print.dxf @@ -0,0 +1,7522 @@ + 0 +SECTION + 2 +HEADER + 9 +$ACADVER + 1 +AC1024 + 9 +$ACADMAINTVER + 70 +6 + 9 +$DWGCODEPAGE + 3 +ANSI_1252 + 9 +$LASTSAVEDBY + 1 +Synthetic reproduction + 9 +$INSBASE + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$EXTMIN + 10 +1e+20 + 20 +1e+20 + 30 +1e+20 + 9 +$EXTMAX + 10 +-1e+20 + 20 +-1e+20 + 30 +-1e+20 + 9 +$LIMMIN + 10 +0.0 + 20 +0.0 + 9 +$LIMMAX + 10 +420.0 + 20 +297.0 + 9 +$ORTHOMODE + 70 +0 + 9 +$REGENMODE + 70 +1 + 9 +$FILLMODE + 70 +1 + 9 +$QTEXTMODE + 70 +0 + 9 +$MIRRTEXT + 70 +1 + 9 +$LTSCALE + 40 +1.0 + 9 +$ATTMODE + 70 +1 + 9 +$TEXTSIZE + 40 +2.5 + 9 +$TRACEWID + 40 +1.0 + 9 +$TEXTSTYLE + 7 +OpenSans + 9 +$CLAYER + 8 +0 + 9 +$CELTYPE + 6 +ByLayer + 9 +$CECOLOR + 62 +256 + 9 +$CELTSCALE + 40 +1.0 + 9 +$DISPSILH + 70 +0 + 9 +$DIMSCALE + 40 +1.0 + 9 +$DIMASZ + 40 +0.175 + 9 +$DIMEXO + 40 +0.125 + 9 +$DIMDLI + 40 +3.75 + 9 +$DIMRND + 40 +0.0 + 9 +$DIMDLE + 40 +0.0 + 9 +$DIMEXE + 40 +0.375 + 9 +$DIMTP + 40 +0.0 + 9 +$DIMTM + 40 +0.0 + 9 +$DIMTXT + 40 +0.25 + 9 +$DIMCEN + 40 +2.5 + 9 +$DIMTSZ + 40 +0.0 + 9 +$DIMTOL + 70 +0 + 9 +$DIMLIM + 70 +0 + 9 +$DIMTIH + 70 +0 + 9 +$DIMTOH + 70 +0 + 9 +$DIMSE1 + 70 +0 + 9 +$DIMSE2 + 70 +0 + 9 +$DIMTAD + 70 +1 + 9 +$DIMZIN + 70 +12 + 9 +$DIMBLK + 1 +ARCHTICK + 9 +$DIMASO + 70 +1 + 9 +$DIMSHO + 70 +1 + 9 +$DIMPOST + 1 + + 9 +$DIMAPOST + 1 + + 9 +$DIMALT + 70 +0 + 9 +$DIMALTD + 70 +3 + 9 +$DIMALTF + 40 +0.03937007874 + 9 +$DIMLFAC + 40 +100.0 + 9 +$DIMTOFL + 70 +1 + 9 +$DIMTVP + 40 +0.0 + 9 +$DIMTIX + 70 +0 + 9 +$DIMSOXD + 70 +0 + 9 +$DIMSAH + 70 +0 + 9 +$DIMBLK1 + 1 + + 9 +$DIMBLK2 + 1 + + 9 +$DIMSTYLE + 2 +EZDXF + 9 +$DIMCLRD + 70 +0 + 9 +$DIMCLRE + 70 +0 + 9 +$DIMCLRT + 70 +0 + 9 +$DIMTFAC + 40 +0.5 + 9 +$DIMGAP + 40 +0.1 + 9 +$DIMJUST + 70 +0 + 9 +$DIMSD1 + 70 +0 + 9 +$DIMSD2 + 70 +0 + 9 +$DIMTOLJ + 70 +0 + 9 +$DIMTZIN + 70 +8 + 9 +$DIMALTZ + 70 +0 + 9 +$DIMALTTZ + 70 +0 + 9 +$DIMUPT + 70 +1 + 9 +$DIMDEC + 70 +2 + 9 +$DIMTDEC + 70 +2 + 9 +$DIMALTU + 70 +2 + 9 +$DIMALTTD + 70 +3 + 9 +$DIMTXSTY + 7 +OpenSansCondensed-Light + 9 +$DIMAUNIT + 70 +0 + 9 +$DIMADEC + 70 +2 + 9 +$DIMALTRND + 40 +0.0 + 9 +$DIMAZIN + 70 +3 + 9 +$DIMDSEP + 70 +46 + 9 +$DIMATFIT + 70 +3 + 9 +$DIMFRAC + 70 +0 + 9 +$DIMLDRBLK + 1 + + 9 +$DIMLUNIT + 70 +2 + 9 +$DIMLWD + 70 +-2 + 9 +$DIMLWE + 70 +-2 + 9 +$DIMTMOVE + 70 +2 + 9 +$DIMFXL + 40 +1.0 + 9 +$DIMFXLON + 70 +0 + 9 +$DIMJOGANG + 40 +0.785398163397 + 9 +$DIMTFILL + 70 +0 + 9 +$DIMTFILLCLR + 70 +0 + 9 +$DIMARCSYM + 70 +0 + 9 +$DIMLTYPE + 6 + + 9 +$DIMLTEX1 + 6 + + 9 +$DIMLTEX2 + 6 + + 9 +$DIMTXTDIRECTION + 70 +0 + 9 +$LUNITS + 70 +2 + 9 +$LUPREC + 70 +4 + 9 +$SKETCHINC + 40 +1.0 + 9 +$FILLETRAD + 40 +10.0 + 9 +$AUNITS + 70 +0 + 9 +$AUPREC + 70 +2 + 9 +$MENU + 1 +. + 9 +$ELEVATION + 40 +0.0 + 9 +$PELEVATION + 40 +0.0 + 9 +$THICKNESS + 40 +0.0 + 9 +$LIMCHECK + 70 +0 + 9 +$CHAMFERA + 40 +0.0 + 9 +$CHAMFERB + 40 +0.0 + 9 +$CHAMFERC + 40 +0.0 + 9 +$CHAMFERD + 40 +0.0 + 9 +$SKPOLY + 70 +0 + 9 +$TDCREATE + 40 +2461298.5564930555 + 9 +$TDUCREATE + 40 +2458532.153996898 + 9 +$TDUPDATE + 40 +2461298.5564930555 + 9 +$TDUUPDATE + 40 +2458532.1544311 + 9 +$TDINDWG + 40 +0.0 + 9 +$TDUSRTIMER + 40 +0.0 + 9 +$USRTIMER + 70 +1 + 9 +$ANGBASE + 50 +0.0 + 9 +$ANGDIR + 70 +0 + 9 +$PDMODE + 70 +0 + 9 +$PDSIZE + 40 +0.0 + 9 +$PLINEWID + 40 +0.0 + 9 +$SPLFRAME + 70 +0 + 9 +$SPLINETYPE + 70 +6 + 9 +$SPLINESEGS + 70 +8 + 9 +$HANDSEED + 5 +99 + 9 +$SURFTAB1 + 70 +6 + 9 +$SURFTAB2 + 70 +6 + 9 +$SURFTYPE + 70 +6 + 9 +$SURFU + 70 +6 + 9 +$SURFV + 70 +6 + 9 +$UCSBASE + 2 + + 9 +$UCSNAME + 2 + + 9 +$UCSORG + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSXDIR + 10 +1.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSYDIR + 10 +0.0 + 20 +1.0 + 30 +0.0 + 9 +$UCSORTHOREF + 2 + + 9 +$UCSORTHOVIEW + 70 +0 + 9 +$UCSORGTOP + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGBOTTOM + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGLEFT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGRIGHT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGFRONT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGBACK + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSBASE + 2 + + 9 +$PUCSNAME + 2 + + 9 +$PUCSORG + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSXDIR + 10 +1.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSYDIR + 10 +0.0 + 20 +1.0 + 30 +0.0 + 9 +$PUCSORTHOREF + 2 + + 9 +$PUCSORTHOVIEW + 70 +0 + 9 +$PUCSORGTOP + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGBOTTOM + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGLEFT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGRIGHT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGFRONT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGBACK + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$USERI1 + 70 +0 + 9 +$USERI2 + 70 +0 + 9 +$USERI3 + 70 +0 + 9 +$USERI4 + 70 +0 + 9 +$USERI5 + 70 +0 + 9 +$USERR1 + 40 +0.0 + 9 +$USERR2 + 40 +0.0 + 9 +$USERR3 + 40 +0.0 + 9 +$USERR4 + 40 +0.0 + 9 +$USERR5 + 40 +0.0 + 9 +$WORLDVIEW + 70 +1 + 9 +$SHADEDGE + 70 +3 + 9 +$SHADEDIF + 70 +70 + 9 +$TILEMODE + 70 +0 + 9 +$MAXACTVP + 70 +64 + 9 +$PINSBASE + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PLIMCHECK + 70 +0 + 9 +$PEXTMIN + 10 +1e+20 + 20 +1e+20 + 30 +1e+20 + 9 +$PEXTMAX + 10 +-1e+20 + 20 +-1e+20 + 30 +-1e+20 + 9 +$PLIMMIN + 10 +-0.0 + 20 +-0.0 + 9 +$PLIMMAX + 10 +11.0 + 20 +8.5 + 9 +$UNITMODE + 70 +0 + 9 +$VISRETAIN + 70 +1 + 9 +$PLINEGEN + 70 +0 + 9 +$PSLTSCALE + 70 +1 + 9 +$TREEDEPTH + 70 +3020 + 9 +$CMLSTYLE + 2 +Standard + 9 +$CMLJUST + 70 +0 + 9 +$CMLSCALE + 40 +20.0 + 9 +$PROXYGRAPHICS + 70 +1 + 9 +$MEASUREMENT + 70 +0 + 9 +$CELWEIGHT +370 +-1 + 9 +$ENDCAPS +280 +0 + 9 +$JOINSTYLE +280 +0 + 9 +$LWDISPLAY +290 +0 + 9 +$INSUNITS + 70 +1 + 9 +$HYPERLINKBASE + 1 + + 9 +$STYLESHEET + 1 + + 9 +$XEDIT +290 +1 + 9 +$CEPSNTYPE +380 +0 + 9 +$PSTYLEMODE +290 +1 + 9 +$FINGERPRINTGUID + 2 +{AE9B3164-57C5-40A6-B897-3AA314A90313} + 9 +$VERSIONGUID + 2 +{DBBE3EFA-6A7C-4482-B175-433C33A11394} + 9 +$EXTNAMES +290 +1 + 9 +$PSVPSCALE + 40 +0.0 + 9 +$OLESTARTUP +290 +0 + 9 +$SORTENTS +280 +127 + 9 +$INDEXCTL +280 +0 + 9 +$HIDETEXT +280 +1 + 9 +$XCLIPFRAME +280 +1 + 9 +$HALOGAP +280 +0 + 9 +$OBSCOLOR + 70 +257 + 9 +$OBSLTYPE +280 +0 + 9 +$INTERSECTIONDISPLAY +280 +0 + 9 +$INTERSECTIONCOLOR + 70 +257 + 9 +$DIMASSOC +280 +2 + 9 +$PROJECTNAME + 1 + + 9 +$CAMERADISPLAY +290 +0 + 9 +$LENSLENGTH + 40 +50.0 + 9 +$CAMERAHEIGHT + 40 +0.0 + 9 +$STEPSPERSEC + 40 +24.0 + 9 +$STEPSIZE + 40 +100.0 + 9 +$3DDWFPREC + 40 +2.0 + 9 +$PSOLWIDTH + 40 +0.005 + 9 +$PSOLHEIGHT + 40 +0.08 + 9 +$LOFTANG1 + 40 +1.570796326795 + 9 +$LOFTANG2 + 40 +1.570796326795 + 9 +$LOFTMAG1 + 40 +0.0 + 9 +$LOFTMAG2 + 40 +0.0 + 9 +$LOFTPARAM + 70 +7 + 9 +$LOFTNORMALS +280 +1 + 9 +$LATITUDE + 40 +37.795 + 9 +$LONGITUDE + 40 +-122.394 + 9 +$NORTHDIRECTION + 40 +0.0 + 9 +$TIMEZONE + 70 +-8000 + 9 +$LIGHTGLYPHDISPLAY +280 +1 + 9 +$TILEMODELIGHTSYNCH +280 +1 + 9 +$CMATERIAL +347 +20 + 9 +$SOLIDHIST +280 +0 + 9 +$SHOWHIST +280 +1 + 9 +$DWFFRAME +280 +2 + 9 +$DGNFRAME +280 +2 + 9 +$REALWORLDSCALE +290 +1 + 9 +$INTERFERECOLOR + 62 +256 + 9 +$CSHADOW +280 +0 + 9 +$SHADOWPLANELOCATION + 40 +0.0 + 0 +ENDSEC + 0 +SECTION + 2 +CLASSES + 0 +CLASS + 1 +ACDBDICTIONARYWDFLT + 2 +AcDbDictionaryWithDefault + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +SUN + 2 +AcDbSun + 3 +SCENEOE + 90 +1153 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +VISUALSTYLE + 2 +AcDbVisualStyle + 3 +ObjectDBX Classes + 90 +4095 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +MATERIAL + 2 +AcDbMaterial + 3 +ObjectDBX Classes + 90 +1153 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +SCALE + 2 +AcDbScale + 3 +ObjectDBX Classes + 90 +1153 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +TABLESTYLE + 2 +AcDbTableStyle + 3 +ObjectDBX Classes + 90 +4095 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +MLEADERSTYLE + 2 +AcDbMLeaderStyle + 3 +ACDB_MLEADERSTYLE_CLASS + 90 +4095 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +DICTIONARYVAR + 2 +AcDbDictionaryVar + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +CELLSTYLEMAP + 2 +AcDbCellStyleMap + 3 +ObjectDBX Classes + 90 +1152 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +MENTALRAYRENDERSETTINGS + 2 +AcDbMentalRayRenderSettings + 3 +SCENEOE + 90 +1024 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +ACDBDETAILVIEWSTYLE + 2 +AcDbDetailViewStyle + 3 +ObjectDBX Classes + 90 +1025 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +ACDBSECTIONVIEWSTYLE + 2 +AcDbSectionViewStyle + 3 +ObjectDBX Classes + 90 +1025 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +RASTERVARIABLES + 2 +AcDbRasterVariables + 3 +ISM + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +LAYOUT + 2 +AcDbLayout + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +ACDBPLACEHOLDER + 2 +AcDbPlaceHolder + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +ENDSEC + 0 +SECTION + 2 +TABLES + 0 +TABLE + 2 +VPORT + 5 +8 +330 +0 +100 +AcDbSymbolTable + 70 +1 + 0 +VPORT + 5 +8B +330 +8 +100 +AcDbSymbolTableRecord +100 +AcDbViewportTableRecord + 2 +*Active + 70 +0 + 10 +0.0 + 20 +0.0 + 11 +1.0 + 21 +1.0 + 12 +5.5 + 22 +4.25 + 13 +0.0 + 23 +0.0 + 14 +0.5 + 24 +0.5 + 15 +0.5 + 25 +0.5 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 40 +12.0 + 41 +1.34 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 50 +0.0 + 51 +0.0 + 71 +0 + 72 +1000 + 73 +1 + 74 +3 + 75 +0 + 76 +0 + 77 +0 + 78 +0 +281 +0 + 65 +1 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 +0 +146 +0.0 + 0 +ENDTAB + 0 +TABLE + 2 +LTYPE + 5 +2 +330 +0 +100 +AcDbSymbolTable + 70 +21 + 0 +LTYPE + 5 +24 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByBlock + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0.0 + 0 +LTYPE + 5 +25 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByLayer + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0.0 + 0 +LTYPE + 5 +26 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +Continuous + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0.0 + 0 +LTYPE + 5 +2F +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +CENTER + 70 +0 + 3 +Center ____ _ ____ _ ____ _ ____ _ ____ _ ____ + 72 +65 + 73 +4 + 40 +5.08 + 49 +3.175 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +0.635 + 74 +0 + 49 +-0.635 + 74 +0 + 0 +LTYPE + 5 +30 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +CENTERX2 + 70 +0 + 3 +Center (2x) ________ __ ________ __ ________ + 72 +65 + 73 +4 + 40 +8.89 + 49 +6.35 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +1.27 + 74 +0 + 49 +-0.635 + 74 +0 + 0 +LTYPE + 5 +31 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +CENTER2 + 70 +0 + 3 +Center (.5x) ____ _ ____ _ ____ _ ____ _ ____ + 72 +65 + 73 +4 + 40 +2.54 + 49 +1.587 + 74 +0 + 49 +-0.318 + 74 +0 + 49 +0.318 + 74 +0 + 49 +-0.318 + 74 +0 + 0 +LTYPE + 5 +32 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHED + 70 +0 + 3 +Dashed __ __ __ __ __ __ __ __ __ __ __ __ __ _ + 72 +65 + 73 +2 + 40 +1.524 + 49 +1.27 + 74 +0 + 49 +-0.254 + 74 +0 + 0 +LTYPE + 5 +33 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHEDX2 + 70 +0 + 3 +Dashed (2x) ____ ____ ____ ____ ____ ____ + 72 +65 + 73 +2 + 40 +3.048 + 49 +2.54 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +34 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHED2 + 70 +0 + 3 +Dashed (.5x) _ _ _ _ _ _ _ _ _ _ _ _ _ _ + 72 +65 + 73 +2 + 40 +0.762 + 49 +0.635 + 74 +0 + 49 +-0.127 + 74 +0 + 0 +LTYPE + 5 +35 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +PHANTOM + 70 +0 + 3 +Phantom ______ __ __ ______ __ __ ______ + 72 +65 + 73 +6 + 40 +6.35 + 49 +3.175 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +0.635 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +0.635 + 74 +0 + 49 +-0.635 + 74 +0 + 0 +LTYPE + 5 +36 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +PHANTOMX2 + 70 +0 + 3 +Phantom (2x)____________ ____ ____ ____________ + 72 +65 + 73 +6 + 40 +10.795 + 49 +6.35 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +1.27 + 74 +0 + 49 +-0.635 + 74 +0 + 49 +1.27 + 74 +0 + 49 +-0.635 + 74 +0 + 0 +LTYPE + 5 +37 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +PHANTOM2 + 70 +0 + 3 +Phantom (.5x) ___ _ _ ___ _ _ ___ _ _ ___ _ _ ___ + 72 +65 + 73 +6 + 40 +3.175 + 49 +1.587 + 74 +0 + 49 +-0.318 + 74 +0 + 49 +0.318 + 74 +0 + 49 +-0.318 + 74 +0 + 49 +0.318 + 74 +0 + 49 +-0.318 + 74 +0 + 0 +LTYPE + 5 +38 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHDOT + 70 +0 + 3 +Dash dot __ . __ . __ . __ . __ . __ . __ . __ + 72 +65 + 73 +4 + 40 +3.556 + 49 +2.54 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +39 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHDOTX2 + 70 +0 + 3 +Dash dot (2x) ____ . ____ . ____ . ____ + 72 +65 + 73 +4 + 40 +6.096 + 49 +5.08 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +3A +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DASHDOT2 + 70 +0 + 3 +Dash dot (.5x) _ . _ . _ . _ . _ . _ . _ . _ + 72 +65 + 73 +4 + 40 +1.778 + 49 +1.27 + 74 +0 + 49 +-0.254 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.254 + 74 +0 + 0 +LTYPE + 5 +3B +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DOT + 70 +0 + 3 +Dot . . . . . . . . . . . . . . . . + 72 +65 + 73 +2 + 40 +0.508 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +3C +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DOTX2 + 70 +0 + 3 +Dot (2x) . . . . . . . . + 72 +65 + 73 +2 + 40 +1.016 + 49 +0.0 + 74 +0 + 49 +-1.016 + 74 +0 + 0 +LTYPE + 5 +3D +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DOT2 + 70 +0 + 3 +Dot (.5) . . . . . . . . . . . . . . . . . . . + 72 +65 + 73 +2 + 40 +0.254 + 49 +0.0 + 74 +0 + 49 +-0.254 + 74 +0 + 0 +LTYPE + 5 +3E +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DIVIDE + 70 +0 + 3 +Divide __ . . __ . . __ . . __ . . __ . . __ + 72 +65 + 73 +6 + 40 +4.064 + 49 +2.54 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +3F +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DIVIDEX2 + 70 +0 + 3 +Divide (2x) ____ . . ____ . . ____ . . ____ + 72 +65 + 73 +6 + 40 +6.604 + 49 +5.08 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.508 + 74 +0 + 0 +LTYPE + 5 +40 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +DIVIDE2 + 70 +0 + 3 +Divide(.5x) _ . _ . _ . _ . _ . _ . _ . _ + 72 +65 + 73 +6 + 40 +2.032 + 49 +1.27 + 74 +0 + 49 +-0.254 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.254 + 74 +0 + 49 +0.0 + 74 +0 + 49 +-0.254 + 74 +0 + 0 +ENDTAB + 0 +TABLE + 2 +LAYER + 5 +1 +330 +0 +100 +AcDbSymbolTable + 70 +2 + 0 +LAYER + 5 +27 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +0 + 70 +0 + 62 +7 + 6 +Continuous +370 +-3 +390 +13 +347 +21 + 0 +LAYER + 5 +28 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +Defpoints + 70 +0 + 62 +7 + 6 +Continuous +290 +0 +370 +-3 +390 +13 +347 +21 + 0 +ENDTAB + 0 +TABLE + 2 +STYLE + 5 +5 +330 +0 +100 +AcDbSymbolTable + 70 +26 + 0 +STYLE + 5 +29 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +Standard + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +txt + 4 + + 0 +STYLE + 5 +41 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-Light + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-Light.ttf + 4 + + 0 +STYLE + 5 +42 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-Light-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-LightItalic.ttf + 4 + + 0 +STYLE + 5 +43 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-Regular.ttf + 4 + + 0 +STYLE + 5 +44 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-Italic.ttf + 4 + + 0 +STYLE + 5 +45 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-SemiBold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-SemiBold.ttf + 4 + + 0 +STYLE + 5 +46 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-SemiBoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-SemiBoldItalic.ttf + 4 + + 0 +STYLE + 5 +47 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-Bold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-Bold.ttf + 4 + + 0 +STYLE + 5 +48 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-BoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-BoldItalic.ttf + 4 + + 0 +STYLE + 5 +49 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-ExtraBold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-ExtraBold.ttf + 4 + + 0 +STYLE + 5 +4A +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSans-ExtraBoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSans-ExtraBoldItalic.ttf + 4 + + 0 +STYLE + 5 +4B +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSansCondensed-Bold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSansCondensed-Bold.ttf + 4 + + 0 +STYLE + 5 +4C +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSansCondensed-Light + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSansCondensed-Light.ttf + 4 + + 0 +STYLE + 5 +4D +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +OpenSansCondensed-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +OpenSansCondensed-LightItalic.ttf + 4 + + 0 +STYLE + 5 +4E +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSans + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSans-Regular.ttf + 4 + + 0 +STYLE + 5 +4F +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSans-Bold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSans-Bold.ttf + 4 + + 0 +STYLE + 5 +50 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSans-BoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSans-BoldItalic.ttf + 4 + + 0 +STYLE + 5 +51 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSans-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSans-Italic.ttf + 4 + + 0 +STYLE + 5 +52 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSerif + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSerif-Regular.ttf + 4 + + 0 +STYLE + 5 +53 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSerif-Bold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSerif-Bold.ttf + 4 + + 0 +STYLE + 5 +54 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSerif-BoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSerif-BoldItalic.ttf + 4 + + 0 +STYLE + 5 +55 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationSerif-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationSerif-Italic.ttf + 4 + + 0 +STYLE + 5 +56 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationMono + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationMono-Regular.ttf + 4 + + 0 +STYLE + 5 +57 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationMono-Bold + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationMono-Bold.ttf + 4 + + 0 +STYLE + 5 +58 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationMono-BoldItalic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationMono-BoldItalic.ttf + 4 + + 0 +STYLE + 5 +59 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +LiberationMono-Italic + 70 +0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 +0 + 42 +2.5 + 3 +LiberationMono-Italic.ttf + 4 + + 0 +ENDTAB + 0 +TABLE + 2 +VIEW + 5 +7 +330 +0 +100 +AcDbSymbolTable + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +UCS + 5 +6 +330 +0 +100 +AcDbSymbolTable + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +APPID + 5 +3 +330 +0 +100 +AcDbSymbolTable + 70 +3 + 0 +APPID + 5 +2A +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD + 70 +0 + 0 +APPID + 5 +96 +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +HATCHBACKGROUNDCOLOR + 70 +0 + 0 +APPID + 5 +97 +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +EZDXF + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +DIMSTYLE + 5 +4 +330 +0 +100 +AcDbSymbolTable + 70 +12 +100 +AcDbDimStyleTable + 0 +DIMSTYLE +105 +2B +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +Standard + 70 +0 + 40 +1.0 + 41 +2.5 + 42 +0.625 + 43 +3.75 + 44 +1.25 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +2.5 +141 +2.5 +142 +0.0 +143 +0.03937007874 +144 +1.0 +145 +0.0 +146 +1.0 +147 +0.625 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +8 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +44 +279 +0 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +0 +289 +3 +290 +0 +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +73 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZDXF + 70 +0 + 40 +1.0 + 41 +0.175 + 42 +0.125 + 43 +3.75 + 44 +0.375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.25 +141 +2.5 +142 +0.0 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.1 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +342 +74 +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +7C +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_100_H25_CM + 70 +0 + 40 +1.0 + 41 +0.25 + 42 +0.125 + 43 +3.75 + 44 +0.375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.25 +141 +2.5 +142 +0.125 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.1 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +7D +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_50_H25_CM + 70 +0 + 40 +1.0 + 41 +0.125 + 42 +0.0625 + 43 +3.75 + 44 +0.1875 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.125 +141 +2.5 +142 +0.0625 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.05 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +7E +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_25_H25_CM + 70 +0 + 40 +1.0 + 41 +0.0625 + 42 +0.03125 + 43 +3.75 + 44 +0.09375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.0625 +141 +2.5 +142 +0.03125 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.025 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +7F +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_20_H25_CM + 70 +0 + 40 +1.0 + 41 +0.05 + 42 +0.025 + 43 +3.75 + 44 +0.07500000000000001 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.05 +141 +2.5 +142 +0.025 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.020000000000000004 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +80 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_10_H25_CM + 70 +0 + 40 +1.0 + 41 +0.025 + 42 +0.0125 + 43 +3.75 + 44 +0.037500000000000006 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.025 +141 +2.5 +142 +0.0125 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.010000000000000002 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +81 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_5_H25_CM + 70 +0 + 40 +1.0 + 41 +0.0125 + 42 +0.00625 + 43 +3.75 + 44 +0.018750000000000003 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.0125 +141 +2.5 +142 +0.00625 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.005000000000000001 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +82 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_M_1_H25_CM + 70 +0 + 40 +1.0 + 41 +0.0025 + 42 +0.00125 + 43 +3.75 + 44 +0.00375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.0025 +141 +2.5 +142 +0.00125 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.001 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +83 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_RADIUS + 70 +0 + 40 +1.0 + 41 +0.25 + 42 +0.125 + 43 +3.75 + 44 +0.375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.25 +141 +0.0 +142 +0.0 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.1 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +0 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +1 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +342 +84 +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +88 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_RADIUS_INSIDE + 70 +0 + 40 +1.0 + 41 +0.25 + 42 +0.125 + 43 +3.75 + 44 +0.375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.25 +141 +0.0 +142 +0.0 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.1 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +0 + 78 +12 + 79 +3 +170 +0 +171 +3 +172 +0 +173 +0 +174 +1 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +0 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +0 +290 +0 +340 +4C +342 +84 +371 +-2 +372 +-2 + 0 +DIMSTYLE +105 +89 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +EZ_CURVED + 70 +0 + 40 +1.0 + 41 +0.25 + 42 +0.125 + 43 +3.75 + 44 +0.375 + 45 +0.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +2.5 +140 +0.25 +141 +2.5 +142 +0.0 +143 +0.03937007874 +144 +100.0 +145 +0.0 +146 +0.5 +147 +0.1 +148 +0.0 + 69 +0 + 70 +0 + 71 +0 + 72 +0 + 73 +0 + 74 +0 + 75 +0 + 76 +0 + 77 +1 + 78 +12 + 79 +2 + 90 +2 +170 +0 +171 +3 +172 +1 +173 +0 +174 +0 +175 +0 +176 +0 +177 +0 +178 +0 +179 +2 +271 +2 +272 +2 +273 +2 +274 +3 +275 +0 +276 +0 +277 +2 +278 +46 +279 +2 +280 +0 +281 +0 +282 +0 +283 +0 +284 +8 +285 +0 +286 +0 +288 +1 +289 +3 +290 +0 +340 +4C +371 +-2 +372 +-2 + 0 +ENDTAB + 0 +TABLE + 2 +BLOCK_RECORD + 5 +9 +330 +0 +100 +AcDbSymbolTable + 70 +6 + 0 +BLOCK_RECORD + 5 +17 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Model_Space +340 +1A + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +1B +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Paper_Space +340 +1E + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +74 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +_ARCHTICK +340 +0 + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +78 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +_CLOSEDFILLED +340 +0 + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +84 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +_CLOSEDBLANK +340 +0 + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +8C +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +SyntheticImage +340 +0 + 70 +0 +280 +1 +281 +0 + 0 +ENDTAB + 0 +ENDSEC + 0 +SECTION + 2 +BLOCKS + 0 +BLOCK + 5 +18 +330 +17 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*Model_Space + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Model_Space + 1 + + 0 +ENDBLK + 5 +19 +330 +17 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +1C +330 +1B +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*Paper_Space + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Paper_Space + 1 + + 0 +ENDBLK + 5 +1D +330 +1B +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +75 +330 +74 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +_ARCHTICK + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +_ARCHTICK + 1 + + 0 +LWPOLYLINE + 5 +77 +330 +74 +100 +AcDbEntity + 8 +0 + 6 +BYBLOCK + 62 +0 +100 +AcDbPolyline + 90 +2 + 70 +0 + 43 +0.15 + 10 +-0.5 + 20 +-0.5 + 10 +0.5 + 20 +0.5 + 0 +ENDBLK + 5 +76 +330 +74 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +79 +330 +78 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +_CLOSEDFILLED + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +_CLOSEDFILLED + 1 + + 0 +SOLID + 5 +7B +330 +78 +100 +AcDbEntity + 8 +0 + 6 +BYBLOCK + 62 +0 +100 +AcDbTrace + 10 +-1.0 + 20 +0.16439898372402537 + 30 +0.0 + 11 +0.0 + 21 +0.0 + 31 +0.0 + 12 +-1.0 + 22 +-0.16439898372402537 + 32 +0.0 + 13 +-1.0 + 23 +-0.16439898372402537 + 33 +0.0 + 0 +ENDBLK + 5 +7A +330 +78 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +85 +330 +84 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +_CLOSEDBLANK + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +_CLOSEDBLANK + 1 + + 0 +LWPOLYLINE + 5 +87 +330 +84 +100 +AcDbEntity + 8 +0 + 6 +BYBLOCK + 62 +0 +100 +AcDbPolyline + 90 +3 + 70 +1 + 10 +-1.0 + 20 +0.16439898372402537 + 10 +0.0 + 20 +0.0 + 10 +-1.0 + 20 +-0.16439898372402537 + 0 +ENDBLK + 5 +86 +330 +84 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +8D +330 +8C +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +SyntheticImage + 70 +0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +SyntheticImage + 1 + + 0 +OLE2FRAME + 5 +8F +330 +8C +100 +AcDbEntity + 8 +0 +100 +AcDbOle2Frame + 70 +2 + 3 +Picture + 10 +0.0 + 20 +1.0 + 30 +0.0 + 11 +2.0 + 21 +0.0 + 31 +0.0 + 71 +3 + 72 +0 + 73 +0 + 90 +8192 +310 +D0CF11E0A1B11AE1000000000000000000000000000000003E000300FEFF090006000000000000000000000001000000000000000000000000100000FEFFFFFF00000000FEFFFFFF0000000001000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFF52006F006F007400200045006E00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000501FFFFFFFFFFFFFFFF01000000000000000000000000000000000000000000000000000000000000000000000000000000FEFFFFFF000000 +310 +000000000043004F004E00540045004E005400530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000201FFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000000000000000000000000000020000003618 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +310 +0000000000000000FEFFFFFFFDFFFFFF030000000400000005000000060000000700000008000000090000000A0000000B0000000C0000000D0000000E000000FEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +310 +FFFFFFFFFFFFFFFFFFFFFFFF424D361800000000000036000000280000004000000020000000010018000000000000180000C40E0000C40E000000000000000000000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF +310 +0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF00FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF00 +310 +00FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000 +310 +FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF000000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF0000FF0000FF00FF0000 +310 +FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF0000FF0000FF00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF0000FF00FF0000FF0000FF0000FF0000 +310 +FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000 +310 +FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00 +310 +00FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF00000000FF00FF0000FF0000FF000000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF +310 +0000FF0000FF0000FF0000FF0000FF0000FFFF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF0000FF000000000000000000000000000000 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +310 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + 1 +OLE + 0 +ENDBLK + 5 +8E +330 +8C +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +ENDSEC + 0 +SECTION + 2 +ENTITIES + 0 +VIEWPORT + 5 +8A +330 +1B +100 +AcDbEntity + 67 +1 + 8 +VIEWPORTS +100 +AcDbViewport + 10 +5.5 + 20 +4.25 + 30 +0.0 + 40 +12.100000000000001 + 41 +9.350000000000001 + 68 +1 + 69 +1 + 12 +5.5 + 22 +4.25 + 13 +0.0 + 23 +0.0 + 14 +10.0 + 24 +10.0 + 15 +10.0 + 25 +10.0 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +9.350000000000001 + 50 +0.0 + 51 +0.0 + 72 +100 + 90 +557088 + 1 + +281 +0 + 71 +0 + 74 +0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 +0 +146 +0.0 +282 +0 + 0 +INSERT + 5 +90 +330 +1B +100 +AcDbEntity + 67 +1 + 8 +0 +100 +AcDbBlockReference + 2 +SyntheticImage + 10 +2.0 + 20 +4.0 + 30 +0.0 + 0 +LWPOLYLINE + 5 +92 +330 +1B +100 +AcDbEntity + 67 +1 + 8 +0 +100 +AcDbPolyline + 90 +4 + 70 +1 + 10 +2.0 + 20 +4.0 + 10 +4.0 + 20 +4.0 + 10 +4.0 + 20 +5.0 + 10 +2.0 + 20 +5.0 + 0 +TEXT + 5 +93 +330 +1B +100 +AcDbEntity + 67 +1 + 8 +0 +100 +AcDbText + 10 +2.0 + 20 +5.4 + 30 +0.0 + 40 +0.2 + 1 +EMBEDDED BITMAP +100 +AcDbText + 0 +LWPOLYLINE + 5 +94 +330 +1B +100 +AcDbEntity + 67 +1 + 8 +0 +100 +AcDbPolyline + 90 +3 + 70 +1 + 10 +6.0 + 20 +4.0 + 10 +8.0 + 20 +4.0 + 10 +7.0 + 20 +5.0 + 0 +TEXT + 5 +95 +330 +1B +100 +AcDbEntity + 67 +1 + 8 +0 +100 +AcDbText + 10 +6.0 + 20 +5.4 + 30 +0.0 + 40 +0.2 + 1 +VECTOR CONTROL +100 +AcDbText + 0 +ENDSEC + 0 +SECTION + 2 +OBJECTS + 0 +DICTIONARY + 5 +A +330 +0 +100 +AcDbDictionary +281 +1 + 3 +ACAD_COLOR +350 +B + 3 +ACAD_GROUP +350 +C + 3 +ACAD_LAYOUT +350 +D + 3 +ACAD_MATERIAL +350 +E + 3 +ACAD_MLEADERSTYLE +350 +F + 3 +ACAD_MLINESTYLE +350 +10 + 3 +ACAD_PLOTSETTINGS +350 +11 + 3 +ACAD_PLOTSTYLENAME +350 +12 + 3 +ACAD_SCALELIST +350 +14 + 3 +ACAD_TABLESTYLE +350 +15 + 3 +ACAD_VISUALSTYLE +350 +16 + 3 +EZDXF_META +350 +2D + 0 +DICTIONARY + 5 +B +330 +A +100 +AcDbDictionary +281 +1 + 0 +DICTIONARY + 5 +C +330 +A +100 +AcDbDictionary +281 +1 + 0 +DICTIONARY + 5 +D +330 +A +100 +AcDbDictionary +281 +1 + 3 +Model +350 +1A + 3 +Layout1 +350 +1E + 0 +DICTIONARY + 5 +E +330 +A +100 +AcDbDictionary +281 +1 + 3 +ByBlock +350 +1F + 3 +ByLayer +350 +20 + 3 +Global +350 +21 + 0 +DICTIONARY + 5 +F +330 +A +100 +AcDbDictionary +281 +1 + 3 +Standard +350 +2C + 0 +DICTIONARY + 5 +10 +330 +A +100 +AcDbDictionary +281 +1 + 3 +Standard +350 +22 + 0 +DICTIONARY + 5 +11 +330 +A +100 +AcDbDictionary +281 +1 + 0 +ACDBDICTIONARYWDFLT + 5 +12 +330 +A +100 +AcDbDictionary +281 +1 + 3 +Normal +350 +13 +100 +AcDbDictionaryWithDefault +340 +13 + 0 +ACDBPLACEHOLDER + 5 +13 +330 +12 + 0 +DICTIONARY + 5 +14 +330 +A +100 +AcDbDictionary +281 +1 + 0 +DICTIONARY + 5 +15 +330 +A +100 +AcDbDictionary +281 +1 + 0 +DICTIONARY + 5 +16 +330 +A +100 +AcDbDictionary +281 +1 + 3 +2dWireframe +350 +5A + 3 +Basic +350 +5B + 3 +Brighten +350 +5C + 3 +ColorChange +350 +5D + 3 +Conceptual +350 +5E + 3 +Dim +350 +5F + 3 +EdgeColorOff +350 +60 + 3 +Facepattern +350 +61 + 3 +Flat +350 +62 + 3 +FlatWithEdges +350 +63 + 3 +Gouraud +350 +64 + 3 +GouraudWithEdges +350 +65 + 3 +Hidden +350 +66 + 3 +JitterOff +350 +67 + 3 +Linepattern +350 +68 + 3 +Modeling +350 +69 + 3 +OverhangOff +350 +6A + 3 +Realistic +350 +6B + 3 +Shaded +350 +6C + 3 +Shaded with edges +350 +6D + 3 +Shades of Gray +350 +6E + 3 +Sketchy +350 +6F + 3 +Thicken +350 +70 + 3 +Wireframe +350 +71 + 3 +X-Ray +350 +72 + 0 +LAYOUT + 5 +1A +330 +D +100 +AcDbPlotSettings + 1 + + 4 +A3 + 6 + + 40 +7.5 + 41 +20.0 + 42 +7.5 + 43 +20.0 + 44 +420.0 + 45 +297.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +1.0 + 70 +1024 + 72 +1 + 73 +0 + 74 +5 + 7 + + 75 +16 + 76 +0 + 77 +2 + 78 +300 +147 +1.0 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +Model + 70 +1 + 71 +0 + 10 +0.0 + 20 +0.0 + 11 +420.0 + 21 +297.0 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +1e+20 + 24 +1e+20 + 34 +1e+20 + 15 +-1e+20 + 25 +-1e+20 + 35 +-1e+20 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 +1 +330 +17 + 0 +LAYOUT + 5 +1E +330 +D +100 +AcDbPlotSettings + 1 + + 2 +Example saved printer + 4 +ezdxf_(11.00_x_8.50_Inches) + 6 + + 40 +0.0 + 41 +0.0 + 42 +0.0 + 43 +0.0 + 44 +279.4 + 45 +215.89999999999998 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +1.0 + 70 +672 + 72 +0 + 73 +0 + 74 +5 + 7 + + 75 +16 + 76 +0 + 77 +2 + 78 +300 +147 +0.03937007874015748 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +Layout1 + 70 +1 + 71 +1 + 10 +-0.0 + 20 +-0.0 + 11 +11.0 + 21 +8.5 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +1e+20 + 24 +1e+20 + 34 +1e+20 + 15 +-1e+20 + 25 +-1e+20 + 35 +-1e+20 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 +1 +330 +1B +331 +8A + 0 +MATERIAL + 5 +1F +102 +{ACAD_REACTORS +330 +E +102 +} +330 +E +100 +AcDbMaterial + 1 +ByBlock + 2 + + 70 +0 + 40 +1.0 + 71 +1 + 41 +1.0 + 91 +-1023410177 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 44 +0.5 + 73 +0 + 45 +1.0 + 46 +1.0 + 77 +1 + 4 + + 78 +1 + 79 +1 +170 +1 + 48 +1.0 +171 +1 + 6 + +172 +1 +173 +1 +174 +1 +140 +1.0 +141 +1.0 +175 +1 + 7 + +176 +1 +177 +1 +178 +1 +143 +1.0 +179 +1 + 8 + +270 +1 +271 +1 +272 +1 +145 +1.0 +146 +1.0 +273 +1 + 9 + +274 +1 +275 +1 +276 +1 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 94 +63 + 0 +MATERIAL + 5 +20 +102 +{ACAD_REACTORS +330 +E +102 +} +330 +E +100 +AcDbMaterial + 1 +ByLayer + 2 + + 70 +0 + 40 +1.0 + 71 +1 + 41 +1.0 + 91 +-1023410177 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 44 +0.5 + 73 +0 + 45 +1.0 + 46 +1.0 + 77 +1 + 4 + + 78 +1 + 79 +1 +170 +1 + 48 +1.0 +171 +1 + 6 + +172 +1 +173 +1 +174 +1 +140 +1.0 +141 +1.0 +175 +1 + 7 + +176 +1 +177 +1 +178 +1 +143 +1.0 +179 +1 + 8 + +270 +1 +271 +1 +272 +1 +145 +1.0 +146 +1.0 +273 +1 + 9 + +274 +1 +275 +1 +276 +1 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 94 +63 + 0 +MATERIAL + 5 +21 +102 +{ACAD_REACTORS +330 +E +102 +} +330 +E +100 +AcDbMaterial + 1 +Global + 2 + + 70 +0 + 40 +1.0 + 71 +1 + 41 +1.0 + 91 +-1023410177 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 44 +0.5 + 73 +0 + 45 +1.0 + 46 +1.0 + 77 +1 + 4 + + 78 +1 + 79 +1 +170 +1 + 48 +1.0 +171 +1 + 6 + +172 +1 +173 +1 +174 +1 +140 +1.0 +141 +1.0 +175 +1 + 7 + +176 +1 +177 +1 +178 +1 +143 +1.0 +179 +1 + 8 + +270 +1 +271 +1 +272 +1 +145 +1.0 +146 +1.0 +273 +1 + 9 + +274 +1 +275 +1 +276 +1 + 42 +1.0 + 72 +1 + 3 + + 73 +1 + 74 +1 + 75 +1 + 94 +63 + 0 +MLINESTYLE + 5 +22 +102 +{ACAD_REACTORS +330 +10 +102 +} +330 +10 +100 +AcDbMlineStyle + 2 +Standard + 70 +0 + 3 + + 62 +256 + 51 +90.0 + 52 +90.0 + 71 +2 + 49 +0.5 + 62 +256 + 6 +BYLAYER + 49 +-0.5 + 62 +256 + 6 +BYLAYER + 0 +MLEADERSTYLE + 5 +2C +102 +{ACAD_REACTORS +330 +F +102 +} +330 +F +100 +AcDbMLeaderStyle +179 +2 +170 +2 +171 +1 +172 +0 + 90 +2 + 40 +0.0 + 41 +0.0 +173 +1 + 91 +-1056964608 + 92 +-2 +290 +1 + 42 +2.0 +291 +1 + 43 +8.0 + 3 +Standard + 44 +4.0 +300 + +342 +29 +174 +1 +175 +1 +176 +0 +178 +1 + 93 +-1056964608 + 45 +4.0 +292 +0 +297 +0 + 46 +4.0 + 94 +-1056964608 + 47 +1.0 + 49 +1.0 +140 +1.0 +294 +1 +141 +0.0 +177 +0 +142 +1.0 +295 +0 +296 +0 +143 +3.75 +271 +0 +272 +9 +273 +9 + 0 +DICTIONARY + 5 +2D +330 +A +100 +AcDbDictionary +280 +1 +281 +1 + 3 +CREATED_BY_EZDXF +350 +2E + 3 +WRITTEN_BY_EZDXF +350 +98 + 0 +DICTIONARYVAR + 5 +2E +330 +2D +100 +DictionaryVariables +280 +0 + 1 +1.4.3 @ 2026-09-14T18:21:21.360639+00:00 + 0 +VISUALSTYLE + 5 +5A +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +2dWireframe + 70 +4 + 90 +0 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +5B +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Basic + 70 +7 + 90 +1 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +5C +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Brighten + 70 +12 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +5D +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +ColorChange + 70 +16 + 90 +2 + 40 +0.6 + 62 +8 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +5E +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Conceptual + 70 +9 + 90 +3 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +5F +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Dim + 70 +11 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +60 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +EdgeColorOff + 70 +22 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +61 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Facepattern + 70 +15 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +62 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Flat + 70 +0 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +63 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +FlatWithEdges + 70 +1 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +64 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Gouraud + 70 +2 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +65 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +GouraudWithEdges + 70 +3 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +66 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Hidden + 70 +6 + 90 +1 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +67 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +JitterOff + 70 +20 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +68 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Linepattern + 70 +14 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +69 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Modeling + 70 +10 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +6A +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +OverhangOff + 70 +21 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +6B +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Realistic + 70 +8 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +6C +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Shaded + 70 +27 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +6D +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Shaded with edges + 70 +26 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +6E +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Shades of Gray + 70 +23 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +6F +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Sketchy + 70 +24 + 90 +1 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +70 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Thicken + 70 +13 + 90 +2 + 40 +0.6 + 62 +7 +290 +0 +291 +1 + 0 +VISUALSTYLE + 5 +71 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +Wireframe + 70 +5 + 90 +0 + 40 +0.6 + 62 +7 +290 +0 +291 +0 + 0 +VISUALSTYLE + 5 +72 +102 +{ACAD_REACTORS +330 +16 +102 +} +330 +16 +100 +AcDbVisualStyle + 2 +X-Ray + 70 +25 + 90 +2 + 40 +0.5 + 62 +7 +290 +0 +291 +0 + 0 +DICTIONARYVAR + 5 +98 +330 +2D +100 +DictionaryVariables +280 +0 + 1 +1.4.3 @ 2026-09-14T18:21:21.373779+00:00 + 0 +ENDSEC + 0 +EOF diff --git a/tests/fixtures/embedded-image-print.md b/tests/fixtures/embedded-image-print.md new file mode 100644 index 000000000..8114d574d --- /dev/null +++ b/tests/fixtures/embedded-image-print.md @@ -0,0 +1,8 @@ +Synthetic regression fixture for #1266. Generated with ezdxf/Pillow from basic +geometry; it contains no external references or customer drawing content. + +Layout1 has an INSERT of SyntheticImage at (2, 4). That block contains one +OLE2FRAME whose compound-file CONTENTS stream embeds a 64 x 32 BMP: red on +the left, blue on the right, with a green diagonal from top-left to bottom-right. +The frame occupies (0, 0) to (2, 1) in the block. Text and a triangle provide +vector controls for manually opening the fixture and checking Plot Preview. diff --git a/tests/pdf_export_images_check.rs b/tests/pdf_export_images_check.rs new file mode 100644 index 000000000..c625faa36 --- /dev/null +++ b/tests/pdf_export_images_check.rs @@ -0,0 +1,512 @@ +//! #1266: decoded IMAGE/OLE content must survive the shared PDF/preview/print path. +#![cfg(not(target_arch = "wasm32"))] + +use acadrust::entities::{Insert, Viewport, Wipeout}; +use acadrust::types::{Vector2, Vector3}; +use acadrust::EntityType; +use std::sync::Arc; +use OpenCADStudio::io::pdf_export::{ + export_pdf, export_pdf_pages, PdfPageInput, PdfPlotOptions, PlotContent, PlotGroupSplits, + PlotImage, PlotWire, +}; +use OpenCADStudio::scene::model::image_model::{ImageModel, ImageQuadVertex}; +use OpenCADStudio::scene::{Scene, WireModel}; + +fn embedded_scene() -> Scene { + let mut scene = Scene::new(); + scene.document = OpenCADStudio::io::load_bytes( + "embedded-image-print.dxf", + include_bytes!("fixtures/embedded-image-print.dxf").to_vec(), + ) + .expect("synthetic embedded bitmap DXF"); + scene.images = + OpenCADStudio::scene::build_derived_caches_with_progress(&scene.document, &|_| {}, None) + .images; + scene.set_current_layout("Layout1".into()); + scene +} + +fn page(images: Vec) -> PdfPageInput { + PdfPageInput { + content: PlotContent { + images, + ..Default::default() + }, + paper_w: 100.0, + paper_h: 100.0, + offset_x: 0.0, + offset_y: 0.0, + rotation_deg: 0, + scale: 1.0, + clip: None, + options: PdfPlotOptions::default(), + plot_style: None, + } +} + +fn export(pages: &[PdfPageInput]) -> Vec { + static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let path = std::env::temp_dir().join(format!( + "ocs1266-{}-{}.pdf", + std::process::id(), + NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); + if let [page] = pages { + export_pdf(page, &path).expect("export bitmap PDF"); + } else { + export_pdf_pages(pages, &path, None).expect("export bitmap PDF pages"); + } + let bytes = std::fs::read(&path).unwrap(); + std::fs::remove_file(path).unwrap(); + bytes +} + +struct Raster { + pixels: Vec, + width: usize, + height: usize, + scale: f32, +} +impl Raster { + fn from_pdf(bytes: &[u8], page: usize) -> Self { + Self::at_scale(bytes, page, 2.0) + } + + fn at_scale(bytes: &[u8], page: usize, scale: f32) -> Self { + let pdf = hayro::hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap(); + let pixmap = hayro::render( + &pdf.pages()[page], + &hayro::RenderCache::new(), + &hayro::hayro_interpret::InterpreterSettings::default(), + &hayro::RenderSettings { + x_scale: scale, + y_scale: scale, + bg_color: hayro::vello_cpu::color::palette::css::WHITE, + ..Default::default() + }, + ); + Self { + pixels: pixmap.data_as_u8_slice().to_vec(), + width: u32::from(pixmap.width()) as usize, + height: u32::from(pixmap.height()) as usize, + scale, + } + } + + fn assert_color(&self, x_mm: f32, y_mm: f32, expected: [u8; 3]) { + let x = (x_mm * 72.0 / 25.4 * self.scale) as usize; + let y = self.height - 1 - (y_mm * 72.0 / 25.4 * self.scale) as usize; + let actual = &self.pixels[(y * self.width + x) * 4..][..3]; + assert!( + actual + .iter() + .zip(expected) + .all(|(&a, b)| a.abs_diff(b) < 25), + "at ({x_mm}, {y_mm}): expected {expected:?}, got {actual:?}" + ); + } +} + +fn image() -> PlotImage { + let corners = [ + [10.0, 10.0, 0.0], + [50.0, 10.0, 0.0], + [50.0, 50.0, 0.0], + [10.0, 50.0, 0.0], + ]; + let uv = [[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]]; + // Four distinct quadrants detect flips, reflections, and diagonal seams. + let pixels = (0..32) + .flat_map(|y| { + (0..32).flat_map(move |x| match (x < 16, y < 16) { + (true, true) => [255, 0, 0, 255], + (false, true) => [0, 0, 255, 255], + (true, false) => [0, 255, 0, 255], + (false, false) => [255, 255, 0, 255], + }) + }) + .collect(); + PlotImage { + image: ImageModel { + render_instance: None, + file_path: "synthetic".into(), + pixels: Arc::new(pixels), + width: 32, + height: 32, + opacity: 1.0, + corners, + corners_low: [[0.0; 3]; 4], + draw_depth: 0.5, + verts: [0, 1, 2, 0, 2, 3] + .into_iter() + .map(|i| ImageQuadVertex { + pos: corners[i], + pos_low: [0.0; 3], + uv: uv[i], + }) + .collect(), + }, + clips: Vec::new(), + } +} + +#[test] +fn embedded_ole_inside_insert_reaches_rendered_pdf() { + let scene = embedded_scene(); + let images = scene.paper_plot_images(); + assert_eq!(images.len(), 1); + assert_eq!((images[0].image.width, images[0].image.height), (64, 32)); + assert!(scene + .images + .values() + .any(|source| Arc::ptr_eq(&source.pixels, &images[0].image.pixels))); + let mut input = page(images); + input.scale = 10.0; + let raster = Raster::from_pdf(&export(&[input]), 0); + raster.assert_color(23.0, 42.0, [255, 0, 0]); + raster.assert_color(37.0, 48.0, [0, 0, 255]); + raster.assert_color(25.0, 47.5, [0, 255, 0]); + raster.assert_color(45.0, 45.0, [255, 255, 255]); +} + +#[test] +fn rotated_nonuniform_insert_preserves_pixel_orientation() { + let mut scene = embedded_scene(); + let handle = scene + .document + .entities() + .find_map(|e| match e { + EntityType::Insert(i) if i.block_name == "SyntheticImage" => Some(i.common.handle), + _ => None, + }) + .unwrap(); + let Some(EntityType::Insert(insert)) = scene.document.get_entity_mut(handle) else { + panic!() + }; + insert.insert_point = Vector3::new(60.0, 20.0, 0.0); + insert.rotation = std::f64::consts::FRAC_PI_2; + insert.set_x_scale(20.0); + insert.set_y_scale(10.0); + let raster = Raster::from_pdf(&export(&[page(scene.paper_plot_images())]), 0); + raster.assert_color(58.0, 26.0, [255, 0, 0]); + raster.assert_color(52.0, 54.0, [0, 0, 255]); + raster.assert_color(52.5, 30.0, [0, 255, 0]); + raster.assert_color(65.0, 40.0, [255, 255, 255]); +} + +#[test] +fn clipping_page_transform_and_shared_resources_survive_serialization() { + let mut first = image(); + // A visible-region triangle (IMAGE clip), plus inherited and viewport clips. + first.image.verts.truncate(3); + first + .clips + .push(vec![[15.0, 5.0], [55.0, 5.0], [55.0, 45.0], [15.0, 45.0]]); + first + .clips + .push(vec![[5.0, 15.0], [45.0, 15.0], [45.0, 55.0], [5.0, 55.0]]); + let mut second = page(vec![first.clone()]); + second.rotation_deg = 90; + second.scale = 0.5; + second.offset_x = 10.0; + second.offset_y = 10.0; + let bytes = export(&[page(vec![first]), second]); + let parsed = printpdf::PdfDocument::parse( + &bytes, + &printpdf::PdfParseOptions::default(), + &mut Vec::new(), + ) + .unwrap(); + let uses: Vec<_> = parsed + .pages + .iter() + .flat_map(|p| p.ops.iter()) + .filter_map(|op| { + if let printpdf::Op::UseXobject { id, .. } = op { + Some(id) + } else { + None + } + }) + .collect(); + assert_eq!( + uses.len(), + 2, + "one image draw per affine quad, without a diagonal seam" + ); + assert_eq!(uses[0], uses[1], "pages should share one image resource"); + let raster = Raster::from_pdf(&bytes, 0); + raster.assert_color(40.0, 20.0, [255, 255, 0]); + for (x, y) in [(12.0, 20.0), (48.0, 20.0), (20.0, 40.0), (40.0, 12.0)] { + raster.assert_color(x, y, [255, 255, 255]); + } + Raster::from_pdf(&bytes, 1).assert_color(85.0, 25.0, [255, 255, 0]); +} + +#[test] +fn image_alpha_and_plot_transparency_are_preserved() { + let mut plot = image(); + plot.image.opacity = 0.5; + // Intrinsic transparent texels remain transparent with either plot setting. + let pixels = Arc::make_mut(&mut plot.image.pixels); + for y in 16..32 { + for x in 0..16 { + pixels[(y * 32 + x) * 4 + 3] = 0; + } + } + let opaque = page(vec![plot.clone()]); + let mut transparent = page(vec![plot]); + transparent.options.transparency = true; + let bytes = export(&[opaque, transparent]); + let off = Raster::from_pdf(&bytes, 0); + off.assert_color(20.0, 40.0, [255, 0, 0]); + off.assert_color(20.0, 20.0, [255, 255, 255]); + let on = Raster::from_pdf(&bytes, 1); + on.assert_color(20.0, 40.0, [255, 128, 128]); + on.assert_color(20.0, 20.0, [255, 255, 255]); +} + +#[test] +fn depth_and_paper_model_groups_order_images_with_wipeouts_and_wires() { + let mut scene = Scene::new(); + let wipeout = Wipeout::polygonal( + &[ + Vector2::new(10.0, 30.0), + Vector2::new(30.0, 30.0), + Vector2::new(30.0, 50.0), + Vector2::new(10.0, 50.0), + ], + 0.0, + ); + scene.add_entity(EntityType::Wipeout(wipeout)); + let mut input = page(vec![image()]); + input.content.wipeouts = scene.paper_plot_wipeouts().as_ref().clone(); + assert_eq!(input.content.wipeouts.len(), 1); + input.content.wipeouts[0].draw_depth = 0.8; + let mut wire = WireModel::solid( + "over image".into(), + vec![[35.0, 15.0, 0.0], [45.0, 15.0, 0.0]], + [0.0, 0.0, 0.0, 1.0], + false, + ); + wire.line_weight_px = 8.0; + input.content.wires = Arc::new(vec![PlotWire { + wire, + draw_depth: 0.9, + }]); + let raster = Raster::from_pdf(&export(&[input]), 0); + raster.assert_color(20.0, 40.0, [255, 255, 255]); + raster.assert_color(40.0, 40.0, [0, 0, 255]); + raster.assert_color(40.0, 15.0, [0, 0, 0]); + + // First group wins by depth internally, second group still paints last. + let mut input = page(vec![image()]); + input.content.wipeouts = scene.paper_plot_wipeouts().as_ref().clone(); + input.content.wipeouts[0].draw_depth = 0.9; + input.content.group_splits = PlotGroupSplits { + wipeouts: 1, + ..Default::default() + }; + Raster::from_pdf(&export(&[input]), 0).assert_color(20.0, 40.0, [255, 0, 0]); +} + +#[test] +fn viewport_projection_clips_images_and_respects_frozen_and_nonplot_layers() { + let mut scene = embedded_scene(); + scene.set_current_layout("Model".into()); + scene.ensure_layer("Bitmap"); + let mut insert = Insert::new("SyntheticImage", Vector3::new(0.0, 0.0, 0.0)); + insert.common.layer = "Bitmap".into(); + insert.set_x_scale(20.0); + insert.set_y_scale(20.0); + scene.add_entity(EntityType::Insert(insert)); + scene.set_current_layout("Layout1".into()); + let mut viewport = Viewport::new(); + viewport.center = Vector3::new(50.0, 50.0, 0.0); + viewport.width = 20.0; + viewport.height = 20.0; + viewport.view_height = 20.0; + viewport.view_center = Vector3::new(20.0, 10.0, 0.0); + viewport.id = 2; + viewport.status.is_on = true; + let handle = scene.add_entity(EntityType::Viewport(viewport)); + let (_, _, _, images) = scene.viewport_plot_fills(); + assert_eq!(images.len(), 1); + let raster = Raster::from_pdf(&export(&[page(images)]), 0); + raster.assert_color(42.0, 43.0, [255, 0, 0]); + raster.assert_color(58.0, 57.0, [0, 0, 255]); + raster.assert_color(35.0, 45.0, [255, 255, 255]); + let layer = scene.document.layers.get("Bitmap").unwrap().handle; + let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(handle) else { + panic!() + }; + vp.frozen_layers.push(layer); + assert!(scene.viewport_plot_fills().3.is_empty()); + let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(handle) else { + panic!() + }; + vp.frozen_layers.clear(); + scene + .document + .layers + .get_mut("Bitmap") + .unwrap() + .is_plottable = false; + assert!(scene.viewport_plot_fills().3.is_empty()); +} + +#[test] +fn linked_image_uses_decoded_pixels_and_its_image_clip_without_reopening() { + use acadrust::entities::{ClipBoundary, RasterImage}; + let path = std::env::temp_dir().join(format!("ocs1266-linked-{}.png", std::process::id())); + let source = image(); + image::save_buffer(&path, &source.image.pixels, 32, 32, image::ColorType::Rgba8).unwrap(); + let mut raster = RasterImage::new( + path.to_str().unwrap(), + Vector3::new(10.0, 10.0, 0.0), + 32.0, + 32.0, + ); + raster.u_vector = Vector3::new(1.25, 0.0, 0.0); + raster.v_vector = Vector3::new(0.0, 1.25, 0.0); + raster.clipping_enabled = true; + raster.clip_boundary = + ClipBoundary::rectangular(Vector2::new(0.0, 0.0), Vector2::new(16.0, 32.0)); + let mut scene = Scene::new(); + let handle = scene.add_entity(EntityType::RasterImage(raster)); + scene.populate_images_from_document(); + std::fs::remove_file(path).unwrap(); + let images = scene.paper_plot_images(); + assert_eq!(images.len(), 1); + assert!(Arc::ptr_eq( + &images[0].image.pixels, + &scene.images[&handle].pixels + )); + let rendered = Raster::from_pdf(&export(&[page(images)]), 0); + rendered.assert_color(20.0, 40.0, [255, 0, 0]); + rendered.assert_color(20.0, 20.0, [0, 255, 0]); + rendered.assert_color(40.0, 40.0, [255, 255, 255]); +} + +#[test] +fn equal_sized_sources_remain_distinct_across_pages() { + let pages = { + let red = image(); + let mut cyan = image(); + Arc::make_mut(&mut cyan.image.pixels) + .chunks_exact_mut(4) + .for_each(|pixel| pixel.copy_from_slice(&[0, 255, 255, 255])); + assert!(!Arc::ptr_eq(&red.image.pixels, &cyan.image.pixels)); + vec![ + page(vec![red.clone()]), + page(vec![cyan.clone()]), + page(vec![red]), + page(vec![cyan]), + ] + }; + let bytes = export(&pages); + let parsed = printpdf::PdfDocument::parse( + &bytes, + &printpdf::PdfParseOptions::default(), + &mut Vec::new(), + ) + .unwrap(); + let uses: Vec<_> = parsed + .pages + .iter() + .flat_map(|page| &page.ops) + .filter_map(|op| { + if let printpdf::Op::UseXobject { id, .. } = op { + Some(id) + } else { + None + } + }) + .collect(); + assert_eq!(uses.len(), 4); + assert_ne!(uses[0], uses[1]); + assert_eq!(uses[0], uses[2]); + assert_eq!(uses[1], uses[3]); + for (index, expected) in [[255, 0, 0], [0, 255, 255], [255, 0, 0], [0, 255, 255]] + .into_iter() + .enumerate() + { + Raster::from_pdf(&bytes, index).assert_color(20.0, 40.0, expected); + } +} + +#[test] +fn affine_triangle_clip_has_no_shared_edge_seam() { + let mut plot = image(); + Arc::make_mut(&mut plot.image.pixels) + .chunks_exact_mut(4) + .for_each(|pixel| pixel.copy_from_slice(&[0, 0, 0, 128])); + let mut sheared = plot.clone(); + let transform = |[x, y, z]: [f32; 3]| [x + 0.27 * y + 5.0, 0.19 * x + y + 3.0, z]; + sheared + .image + .corners + .iter_mut() + .for_each(|p| *p = transform(*p)); + sheared + .image + .verts + .iter_mut() + .for_each(|v| v.pos = transform(v.pos)); + let pages = [page(vec![plot]), page(vec![sheared])]; + let bytes = export(&pages); + for (index, page) in pages.iter().enumerate() { + let raster = Raster::at_scale(&bytes, index, 8.0); + let corners = page.content.images[0].image.corners; + let u = [corners[1][0] - corners[0][0], corners[1][1] - corners[0][1]]; + let v = [corners[3][0] - corners[0][0], corners[3][1] - corners[0][1]]; + let determinant = u[0] * v[1] - u[1] * v[0]; + let mut checked = 0; + for y in 0..raster.height { + for x in 0..raster.width { + let dx = (x as f32 + 0.5) / raster.scale * 25.4 / 72.0 - corners[0][0]; + let dy = (raster.height as f32 - y as f32 - 0.5) / raster.scale * 25.4 / 72.0 + - corners[0][1]; + let a = (dx * v[1] - dy * v[0]) / determinant; + let b = (u[0] * dy - u[1] * dx) / determinant; + if (0.15..0.85).contains(&a) && (0.15..0.85).contains(&b) { + let rgb = &raster.pixels[(y * raster.width + x) * 4..][..3]; + assert!(rgb.iter().all(|&c| c.abs_diff(127) <= 2), + "page {index}, pixel ({x}, {y}): shared-edge gap or double coverage: {rgb:?}"); + checked += 1; + } + } + } + assert!(checked > 100_000); + } +} + +#[test] +fn malformed_bitmap_reports_an_error_without_replacing_the_pdf() { + let path = std::env::temp_dir().join(format!("ocs1266-invalid-{}.pdf", std::process::id())); + std::fs::write(&path, b"existing PDF").unwrap(); + let mut pixels = image(); + pixels.image.width += 1; + let mut coordinates = image(); + coordinates.image.corners[0][0] = f32::NAN; + let mut mapping = image(); + mapping.image.corners[2][0] += 10.0; + mapping + .image + .verts + .iter_mut() + .for_each(|vertex| vertex.uv = [0.0, 0.0]); + for (plot, reason) in [ + (pixels, "dimensions"), + (coordinates, "coordinates"), + (mapping, "texture mapping"), + ] { + let error = export_pdf(&page(vec![plot]), &path).unwrap_err(); + assert!( + error.contains("Page 1") && error.contains(reason), + "{error}" + ); + assert_eq!(std::fs::read(&path).unwrap(), b"existing PDF"); + } + std::fs::remove_file(path).unwrap(); +} diff --git a/tests/pdf_export_text_check.rs b/tests/pdf_export_text_check.rs index c15db1277..1efd0825a 100644 --- a/tests/pdf_export_text_check.rs +++ b/tests/pdf_export_text_check.rs @@ -11,7 +11,9 @@ use acadrust::entities::{Dimension, DimensionLinear, Text}; use acadrust::types::Vector3; use acadrust::EntityType; -use OpenCADStudio::io::pdf_export::{export_pdf, PdfPlotOptions, PlotWire}; +use OpenCADStudio::io::pdf_export::{ + export_pdf, PdfPageInput, PdfPlotOptions, PlotContent, PlotWire, +}; use OpenCADStudio::scene::Scene; #[test] @@ -60,49 +62,38 @@ fn text_and_dim_reach_pdf_export() { .collect(); let p_text = dir.join("with_text.pdf"); - export_pdf( - &plot_wires, - &[], - &[], - 210.0, - 297.0, - 0.0, - 0.0, - 0, - 1.0, - None, - &p_text, - None, - PdfPlotOptions::default(), - ) - .expect("export with text"); + let mut page = PdfPageInput { + content: PlotContent { + wires: std::sync::Arc::new(plot_wires), + ..Default::default() + }, + paper_w: 210.0, + paper_h: 297.0, + offset_x: 0.0, + offset_y: 0.0, + rotation_deg: 0, + scale: 1.0, + clip: None, + options: PdfPlotOptions::default(), + plot_style: None, + }; + export_pdf(&page, &p_text).expect("export with text"); let with_text = std::fs::read(&p_text).expect("read pdf"); assert!(with_text.starts_with(b"%PDF"), "not a PDF"); - let stripped: Vec<_> = plot_wires - .into_iter() + let stripped: Vec<_> = page + .content + .wires + .iter() + .cloned() .map(|mut w| { w.wire.text_verts.clear(); w }) .collect(); let p_bare = dir.join("no_text.pdf"); - export_pdf( - &stripped, - &[], - &[], - 210.0, - 297.0, - 0.0, - 0.0, - 0, - 1.0, - None, - &p_bare, - None, - PdfPlotOptions::default(), - ) - .expect("export without text"); + page.content.wires = std::sync::Arc::new(stripped); + export_pdf(&page, &p_bare).expect("export without text"); let no_text = std::fs::read(&p_bare).expect("read pdf"); assert!(