feat(print): Enhanced Print Target Settings & Auto-Fitting Scaler (#34) #35
@@ -13,7 +13,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build Windows
|
name: Build Windows
|
||||||
runs-on: windows-latest
|
runs-on: windows
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -26,17 +26,25 @@ jobs:
|
|||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
shell: powershell
|
||||||
with:
|
run: |
|
||||||
toolchain: stable
|
if (!(Get-Command rustup -ErrorAction SilentlyContinue) -and !(Test-Path "$env:USERPROFILE\.cargo\bin\rustup.exe")) {
|
||||||
targets: x86_64-pc-windows-msvc
|
Write-Host "Downloading rustup-init.exe..."
|
||||||
|
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile "$env:TEMP\rustup-init.exe"
|
||||||
|
& "$env:TEMP\rustup-init.exe" -y --default-toolchain stable --profile minimal
|
||||||
|
}
|
||||||
|
"$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
rustup default stable
|
||||||
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
workspaces: "src-tauri -> target"
|
workspaces: "src-tauri -> target"
|
||||||
|
|
||||||
- name: Check sidecar binaries
|
- name: Check sidecar binaries
|
||||||
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
if (!(Test-Path "src-tauri\argyll\windows-x86_64\argyll-instlist.exe")) {
|
if (!(Test-Path "src-tauri\argyll\windows-x86_64\argyll-instlist.exe")) {
|
||||||
Write-Host "Warning: Windows ArgyllCMS binaries not found. Please ensure they are downloaded/committed to src-tauri/argyll/windows-x86_64 before release."
|
Write-Host "Warning: Windows ArgyllCMS binaries not found. Please ensure they are downloaded/committed to src-tauri/argyll/windows-x86_64 before release."
|
||||||
@@ -47,14 +55,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Get App Version
|
- name: Get App Version
|
||||||
id: get_version
|
id: get_version
|
||||||
shell: bash
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(node -p "require('./package.json').version")
|
$VERSION = node -p "require('./package.json').version"
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
if ($env:GITHUB_REF -eq "refs/heads/main") {
|
||||||
echo "APP_VERSION=${VERSION}" >> $GITHUB_ENV
|
"APP_VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
else
|
} else {
|
||||||
echo "APP_VERSION=${VERSION}-dev" >> $GITHUB_ENV
|
"APP_VERSION=$VERSION-dev" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
fi
|
}
|
||||||
|
|
||||||
- name: Build Tauri App
|
- name: Build Tauri App
|
||||||
run: npm run tauri build -- -v
|
run: npm run tauri build -- -v
|
||||||
|
|||||||
+112
-35
@@ -109,16 +109,22 @@ pub struct TargenConfig {
|
|||||||
pub patch_count: u32,
|
pub patch_count: u32,
|
||||||
pub white_patches: Option<u32>,
|
pub white_patches: Option<u32>,
|
||||||
pub black_patches: Option<u32>,
|
pub black_patches: Option<u32>,
|
||||||
|
pub total_patches: Option<u32>,
|
||||||
pub basename: String,
|
pub basename: String,
|
||||||
pub cwd: String,
|
pub cwd: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_dpi() -> u32 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct PrinttargConfig {
|
pub struct PrinttargConfig {
|
||||||
pub instrument: String, // One of: "i1", "p3", "CM", "SS", "20", "22", "41", "51"
|
pub instrument: String, // One of: "i1", "p3", "CM", "SS", "20", "22", "41", "51"
|
||||||
pub page_size: String, // One of: "A4", "A4R", "A3", "A2", "Letter", "LetterR", "Legal", "4x6", "11x17", or "WWWxHHH"
|
pub page_size: String, // One of: "A4", "A4R", "A3", "A2", "Letter", "LetterR", "Legal", "4x6", "11x17", or "WWWxHHH"
|
||||||
pub bit_depth: u8, // 8 or 16
|
pub bit_depth: u8, // 8 or 16
|
||||||
pub dpi: u32, // TIFF resolution, e.g. 100, 200, 300
|
#[serde(default = "default_dpi")]
|
||||||
|
pub dpi: u32, // TIFF resolution, defaults to 300 DPI
|
||||||
pub basename: String, // Must match the .ti1 basename from Stage 1
|
pub basename: String, // Must match the .ti1 basename from Stage 1
|
||||||
pub cwd: String, // Working directory where the .ti1 file resides
|
pub cwd: String, // Working directory where the .ti1 file resides
|
||||||
}
|
}
|
||||||
@@ -135,8 +141,10 @@ pub fn build_targen_args(config: &TargenConfig) -> Vec<String> {
|
|||||||
args.push("2".to_string()); // Default to RGB
|
args.push("2".to_string()); // Default to RGB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(patches) = config.total_patches {
|
||||||
args.push("-f".to_string());
|
args.push("-f".to_string());
|
||||||
args.push(config.patch_count.to_string());
|
args.push(patches.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(white) = config.white_patches {
|
if let Some(white) = config.white_patches {
|
||||||
args.push("-e".to_string());
|
args.push("-e".to_string());
|
||||||
@@ -256,34 +264,48 @@ pub async fn run_chartread(
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct ColprofConfig {
|
pub struct ColprofConfig {
|
||||||
pub quality: String,
|
|
||||||
pub algorithm: String,
|
pub algorithm: String,
|
||||||
pub description: String,
|
pub quality: String,
|
||||||
|
pub intent: Option<String>,
|
||||||
pub copyright: Option<String>,
|
pub copyright: Option<String>,
|
||||||
pub basename: String,
|
pub description: Option<String>,
|
||||||
pub cwd: String,
|
pub cwd: String,
|
||||||
|
pub ti3_path: String,
|
||||||
|
pub icc_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_colprof_args(config: &ColprofConfig) -> Vec<String> {
|
pub fn build_colprof_args(config: &ColprofConfig) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"-v".to_string(),
|
"-v".to_string(),
|
||||||
"-u".to_string(),
|
|
||||||
"-q".to_string(),
|
|
||||||
config.quality.clone(),
|
|
||||||
"-a".to_string(),
|
"-a".to_string(),
|
||||||
config.algorithm.clone(),
|
config.algorithm.clone(),
|
||||||
"-D".to_string(),
|
"-q".to_string(),
|
||||||
config.description.clone(),
|
config.quality.clone(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(copyright) = &config.copyright {
|
if let Some(ref intent) = config.intent {
|
||||||
if !copyright.trim().is_empty() {
|
if !intent.trim().is_empty() {
|
||||||
args.push("-C".to_string());
|
args.push("-p".to_string());
|
||||||
args.push(copyright.clone());
|
args.push(intent.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push(config.basename.clone());
|
if let Some(ref cr) = config.copyright {
|
||||||
|
if !cr.trim().is_empty() {
|
||||||
|
args.push("-C".to_string());
|
||||||
|
args.push(cr.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref desc) = config.description {
|
||||||
|
if !desc.trim().is_empty() {
|
||||||
|
args.push("-D".to_string());
|
||||||
|
args.push(desc.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(config.ti3_path.clone());
|
||||||
|
args.push(config.icc_path.clone());
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +317,7 @@ pub async fn run_colprof(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let binary = resolve_binary(app.clone(), "colprof".to_string()).await?;
|
let binary = resolve_binary(app.clone(), "colprof".to_string()).await?;
|
||||||
let args = build_colprof_args(&config);
|
let args = build_colprof_args(&config);
|
||||||
let id = format!("colprof_{}", config.basename);
|
let id = format!("colprof_{}", config.icc_path);
|
||||||
|
|
||||||
let cwd = if config.cwd.trim().is_empty() {
|
let cwd = if config.cwd.trim().is_empty() {
|
||||||
None
|
None
|
||||||
@@ -317,7 +339,7 @@ pub fn build_profcheck_args(config: &ProfcheckConfig) -> Vec<String> {
|
|||||||
vec![
|
vec![
|
||||||
"-v".to_string(),
|
"-v".to_string(),
|
||||||
"-k".to_string(),
|
"-k".to_string(),
|
||||||
"-u".to_string(),
|
"-s".to_string(),
|
||||||
config.ti3_path.clone(),
|
config.ti3_path.clone(),
|
||||||
config.icc_path.clone(),
|
config.icc_path.clone(),
|
||||||
]
|
]
|
||||||
@@ -355,14 +377,19 @@ pub async fn get_windows_printers() -> Result<Vec<String>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn print_target_windows(printer_name: String, tiff_path: String) -> Result<(), String> {
|
pub async fn print_target_windows(
|
||||||
|
state: State<'_, crate::print::PrinterDevModeStore>,
|
||||||
|
printer_name: String,
|
||||||
|
tiff_path: String,
|
||||||
|
options: Option<crate::print::PrintOptions>,
|
||||||
|
) -> Result<(), String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
crate::print::windows::print_target(&printer_name, &tiff_path)
|
crate::print::windows::print_target(&printer_name, &tiff_path, options.as_ref(), Some(&state))
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
let _ = (printer_name, tiff_path);
|
let _ = (state, printer_name, tiff_path, options);
|
||||||
Err("Windows native printing is only supported on Windows.".to_string())
|
Err("Windows native printing is only supported on Windows.".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,19 +410,19 @@ pub async fn get_cups_printers() -> Result<Vec<crate::print::Printer>, String> {
|
|||||||
pub async fn print_target_cups(
|
pub async fn print_target_cups(
|
||||||
printer_name: String,
|
printer_name: String,
|
||||||
tiff_path: String,
|
tiff_path: String,
|
||||||
ppd_uncorrected_passthrough: Option<bool>,
|
options: Option<crate::print::PrintOptions>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
crate::print::unix::print_target(
|
crate::print::unix::print_target(
|
||||||
&printer_name,
|
&printer_name,
|
||||||
&tiff_path,
|
&tiff_path,
|
||||||
ppd_uncorrected_passthrough.unwrap_or(false),
|
options.as_ref(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
let _ = (printer_name, tiff_path, ppd_uncorrected_passthrough);
|
let _ = (printer_name, tiff_path, options);
|
||||||
Err("CUPS printing is only supported on macOS and Linux.".to_string())
|
Err("CUPS printing is only supported on macOS and Linux.".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,27 +444,73 @@ pub async fn get_printers() -> Result<Vec<crate::print::Printer>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn print_target_native(
|
pub async fn get_printer_capabilities(
|
||||||
printer_name: String,
|
printer_name: String,
|
||||||
tiff_path: String,
|
) -> Result<crate::print::PrinterCapabilities, String> {
|
||||||
ppd_uncorrected_passthrough: Option<bool>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let _ = ppd_uncorrected_passthrough;
|
crate::print::windows::get_printer_capabilities(&printer_name)
|
||||||
crate::print::windows::print_target(&printer_name, &tiff_path)
|
|
||||||
}
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
crate::print::unix::get_printer_capabilities(&printer_name)
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = printer_name;
|
||||||
|
Err("Printer capabilities query is not supported on this platform.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn show_printer_properties(
|
||||||
|
state: State<'_, crate::print::PrinterDevModeStore>,
|
||||||
|
printer_name: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
crate::print::windows::show_printer_properties(&printer_name, &state)
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = (state, printer_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = (state, printer_name);
|
||||||
|
Err("Printer properties dialog is not supported on this platform.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn print_target_native(
|
||||||
|
state: State<'_, crate::print::PrinterDevModeStore>,
|
||||||
|
printer_name: String,
|
||||||
|
tiff_path: String,
|
||||||
|
options: Option<crate::print::PrintOptions>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
crate::print::windows::print_target(
|
||||||
|
&printer_name,
|
||||||
|
&tiff_path,
|
||||||
|
options.as_ref(),
|
||||||
|
Some(&state),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = state;
|
||||||
crate::print::unix::print_target(
|
crate::print::unix::print_target(
|
||||||
&printer_name,
|
&printer_name,
|
||||||
&tiff_path,
|
&tiff_path,
|
||||||
ppd_uncorrected_passthrough.unwrap_or(false),
|
options.as_ref(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[cfg(not(any(windows, unix)))]
|
#[cfg(not(any(windows, unix)))]
|
||||||
{
|
{
|
||||||
let _ = (printer_name, tiff_path, ppd_uncorrected_passthrough);
|
let _ = (state, printer_name, tiff_path, options);
|
||||||
Err("Native raw printing is not supported on this platform.".to_string())
|
Err("Native raw printing is not supported on this platform.".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,6 +526,7 @@ mod tests {
|
|||||||
patch_count: 800,
|
patch_count: 800,
|
||||||
white_patches: Some(4),
|
white_patches: Some(4),
|
||||||
black_patches: None,
|
black_patches: None,
|
||||||
|
total_patches: Some(800),
|
||||||
basename: "my_profile".to_string(),
|
basename: "my_profile".to_string(),
|
||||||
cwd: "/tmp".to_string(),
|
cwd: "/tmp".to_string(),
|
||||||
};
|
};
|
||||||
@@ -467,6 +541,7 @@ mod tests {
|
|||||||
patch_count: 1500,
|
patch_count: 1500,
|
||||||
white_patches: None,
|
white_patches: None,
|
||||||
black_patches: Some(8),
|
black_patches: Some(8),
|
||||||
|
total_patches: Some(1500),
|
||||||
basename: "cmyk_profile".to_string(),
|
basename: "cmyk_profile".to_string(),
|
||||||
cwd: "/tmp".to_string(),
|
cwd: "/tmp".to_string(),
|
||||||
};
|
};
|
||||||
@@ -531,15 +606,17 @@ mod tests {
|
|||||||
let config = ColprofConfig {
|
let config = ColprofConfig {
|
||||||
quality: "h".to_string(),
|
quality: "h".to_string(),
|
||||||
algorithm: "l".to_string(),
|
algorithm: "l".to_string(),
|
||||||
description: "My Profile".to_string(),
|
intent: None,
|
||||||
|
description: Some("My Profile".to_string()),
|
||||||
copyright: Some("2026 ACME".to_string()),
|
copyright: Some("2026 ACME".to_string()),
|
||||||
basename: "my_profile".to_string(),
|
ti3_path: "my_profile.ti3".to_string(),
|
||||||
|
icc_path: "my_profile.icc".to_string(),
|
||||||
cwd: "/home/user".to_string(),
|
cwd: "/home/user".to_string(),
|
||||||
};
|
};
|
||||||
let args = build_colprof_args(&config);
|
let args = build_colprof_args(&config);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args,
|
args,
|
||||||
vec!["-v", "-u", "-q", "h", "-a", "l", "-D", "My Profile", "-C", "2026 ACME", "my_profile"]
|
vec!["-v", "-a", "l", "-q", "h", "-C", "2026 ACME", "-D", "My Profile", "my_profile.ti3", "my_profile.icc"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +628,6 @@ mod tests {
|
|||||||
cwd: "/home/user".to_string(),
|
cwd: "/home/user".to_string(),
|
||||||
};
|
};
|
||||||
let args = build_profcheck_args(&config);
|
let args = build_profcheck_args(&config);
|
||||||
assert_eq!(args, vec!["-v", "-k", "-u", "my_profile.ti3", "my_profile.icc"]);
|
assert_eq!(args, vec!["-v", "-k", "-s", "my_profile.ti3", "my_profile.icc"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.manage(process_manager::ProcessManager::new())
|
.manage(process_manager::ProcessManager::new())
|
||||||
|
.manage(print::PrinterDevModeStore::new())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::spawn_process,
|
commands::spawn_process,
|
||||||
commands::get_app_info,
|
commands::get_app_info,
|
||||||
@@ -29,6 +30,8 @@ pub fn run() {
|
|||||||
commands::get_cups_printers,
|
commands::get_cups_printers,
|
||||||
commands::print_target_cups,
|
commands::print_target_cups,
|
||||||
commands::get_printers,
|
commands::get_printers,
|
||||||
|
commands::get_printer_capabilities,
|
||||||
|
commands::show_printer_properties,
|
||||||
commands::print_target_native,
|
commands::print_target_native,
|
||||||
settings::load_settings,
|
settings::load_settings,
|
||||||
settings::save_settings,
|
settings::save_settings,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Printer {
|
pub struct Printer {
|
||||||
@@ -7,12 +9,62 @@ pub struct Printer {
|
|||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PrinterTray {
|
||||||
|
pub id: u16,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PrinterPaperSize {
|
||||||
|
pub id: u16,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PrinterCapabilities {
|
||||||
|
pub trays: Vec<PrinterTray>,
|
||||||
|
pub paper_sizes: Vec<PrinterPaperSize>,
|
||||||
|
pub supports_orientation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
|
pub struct PrintOptions {
|
||||||
|
pub paper_source: Option<u16>,
|
||||||
|
pub orientation: Option<String>,
|
||||||
|
pub paper_size: Option<String>,
|
||||||
|
pub ppd_uncorrected_passthrough: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct PrinterDevModeStore {
|
||||||
|
pub devmodes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrinterDevModeStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
devmodes: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, printer_name: &str) -> Option<Vec<u8>> {
|
||||||
|
let map = self.devmodes.lock().ok()?;
|
||||||
|
map.get(printer_name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, printer_name: &str, devmode: Vec<u8>) {
|
||||||
|
if let Ok(mut map) = self.devmodes.lock() {
|
||||||
|
map.insert(printer_name.to_string(), devmode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(any(unix, test))]
|
||||||
pub mod unix;
|
pub mod unix;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_tests {
|
mod integration_tests {
|
||||||
use crate::print::Printer;
|
use crate::print::{
|
||||||
|
PrintOptions, Printer, PrinterCapabilities, PrinterDevModeStore, PrinterPaperSize,
|
||||||
|
PrinterTray,
|
||||||
|
};
|
||||||
use crate::print::unix::{build_lp_args, parse_lpstat_d, parse_lpstat_e, parse_lpstat_p, print_target};
|
use crate::print::unix::{build_lp_args, parse_lpstat_d, parse_lpstat_e, parse_lpstat_p, print_target};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -20,6 +23,98 @@ mod integration_tests {
|
|||||||
assert_eq!(deserialized, printer);
|
assert_eq!(deserialized, printer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_printer_capabilities_serialization() {
|
||||||
|
let caps = PrinterCapabilities {
|
||||||
|
trays: vec![
|
||||||
|
PrinterTray {
|
||||||
|
id: 1,
|
||||||
|
name: "Auto Sheet Feeder".to_string(),
|
||||||
|
},
|
||||||
|
PrinterTray {
|
||||||
|
id: 2,
|
||||||
|
name: "Rear Manual Feed".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paper_sizes: vec![
|
||||||
|
PrinterPaperSize {
|
||||||
|
id: 9,
|
||||||
|
name: "A4 (210 x 297 mm)".to_string(),
|
||||||
|
},
|
||||||
|
PrinterPaperSize {
|
||||||
|
id: 1,
|
||||||
|
name: "Letter (8.5 x 11 in)".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
supports_orientation: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&caps).expect("Failed to serialize capabilities");
|
||||||
|
assert!(json.contains("Auto Sheet Feeder"));
|
||||||
|
assert!(json.contains("Rear Manual Feed"));
|
||||||
|
assert!(json.contains("A4 (210 x 297 mm)"));
|
||||||
|
|
||||||
|
let deserialized: PrinterCapabilities =
|
||||||
|
serde_json::from_str(&json).expect("Failed to deserialize capabilities");
|
||||||
|
assert_eq!(deserialized, caps);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_print_options_serialization() {
|
||||||
|
let opts = PrintOptions {
|
||||||
|
paper_source: Some(2),
|
||||||
|
orientation: Some("landscape".to_string()),
|
||||||
|
paper_size: Some("A4".to_string()),
|
||||||
|
ppd_uncorrected_passthrough: Some(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&opts).expect("Failed to serialize PrintOptions");
|
||||||
|
let deserialized: PrintOptions =
|
||||||
|
serde_json::from_str(&json).expect("Failed to deserialize PrintOptions");
|
||||||
|
assert_eq!(deserialized.paper_source, Some(2));
|
||||||
|
assert_eq!(deserialized.orientation.as_deref(), Some("landscape"));
|
||||||
|
assert_eq!(deserialized.paper_size.as_deref(), Some("A4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_devmode_store_concurrency() {
|
||||||
|
let store = PrinterDevModeStore::new();
|
||||||
|
assert!(store.get("Epson-P900").is_none());
|
||||||
|
|
||||||
|
let fake_devmode = vec![1, 2, 3, 4, 5];
|
||||||
|
store.set("Epson-P900", fake_devmode.clone());
|
||||||
|
assert_eq!(store.get("Epson-P900"), Some(fake_devmode));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auto_scaler_proportional_fit_math() {
|
||||||
|
// Page printable area: 4800 x 6800 device units
|
||||||
|
let page_w = 4800f64;
|
||||||
|
let page_h = 6800f64;
|
||||||
|
|
||||||
|
// Image: 2400 x 3000 (aspect ratio 0.8)
|
||||||
|
let img_w = 2400f64;
|
||||||
|
let img_h = 3000f64;
|
||||||
|
|
||||||
|
let scale_x = page_w / img_w; // 2.0
|
||||||
|
let scale_y = page_h / img_h; // 2.2666
|
||||||
|
let scale = scale_x.min(scale_y); // 2.0
|
||||||
|
|
||||||
|
let dest_w = (img_w * scale).floor() as i32;
|
||||||
|
let dest_h = (img_h * scale).floor() as i32;
|
||||||
|
|
||||||
|
assert_eq!(dest_w, 4800);
|
||||||
|
assert_eq!(dest_h, 6000);
|
||||||
|
assert!(dest_w <= page_w as i32);
|
||||||
|
assert!(dest_h <= page_h as i32);
|
||||||
|
|
||||||
|
let dest_x = (page_w as i32 - dest_w) / 2;
|
||||||
|
let dest_y = (page_h as i32 - dest_h) / 2;
|
||||||
|
|
||||||
|
assert_eq!(dest_x, 0);
|
||||||
|
assert_eq!(dest_y, 400); // Vertically centered
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lpstat_e_multiline_and_whitespace_handling() {
|
fn test_lpstat_e_multiline_and_whitespace_handling() {
|
||||||
let sample = " \n Epson_SureColor_P900 \n\n Canon_imagePROGRAF_PRO_1000 \n\n";
|
let sample = " \n Epson_SureColor_P900 \n\n Canon_imagePROGRAF_PRO_1000 \n\n";
|
||||||
@@ -62,7 +157,7 @@ printer Custom_Queue unknown state\n\
|
|||||||
|
|
||||||
for (_idx, page) in pages.iter().enumerate() {
|
for (_idx, page) in pages.iter().enumerate() {
|
||||||
let path = format!("/tmp/profiling_run/{}", page);
|
let path = format!("/tmp/profiling_run/{}", page);
|
||||||
let args_raw = build_lp_args(printer, &path, false);
|
let args_raw = build_lp_args(printer, &path, None);
|
||||||
assert_eq!(args_raw[0], "-d");
|
assert_eq!(args_raw[0], "-d");
|
||||||
assert_eq!(args_raw[1], printer);
|
assert_eq!(args_raw[1], printer);
|
||||||
assert_eq!(args_raw[2], "-t");
|
assert_eq!(args_raw[2], "-t");
|
||||||
@@ -71,7 +166,11 @@ printer Custom_Queue unknown state\n\
|
|||||||
assert_eq!(args_raw[5], "raw");
|
assert_eq!(args_raw[5], "raw");
|
||||||
assert_eq!(args_raw[6], path);
|
assert_eq!(args_raw[6], path);
|
||||||
|
|
||||||
let args_fallback = build_lp_args(printer, &path, true);
|
let opts = PrintOptions {
|
||||||
|
ppd_uncorrected_passthrough: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let args_fallback = build_lp_args(printer, &path, Some(&opts));
|
||||||
assert_eq!(args_fallback[4], "-o");
|
assert_eq!(args_fallback[4], "-o");
|
||||||
assert_eq!(args_fallback[5], "ColorModel=Gray");
|
assert_eq!(args_fallback[5], "ColorModel=Gray");
|
||||||
assert_eq!(args_fallback[6], "-o");
|
assert_eq!(args_fallback[6], "-o");
|
||||||
@@ -82,9 +181,10 @@ printer Custom_Queue unknown state\n\
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_print_target_nonexistent_file_handling() {
|
fn test_print_target_nonexistent_file_handling() {
|
||||||
let res = print_target("Test_Printer", "/non/existent/path/target_page_99.tif", false);
|
let res = print_target("Test_Printer", "/non/existent/path/target_page_99.tif", None);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let err = res.unwrap_err();
|
let err = res.unwrap_err();
|
||||||
assert!(err.contains("Target TIFF file not found"));
|
assert!(err.contains("Target TIFF file not found"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+143
-12
@@ -1,5 +1,9 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::print::Printer;
|
use crate::print::{
|
||||||
|
PrintOptions, Printer, PrinterCapabilities, PrinterPaperSize, PrinterTray,
|
||||||
|
};
|
||||||
|
|
||||||
/// Parse `lpstat -e` output into a list of printer destination names.
|
/// Parse `lpstat -e` output into a list of printer destination names.
|
||||||
pub fn parse_lpstat_e(output: &str) -> Vec<String> {
|
pub fn parse_lpstat_e(output: &str) -> Vec<String> {
|
||||||
@@ -94,7 +98,6 @@ pub fn merge_printer_info(
|
|||||||
|
|
||||||
/// Query available CUPS printers, connection statuses, and default destination.
|
/// Query available CUPS printers, connection statuses, and default destination.
|
||||||
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
||||||
// 1. Run lpstat -e
|
|
||||||
let e_output = std::process::Command::new("lpstat")
|
let e_output = std::process::Command::new("lpstat")
|
||||||
.arg("-e")
|
.arg("-e")
|
||||||
.output();
|
.output();
|
||||||
@@ -105,7 +108,6 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Run lpstat -p
|
|
||||||
let p_output = std::process::Command::new("lpstat")
|
let p_output = std::process::Command::new("lpstat")
|
||||||
.arg("-p")
|
.arg("-p")
|
||||||
.output();
|
.output();
|
||||||
@@ -121,7 +123,6 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Run lpstat -d
|
|
||||||
let d_output = std::process::Command::new("lpstat")
|
let d_output = std::process::Command::new("lpstat")
|
||||||
.arg("-d")
|
.arg("-d")
|
||||||
.output();
|
.output();
|
||||||
@@ -133,11 +134,69 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref()))
|
Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct `lp` command line arguments for target printing.
|
/// Parse `lpoptions -p <printer> -l` output into trays and paper sizes.
|
||||||
|
pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSize>) {
|
||||||
|
let mut trays = Vec::new();
|
||||||
|
let mut paper_sizes = Vec::new();
|
||||||
|
|
||||||
|
for line in output.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((key_part, values_part)) = trimmed.split_once(':') {
|
||||||
|
let key_name = key_part.split('/').next().unwrap_or("").trim();
|
||||||
|
let values = values_part.split_whitespace();
|
||||||
|
|
||||||
|
if key_name.eq_ignore_ascii_case("InputSlot") || key_name.eq_ignore_ascii_case("MediaSource") {
|
||||||
|
for (idx, val) in values.enumerate() {
|
||||||
|
let clean_val = val.trim_start_matches('*');
|
||||||
|
trays.push(PrinterTray {
|
||||||
|
id: (idx + 1) as u16,
|
||||||
|
name: clean_val.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if key_name.eq_ignore_ascii_case("PageSize") || key_name.eq_ignore_ascii_case("MediaSize") {
|
||||||
|
for (idx, val) in values.enumerate() {
|
||||||
|
let clean_val = val.trim_start_matches('*');
|
||||||
|
paper_sizes.push(PrinterPaperSize {
|
||||||
|
id: (idx + 1) as u16,
|
||||||
|
name: clean_val.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(trays, paper_sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query CUPS printer capabilities via `lpoptions -p <printer> -l`.
|
||||||
|
pub fn get_printer_capabilities(printer_name: &str) -> Result<PrinterCapabilities, String> {
|
||||||
|
let output = std::process::Command::new("lpoptions")
|
||||||
|
.args(["-p", printer_name, "-l"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let (trays, paper_sizes) = match output {
|
||||||
|
Ok(out) if out.status.success() => {
|
||||||
|
parse_lpoptions_l(&String::from_utf8_lossy(&out.stdout))
|
||||||
|
}
|
||||||
|
_ => (Vec::new(), Vec::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PrinterCapabilities {
|
||||||
|
trays,
|
||||||
|
paper_sizes,
|
||||||
|
supports_orientation: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct `lp` command line arguments for target printing with options.
|
||||||
pub fn build_lp_args(
|
pub fn build_lp_args(
|
||||||
printer_name: &str,
|
printer_name: &str,
|
||||||
tiff_path: &str,
|
tiff_path: &str,
|
||||||
ppd_uncorrected_passthrough: bool,
|
options: Option<&PrintOptions>,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let path = Path::new(tiff_path);
|
let path = Path::new(tiff_path);
|
||||||
let title = format!(
|
let title = format!(
|
||||||
@@ -152,7 +211,11 @@ pub fn build_lp_args(
|
|||||||
title,
|
title,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ppd_uncorrected_passthrough {
|
let ppd_fallback = options
|
||||||
|
.and_then(|o| o.ppd_uncorrected_passthrough)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if ppd_fallback {
|
||||||
args.push("-o".to_string());
|
args.push("-o".to_string());
|
||||||
args.push("ColorModel=Gray".to_string());
|
args.push("ColorModel=Gray".to_string());
|
||||||
args.push("-o".to_string());
|
args.push("-o".to_string());
|
||||||
@@ -162,6 +225,24 @@ pub fn build_lp_args(
|
|||||||
args.push("raw".to_string());
|
args.push("raw".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(opts) = options {
|
||||||
|
if let Some(ref orient) = opts.orientation {
|
||||||
|
args.push("-o".to_string());
|
||||||
|
if orient.eq_ignore_ascii_case("landscape") {
|
||||||
|
args.push("orientation-requested=4".to_string());
|
||||||
|
} else {
|
||||||
|
args.push("orientation-requested=3".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref page_size) = opts.paper_size {
|
||||||
|
if !page_size.trim().is_empty() {
|
||||||
|
args.push("-o".to_string());
|
||||||
|
args.push(format!("PageSize={}", page_size.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args.push(tiff_path.to_string());
|
args.push(tiff_path.to_string());
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
@@ -170,14 +251,14 @@ pub fn build_lp_args(
|
|||||||
pub fn print_target(
|
pub fn print_target(
|
||||||
printer_name: &str,
|
printer_name: &str,
|
||||||
tiff_path: &str,
|
tiff_path: &str,
|
||||||
ppd_uncorrected_passthrough: bool,
|
options: Option<&PrintOptions>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let path = Path::new(tiff_path);
|
let path = Path::new(tiff_path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(format!("Target TIFF file not found: {}", tiff_path));
|
return Err(format!("Target TIFF file not found: {}", tiff_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = build_lp_args(printer_name, tiff_path, ppd_uncorrected_passthrough);
|
let args = build_lp_args(printer_name, tiff_path, options);
|
||||||
|
|
||||||
let output = std::process::Command::new("lp")
|
let output = std::process::Command::new("lp")
|
||||||
.args(&args)
|
.args(&args)
|
||||||
@@ -200,6 +281,7 @@ pub fn print_target(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -295,9 +377,27 @@ printer Zebra_Label disabled since Wed 10 Jun 2026 - reason: out of ribbon\n\
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_lpoptions_l() {
|
||||||
|
let sample = "\
|
||||||
|
PageSize/Media Size: *A4 Letter Legal A3\n\
|
||||||
|
InputSlot/Media Source: *Auto Upper Lower Rear Manual\n\
|
||||||
|
Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\
|
||||||
|
";
|
||||||
|
let (trays, paper_sizes) = parse_lpoptions_l(sample);
|
||||||
|
assert_eq!(trays.len(), 5);
|
||||||
|
assert_eq!(trays[0].name, "Auto");
|
||||||
|
assert_eq!(trays[1].name, "Upper");
|
||||||
|
assert_eq!(trays[4].name, "Manual");
|
||||||
|
|
||||||
|
assert_eq!(paper_sizes.len(), 4);
|
||||||
|
assert_eq!(paper_sizes[0].name, "A4");
|
||||||
|
assert_eq!(paper_sizes[1].name, "Letter");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_lp_args_raw() {
|
fn test_build_lp_args_raw() {
|
||||||
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", false);
|
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args,
|
args,
|
||||||
vec![
|
vec![
|
||||||
@@ -314,7 +414,11 @@ printer Zebra_Label disabled since Wed 10 Jun 2026 - reason: out of ribbon\n\
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_lp_args_ppd_fallback() {
|
fn test_build_lp_args_ppd_fallback() {
|
||||||
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", true);
|
let opts = PrintOptions {
|
||||||
|
ppd_uncorrected_passthrough: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args,
|
args,
|
||||||
vec![
|
vec![
|
||||||
@@ -331,9 +435,36 @@ printer Zebra_Label disabled since Wed 10 Jun 2026 - reason: out of ribbon\n\
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_lp_args_with_orientation_and_size() {
|
||||||
|
let opts = PrintOptions {
|
||||||
|
orientation: Some("landscape".to_string()),
|
||||||
|
paper_size: Some("A4".to_string()),
|
||||||
|
ppd_uncorrected_passthrough: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts));
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
"-d",
|
||||||
|
"Epson-Stylus-SX420W",
|
||||||
|
"-t",
|
||||||
|
"ICCery Target - target.tif",
|
||||||
|
"-o",
|
||||||
|
"raw",
|
||||||
|
"-o",
|
||||||
|
"orientation-requested=4",
|
||||||
|
"-o",
|
||||||
|
"PageSize=A4",
|
||||||
|
"/tmp/target.tif"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_print_target_file_not_found() {
|
fn test_print_target_file_not_found() {
|
||||||
let res = print_target("Epson-Stylus-SX420W", "/non/existent/target.tif", false);
|
let res = print_target("Epson-Stylus-SX420W", "/non/existent/target.tif", None);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
assert!(res.unwrap_err().contains("not found"));
|
assert!(res.unwrap_err().contains("not found"));
|
||||||
}
|
}
|
||||||
|
|||||||
+262
-22
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(non_snake_case, dead_code)]
|
||||||
|
|
||||||
use std::ffi::{c_void, OsStr};
|
use std::ffi::{c_void, OsStr};
|
||||||
use std::os::windows::ffi::OsStrExt;
|
use std::os::windows::ffi::OsStrExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -12,8 +14,11 @@ use windows::Win32::Graphics::Printing::{
|
|||||||
ClosePrinter, DocumentPropertiesW, EnumPrintersW, OpenPrinterW,
|
ClosePrinter, DocumentPropertiesW, EnumPrintersW, OpenPrinterW,
|
||||||
PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_4W,
|
PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_4W,
|
||||||
};
|
};
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||||
|
|
||||||
use crate::print::Printer;
|
use crate::print::{
|
||||||
|
PrintOptions, Printer, PrinterCapabilities, PrinterDevModeStore, PrinterPaperSize, PrinterTray,
|
||||||
|
};
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct DOCINFOW {
|
struct DOCINFOW {
|
||||||
@@ -30,17 +35,41 @@ extern "system" {
|
|||||||
fn StartPage(hdc: HDC) -> i32;
|
fn StartPage(hdc: HDC) -> i32;
|
||||||
fn EndPage(hdc: HDC) -> i32;
|
fn EndPage(hdc: HDC) -> i32;
|
||||||
fn EndDoc(hdc: HDC) -> i32;
|
fn EndDoc(hdc: HDC) -> i32;
|
||||||
|
fn DeviceCapabilitiesW(
|
||||||
|
pdevicename: PCWSTR,
|
||||||
|
pport: PCWSTR,
|
||||||
|
fwcapability: u16,
|
||||||
|
poutput: *mut u16,
|
||||||
|
pdevmode: *const DEVMODEW,
|
||||||
|
) -> i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICM_OFF: i32 = 1;
|
const ICM_OFF: i32 = 1;
|
||||||
|
const DM_IN_PROMPT: u32 = 4;
|
||||||
|
const DM_IN_BUFFER: u32 = 8;
|
||||||
const DM_OUT_BUFFER: u32 = 2;
|
const DM_OUT_BUFFER: u32 = 2;
|
||||||
|
const DM_ORIENTATION: u32 = 0x00000001;
|
||||||
|
const DM_DEFAULTSOURCE: u32 = 0x00000200;
|
||||||
const DM_ICMMETHOD: u32 = 0x00800000;
|
const DM_ICMMETHOD: u32 = 0x00800000;
|
||||||
const DMICMMETHOD_NONE: u32 = 1;
|
const DMICMMETHOD_NONE: u32 = 1;
|
||||||
|
const DMORIENT_PORTRAIT: i16 = 1;
|
||||||
|
const DMORIENT_LANDSCAPE: i16 = 2;
|
||||||
|
|
||||||
|
const DC_PAPERS: u16 = 2;
|
||||||
|
const DC_BINS: u16 = 6;
|
||||||
|
const DC_BINNAMES: u16 = 12;
|
||||||
|
const DC_PAPERNAMES: u16 = 16;
|
||||||
|
const IDOK: i32 = 1;
|
||||||
|
|
||||||
fn to_wide(s: &str) -> Vec<u16> {
|
fn to_wide(s: &str) -> Vec<u16> {
|
||||||
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_null_terminated_string(wide_slice: &[u16]) -> String {
|
||||||
|
let len = wide_slice.iter().position(|&c| c == 0).unwrap_or(wide_slice.len());
|
||||||
|
String::from_utf16_lossy(&wide_slice[..len]).trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Enumerate available printers on the system
|
/// Enumerate available printers on the system
|
||||||
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -48,7 +77,6 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
let mut bytes_needed = 0u32;
|
let mut bytes_needed = 0u32;
|
||||||
let mut count = 0u32;
|
let mut count = 0u32;
|
||||||
|
|
||||||
// Query buffer size using Level 4 (fast local & network connections)
|
|
||||||
let _ = EnumPrintersW(
|
let _ = EnumPrintersW(
|
||||||
flags,
|
flags,
|
||||||
PCWSTR::null(),
|
PCWSTR::null(),
|
||||||
@@ -59,7 +87,6 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if bytes_needed == 0 {
|
if bytes_needed == 0 {
|
||||||
// Fallback to Level 1
|
|
||||||
let _ = EnumPrintersW(
|
let _ = EnumPrintersW(
|
||||||
flags,
|
flags,
|
||||||
PCWSTR::null(),
|
PCWSTR::null(),
|
||||||
@@ -134,8 +161,197 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print target TIFF file to printer with GDI ICM bypass and DIB drawing
|
/// Query hardware capabilities (paper trays and paper sizes) for a given printer
|
||||||
pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
pub fn get_printer_capabilities(printer_name: &str) -> Result<PrinterCapabilities, String> {
|
||||||
|
let printer_wide = to_wide(printer_name);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Query trays (bins)
|
||||||
|
let num_bins = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_BINS,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut trays = Vec::new();
|
||||||
|
if num_bins > 0 {
|
||||||
|
let mut bin_ids = vec![0u16; num_bins as usize];
|
||||||
|
let mut bin_names_raw = vec![0u16; num_bins as usize * 24];
|
||||||
|
|
||||||
|
let res_ids = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_BINS,
|
||||||
|
bin_ids.as_mut_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res_names = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_BINNAMES,
|
||||||
|
bin_names_raw.as_mut_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if res_ids > 0 && res_names > 0 {
|
||||||
|
for i in 0..num_bins as usize {
|
||||||
|
let id = bin_ids[i];
|
||||||
|
let name_slice = &bin_names_raw[i * 24..(i + 1) * 24];
|
||||||
|
let name = extract_null_terminated_string(name_slice);
|
||||||
|
let display_name = if name.is_empty() {
|
||||||
|
format!("Tray {}", id)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
trays.push(PrinterTray {
|
||||||
|
id,
|
||||||
|
name: display_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query paper sizes
|
||||||
|
let num_papers = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_PAPERS,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut paper_sizes = Vec::new();
|
||||||
|
if num_papers > 0 {
|
||||||
|
let mut paper_ids = vec![0u16; num_papers as usize];
|
||||||
|
let mut paper_names_raw = vec![0u16; num_papers as usize * 64];
|
||||||
|
|
||||||
|
let res_ids = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_PAPERS,
|
||||||
|
paper_ids.as_mut_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res_names = DeviceCapabilitiesW(
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
PCWSTR::null(),
|
||||||
|
DC_PAPERNAMES,
|
||||||
|
paper_names_raw.as_mut_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if res_ids > 0 && res_names > 0 {
|
||||||
|
for i in 0..num_papers as usize {
|
||||||
|
let id = paper_ids[i];
|
||||||
|
let name_slice = &paper_names_raw[i * 64..(i + 1) * 64];
|
||||||
|
let name = extract_null_terminated_string(name_slice);
|
||||||
|
let display_name = if name.is_empty() {
|
||||||
|
format!("Paper Size {}", id)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
paper_sizes.push(PrinterPaperSize {
|
||||||
|
id,
|
||||||
|
name: display_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PrinterCapabilities {
|
||||||
|
trays,
|
||||||
|
paper_sizes,
|
||||||
|
supports_orientation: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the native modal Printer Properties / Preferences dialog and retain DEVMODE changes
|
||||||
|
pub fn show_printer_properties(
|
||||||
|
printer_name: &str,
|
||||||
|
devmode_store: &PrinterDevModeStore,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let printer_wide = to_wide(printer_name);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut h_printer = HANDLE::default();
|
||||||
|
let open_res = OpenPrinterW(PCWSTR(printer_wide.as_ptr()), &mut h_printer, None);
|
||||||
|
if open_res.is_err() || h_printer.is_invalid() {
|
||||||
|
return Err(format!("Failed to open printer '{}'", printer_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_hwnd = GetForegroundWindow();
|
||||||
|
|
||||||
|
let devmode_size = DocumentPropertiesW(
|
||||||
|
parent_hwnd,
|
||||||
|
h_printer,
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if devmode_size <= 0 {
|
||||||
|
let _ = ClosePrinter(h_printer);
|
||||||
|
return Err("Failed to query DEVMODE size for printer properties".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut in_buf = devmode_store
|
||||||
|
.get(printer_name)
|
||||||
|
.unwrap_or_else(|| vec![0u8; devmode_size as usize]);
|
||||||
|
|
||||||
|
if in_buf.len() < devmode_size as usize {
|
||||||
|
in_buf.resize(devmode_size as usize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate initial DEVMODE if empty
|
||||||
|
if in_buf.iter().all(|&b| b == 0) {
|
||||||
|
let _ = DocumentPropertiesW(
|
||||||
|
parent_hwnd,
|
||||||
|
h_printer,
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
Some(in_buf.as_mut_ptr() as *mut DEVMODEW),
|
||||||
|
None,
|
||||||
|
DM_OUT_BUFFER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out_buf = vec![0u8; devmode_size as usize];
|
||||||
|
|
||||||
|
let res = DocumentPropertiesW(
|
||||||
|
parent_hwnd,
|
||||||
|
h_printer,
|
||||||
|
PCWSTR(printer_wide.as_ptr()),
|
||||||
|
Some(out_buf.as_mut_ptr() as *mut DEVMODEW),
|
||||||
|
Some(in_buf.as_ptr() as *const DEVMODEW),
|
||||||
|
DM_IN_PROMPT | DM_IN_BUFFER | DM_OUT_BUFFER,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = ClosePrinter(h_printer);
|
||||||
|
|
||||||
|
if res == IDOK {
|
||||||
|
devmode_store.set(printer_name, out_buf);
|
||||||
|
Ok(())
|
||||||
|
} else if res == 2 {
|
||||||
|
// Cancelled by user
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Printer properties dialog was dismissed or encountered an error".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print target TIFF file to printer with GDI ICM bypass, DEVMODE overrides, and auto-fit scaling
|
||||||
|
pub fn print_target(
|
||||||
|
printer_name: &str,
|
||||||
|
tiff_path: &str,
|
||||||
|
options: Option<&PrintOptions>,
|
||||||
|
devmode_store: Option<&PrinterDevModeStore>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let path = Path::new(tiff_path);
|
let path = Path::new(tiff_path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(format!("Target TIFF file not found: {}", tiff_path));
|
return Err(format!("Target TIFF file not found: {}", tiff_path));
|
||||||
@@ -166,7 +382,10 @@ pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let printer_wide = to_wide(printer_name);
|
let printer_wide = to_wide(printer_name);
|
||||||
let doc_name = format!("ICCery Target - {}", path.file_name().and_then(|n| n.to_str()).unwrap_or("Profiling Target"));
|
let doc_name = format!(
|
||||||
|
"ICCery Target - {}",
|
||||||
|
path.file_name().and_then(|n| n.to_str()).unwrap_or("Profiling Target")
|
||||||
|
);
|
||||||
let doc_name_wide = to_wide(&doc_name);
|
let doc_name_wide = to_wide(&doc_name);
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -177,7 +396,7 @@ pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
|||||||
return Err(format!("Failed to open printer '{}'", printer_name));
|
return Err(format!("Failed to open printer '{}'", printer_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure DEVMODE if possible
|
// Configure DEVMODE
|
||||||
let devmode_size = DocumentPropertiesW(
|
let devmode_size = DocumentPropertiesW(
|
||||||
HWND::default(),
|
HWND::default(),
|
||||||
h_printer,
|
h_printer,
|
||||||
@@ -188,7 +407,14 @@ pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut devmode_buf = if devmode_size > 0 {
|
let mut devmode_buf = if devmode_size > 0 {
|
||||||
let mut buf = vec![0u8; devmode_size as usize];
|
let mut buf = devmode_store
|
||||||
|
.and_then(|s| s.get(printer_name))
|
||||||
|
.unwrap_or_else(|| vec![0u8; devmode_size as usize]);
|
||||||
|
|
||||||
|
if buf.len() < devmode_size as usize {
|
||||||
|
buf.resize(devmode_size as usize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW;
|
let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW;
|
||||||
let res = DocumentPropertiesW(
|
let res = DocumentPropertiesW(
|
||||||
HWND::default(),
|
HWND::default(),
|
||||||
@@ -198,10 +424,29 @@ pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
|||||||
None,
|
None,
|
||||||
DM_OUT_BUFFER,
|
DM_OUT_BUFFER,
|
||||||
);
|
);
|
||||||
|
|
||||||
if res >= 0 {
|
if res >= 0 {
|
||||||
// Attempt to explicitly disable driver ICM if supported in DEVMODE
|
// Apply strict ICM bypass
|
||||||
(*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ICMMETHOD);
|
(*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ICMMETHOD);
|
||||||
(*p_devmode).dmICMMethod = DMICMMETHOD_NONE;
|
(*p_devmode).dmICMMethod = DMICMMETHOD_NONE;
|
||||||
|
|
||||||
|
// Apply UI options if specified
|
||||||
|
if let Some(opts) = options {
|
||||||
|
if let Some(tray_id) = opts.paper_source {
|
||||||
|
(*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_DEFAULTSOURCE);
|
||||||
|
(*p_devmode).Anonymous1.Anonymous1.dmDefaultSource = tray_id as i16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref orient) = opts.orientation {
|
||||||
|
(*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ORIENTATION);
|
||||||
|
if orient.eq_ignore_ascii_case("landscape") {
|
||||||
|
(*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_LANDSCAPE;
|
||||||
|
} else {
|
||||||
|
(*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_PORTRAIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some(buf)
|
Some(buf)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -255,25 +500,20 @@ pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> {
|
|||||||
return Err("Failed to start print page (StartPage)".to_string());
|
return Err("Failed to start print page (StartPage)".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query DC dimensions and DPI
|
// Query printable device dimensions
|
||||||
let dpi_x = GetDeviceCaps(hdc, LOGPIXELSX);
|
let dpi_x = GetDeviceCaps(hdc, LOGPIXELSX);
|
||||||
let dpi_y = GetDeviceCaps(hdc, LOGPIXELSY);
|
let dpi_y = GetDeviceCaps(hdc, LOGPIXELSY);
|
||||||
let page_width = GetDeviceCaps(hdc, HORZRES);
|
let page_width = GetDeviceCaps(hdc, HORZRES);
|
||||||
let page_height = GetDeviceCaps(hdc, VERTRES);
|
let page_height = GetDeviceCaps(hdc, VERTRES);
|
||||||
|
|
||||||
// Calculate destination dimensions assuming standard 300 DPI target
|
// AUTOMATIC BEHIND-THE-SCENES PAGE-FIT SCALER:
|
||||||
let target_dpi = 300.0f64;
|
// Proportionally scale the target TIFF to fill the available physical printable area
|
||||||
let mut dest_width = ((img_width as f64 / target_dpi) * dpi_x as f64).round() as i32;
|
let scale_x = page_width as f64 / img_width as f64;
|
||||||
let mut dest_height = ((img_height as f64 / target_dpi) * dpi_y as f64).round() as i32;
|
let scale_y = page_height as f64 / img_height as f64;
|
||||||
|
|
||||||
// If target exceeds page bounds, scale proportionally to fit printable area
|
|
||||||
if dest_width > page_width || dest_height > page_height {
|
|
||||||
let scale_x = page_width as f64 / dest_width as f64;
|
|
||||||
let scale_y = page_height as f64 / dest_height as f64;
|
|
||||||
let scale = scale_x.min(scale_y);
|
let scale = scale_x.min(scale_y);
|
||||||
dest_width = (dest_width as f64 * scale).round() as i32;
|
|
||||||
dest_height = (dest_height as f64 * scale).round() as i32;
|
let dest_width = (img_width as f64 * scale).floor() as i32;
|
||||||
}
|
let dest_height = (img_height as f64 * scale).floor() as i32;
|
||||||
|
|
||||||
let dest_x = (page_width - dest_width).max(0) / 2;
|
let dest_x = (page_width - dest_width).max(0) / 2;
|
||||||
let dest_y = (page_height - dest_height).max(0) / 2;
|
let dest_y = (page_height - dest_height).max(0) / 2;
|
||||||
|
|||||||
+24
-7
@@ -166,13 +166,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="tiffDpi" class="sub-label">Resolution (DPI)</label>
|
<label class="sub-label">Automatic Fit Scaling</label>
|
||||||
<select id="tiffDpi">
|
<div class="auto-scaler-badge" title="Master 300 DPI high-resolution target is automatically scaled to fit the device printable area without distortion or clipping">
|
||||||
<option value="100">100 DPI (Draft)</option>
|
<span class="badge-icon">⚡</span>
|
||||||
<option value="200" selected>200 DPI (Standard)</option>
|
<span>300 DPI Auto-Fit Scaler (Active)</span>
|
||||||
<option value="300">300 DPI (High Quality)</option>
|
</div>
|
||||||
<option value="600">600 DPI (Ultra)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,6 +217,7 @@
|
|||||||
<option value="" disabled selected>Detecting printers...</option>
|
<option value="" disabled selected>Detecting printers...</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn-icon" id="btnRefreshPrinters" title="Refresh printer list">↻</button>
|
<button type="button" class="btn-icon" id="btnRefreshPrinters" title="Refresh printer list">↻</button>
|
||||||
|
<button type="button" class="btn-properties" id="btnPrinterProperties" title="Open native printer driver preferences (disable colour management, set media type, etc.)">⚙️ Preferences</button>
|
||||||
<span id="printerStatusBadge" class="status-badge badge-idle hidden">Idle</span>
|
<span id="printerStatusBadge" class="status-badge badge-idle hidden">Idle</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,6 +230,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Media & Print Job Configuration Row -->
|
||||||
|
<div class="printer-media-row">
|
||||||
|
<div class="form-group media-group">
|
||||||
|
<label for="printerTraySelect">Paper Source / Tray</label>
|
||||||
|
<select id="printerTraySelect">
|
||||||
|
<option value="" selected>Default / Auto Select</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group orientation-group">
|
||||||
|
<label>Orientation</label>
|
||||||
|
<div class="orientation-toggle-group">
|
||||||
|
<button type="button" class="btn-toggle active" id="btnOrientPortrait" data-orient="portrait" title="Portrait orientation">📄 Portrait</button>
|
||||||
|
<button type="button" class="btn-toggle" id="btnOrientLandscape" data-orient="landscape" title="Landscape orientation">📄 Landscape</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="print-actions-row">
|
<div class="print-actions-row">
|
||||||
<button class="primary btn-print" id="btnPrintAll">
|
<button class="primary btn-print" id="btnPrintAll">
|
||||||
<span class="btn-text">🖨️ Print All Pages (Bypass CM)</span>
|
<span class="btn-text">🖨️ Print All Pages (Bypass CM)</span>
|
||||||
|
|||||||
+90
-6
@@ -36,6 +36,10 @@ export function initPrinttarg() {
|
|||||||
const rawPrintPanel = document.getElementById("rawPrintPanel");
|
const rawPrintPanel = document.getElementById("rawPrintPanel");
|
||||||
const printerSelect = document.getElementById("printerSelect");
|
const printerSelect = document.getElementById("printerSelect");
|
||||||
const btnRefreshPrinters = document.getElementById("btnRefreshPrinters");
|
const btnRefreshPrinters = document.getElementById("btnRefreshPrinters");
|
||||||
|
const btnPrinterProperties = document.getElementById("btnPrinterProperties");
|
||||||
|
const printerTraySelect = document.getElementById("printerTraySelect");
|
||||||
|
const btnOrientPortrait = document.getElementById("btnOrientPortrait");
|
||||||
|
const btnOrientLandscape = document.getElementById("btnOrientLandscape");
|
||||||
const printerStatusBadge = document.getElementById("printerStatusBadge");
|
const printerStatusBadge = document.getElementById("printerStatusBadge");
|
||||||
const chkPpdFallback = document.getElementById("chkPpdFallback");
|
const chkPpdFallback = document.getElementById("chkPpdFallback");
|
||||||
const btnPrintAll = document.getElementById("btnPrintAll");
|
const btnPrintAll = document.getElementById("btnPrintAll");
|
||||||
@@ -44,6 +48,23 @@ export function initPrinttarg() {
|
|||||||
const printNotificationIcon = document.getElementById("printNotificationIcon");
|
const printNotificationIcon = document.getElementById("printNotificationIcon");
|
||||||
const printNotificationText = document.getElementById("printNotificationText");
|
const printNotificationText = document.getElementById("printNotificationText");
|
||||||
|
|
||||||
|
let selectedOrientation = "portrait";
|
||||||
|
|
||||||
|
// Orientation toggle buttons
|
||||||
|
if (btnOrientPortrait && btnOrientLandscape) {
|
||||||
|
btnOrientPortrait.addEventListener("click", () => {
|
||||||
|
selectedOrientation = "portrait";
|
||||||
|
btnOrientPortrait.classList.add("active");
|
||||||
|
btnOrientLandscape.classList.remove("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
btnOrientLandscape.addEventListener("click", () => {
|
||||||
|
selectedOrientation = "landscape";
|
||||||
|
btnOrientLandscape.classList.add("active");
|
||||||
|
btnOrientPortrait.classList.remove("active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show/hide custom page size inputs
|
// Show/hide custom page size inputs
|
||||||
pageSizeSelect.addEventListener("change", (e) => {
|
pageSizeSelect.addEventListener("change", (e) => {
|
||||||
if (e.target.value === "custom") {
|
if (e.target.value === "custom") {
|
||||||
@@ -78,6 +99,44 @@ export function initPrinttarg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch hardware trays and paper capabilities for selected printer.
|
||||||
|
*/
|
||||||
|
async function loadPrinterCapabilities(printerName) {
|
||||||
|
if (!printerTraySelect || !printerName) return;
|
||||||
|
printerTraySelect.innerHTML = '<option value="" selected>Default / Auto Select</option>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const caps = await invoke("get_printer_capabilities", { printerName });
|
||||||
|
if (caps && caps.trays && caps.trays.length > 0) {
|
||||||
|
caps.trays.forEach(tray => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = tray.id;
|
||||||
|
opt.textContent = tray.name;
|
||||||
|
printerTraySelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[ICCery Print] Could not fetch printer capabilities:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build current print options payload from UI controls.
|
||||||
|
*/
|
||||||
|
function getSelectedPrintOptions() {
|
||||||
|
const trayVal = printerTraySelect ? printerTraySelect.value : "";
|
||||||
|
const paperSource = trayVal ? parseInt(trayVal, 10) : null;
|
||||||
|
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
paper_source: paperSource,
|
||||||
|
orientation: selectedOrientation,
|
||||||
|
paper_size: pageSizeSelect ? pageSizeSelect.value : null,
|
||||||
|
ppd_uncorrected_passthrough: ppdFallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the badge reflecting current printer's status.
|
* Update the badge reflecting current printer's status.
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +204,11 @@ export function initPrinttarg() {
|
|||||||
printerSelect.selectedIndex = 0;
|
printerSelect.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activePrinter = printerSelect.value;
|
||||||
|
if (activePrinter) {
|
||||||
|
loadPrinterCapabilities(activePrinter);
|
||||||
|
}
|
||||||
|
|
||||||
if (btnPrintAll && currentManifest) btnPrintAll.disabled = false;
|
if (btnPrintAll && currentManifest) btnPrintAll.disabled = false;
|
||||||
updatePrinterStatusBadge();
|
updatePrinterStatusBadge();
|
||||||
|
|
||||||
@@ -160,6 +224,7 @@ export function initPrinttarg() {
|
|||||||
if (printerSelect) {
|
if (printerSelect) {
|
||||||
printerSelect.addEventListener("change", () => {
|
printerSelect.addEventListener("change", () => {
|
||||||
updatePrinterStatusBadge();
|
updatePrinterStatusBadge();
|
||||||
|
loadPrinterCapabilities(printerSelect.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +234,25 @@ export function initPrinttarg() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (btnPrinterProperties) {
|
||||||
|
btnPrinterProperties.addEventListener("click", async () => {
|
||||||
|
const printerName = printerSelect ? printerSelect.value : "";
|
||||||
|
if (!printerName) {
|
||||||
|
showNotification("error", "Please select a destination printer first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showNotification("info", `Opening native printer preferences for '${printerName}'...`);
|
||||||
|
await invoke("show_printer_properties", { printerName });
|
||||||
|
showNotification("success", `✓ Printer driver preferences configured for '${printerName}'.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ICCery Print] Failed to open printer properties:", err);
|
||||||
|
showNotification("error", `Could not open printer properties: ${err}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load initial printer list on startup
|
// Load initial printer list on startup
|
||||||
loadPrinters();
|
loadPrinters();
|
||||||
|
|
||||||
@@ -212,7 +296,7 @@ export function initPrinttarg() {
|
|||||||
instrument: instrumentSelect.value,
|
instrument: instrumentSelect.value,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
bit_depth: bitDepth,
|
bit_depth: bitDepth,
|
||||||
dpi: parseInt(tiffDpi.value, 10),
|
dpi: 300, // Master high-resolution layout scaled automatically to device bounds
|
||||||
basename: stage1Basename,
|
basename: stage1Basename,
|
||||||
cwd: stage1Cwd,
|
cwd: stage1Cwd,
|
||||||
};
|
};
|
||||||
@@ -274,7 +358,7 @@ export function initPrinttarg() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spool a single TIFF target file with native color management bypass.
|
* Spool a single TIFF target file with native color management bypass and options.
|
||||||
*/
|
*/
|
||||||
async function printTargetFile(filePath, label, triggeringButton) {
|
async function printTargetFile(filePath, label, triggeringButton) {
|
||||||
const printerName = printerSelect ? printerSelect.value : "";
|
const printerName = printerSelect ? printerSelect.value : "";
|
||||||
@@ -283,7 +367,7 @@ export function initPrinttarg() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
const options = getSelectedPrintOptions();
|
||||||
const origBtnContent = triggeringButton ? triggeringButton.innerHTML : "";
|
const origBtnContent = triggeringButton ? triggeringButton.innerHTML : "";
|
||||||
|
|
||||||
if (triggeringButton) {
|
if (triggeringButton) {
|
||||||
@@ -297,7 +381,7 @@ export function initPrinttarg() {
|
|||||||
await invoke("print_target_native", {
|
await invoke("print_target_native", {
|
||||||
printerName,
|
printerName,
|
||||||
tiffPath: filePath,
|
tiffPath: filePath,
|
||||||
ppdUncorrectedPassthrough: ppdFallback,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
showNotification("success", `✓ Successfully spooled ${label} to '${printerName}' with raw color management bypass.`);
|
showNotification("success", `✓ Successfully spooled ${label} to '${printerName}' with raw color management bypass.`);
|
||||||
@@ -328,7 +412,7 @@ export function initPrinttarg() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
const options = getSelectedPrintOptions();
|
||||||
const cwd = stage1Cwd;
|
const cwd = stage1Cwd;
|
||||||
const sep = cwd.includes('\\') ? '\\' : '/';
|
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||||
const pages = currentManifest.pages;
|
const pages = currentManifest.pages;
|
||||||
@@ -350,7 +434,7 @@ export function initPrinttarg() {
|
|||||||
await invoke("print_target_native", {
|
await invoke("print_target_native", {
|
||||||
printerName,
|
printerName,
|
||||||
tiffPath: filePath,
|
tiffPath: filePath,
|
||||||
ppdUncorrectedPassthrough: ppdFallback,
|
options,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[ICCery Print Error] Page ${page.filename}:`, err);
|
console.error(`[ICCery Print Error] Page ${page.filename}:`, err);
|
||||||
|
|||||||
@@ -924,6 +924,97 @@ button.danger:hover {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-scaler-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(0, 122, 204, 0.12);
|
||||||
|
border: 1px solid rgba(0, 122, 204, 0.35);
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-scaler-badge .badge-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-properties {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-properties:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-media-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-media-row .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orientation-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: #aaa;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle.active {
|
||||||
|
background: rgba(0, 122, 204, 0.25);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user