diff --git a/crates/print/README.md b/crates/print/README.md
--- a/crates/print/README.md
+++ b/crates/print/README.md
@@ -27,7 +27,10 @@ opens Windows printer preferences and submits jobs with copies, collation, duple
   from storytold/pdfcraft#599 and #1132.
   A job has one ticket, so one paper and orientation. Sheets of the other orientation are
   turned back with `/Rotate`, so portrait and landscape sheets still print as one job. Exact
-  size refuses sheets of different sizes and paper the driver lacks. Shrink-to-fit uses the
+  size refuses sheets of different sizes and paper the driver lacks; when no named paper matches,
+  it uses the driver's `psk:CustomMediaSize` option with the sheet's actual dimensions (for
+  example a 1067 x 480 mm plotter sheet), refusing a size outside the driver's limits instead of
+  cropping it. Shrink-to-fit uses the
   driver's matching paper, else its default paper, and puts every sheet on it: a sheet that
   doesn't fit inside the paper's widest hardware margin is shrunk, a smaller one keeps its
   size, and both are centred. Both are done by appending an incremental update to the PDF
diff --git a/crates/print/src/ticket.rs b/crates/print/src/ticket.rs
--- a/crates/print/src/ticket.rs
+++ b/crates/print/src/ticket.rs
@@ -146,11 +146,10 @@ fn integer(node: roxmltree::Node<'_, '_>, tag: &str, name: &str) -> Option<u32>
 /// some drivers return empty vendor parameters that a stricter parser would refuse.
 pub(crate) fn verify_ticket(xml: &str, paper: Option<(u32, u32)>, landscape: bool) -> Result<(u32, u32), Error> {
     let document = roxmltree::Document::parse(xml).map_err(spool_error)?;
-    let feature = |name: &str| document.root_element().children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, name));
+    let root = document.root_element();
+    let feature = |name: &str| root.children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, name));
     let media = feature("PageMediaSize").ok_or_else(|| spool_error("the printer's driver did not confirm the paper size; nothing was sent"))?;
-    let size = integer(media, "ScoredProperty", "MediaSizeWidth")
-        .zip(integer(media, "ScoredProperty", "MediaSizeHeight"))
-        .filter(|(w, h)| *w > 0 && *h > 0)
+    let size = resolved_media_size(root, media)
         .ok_or_else(|| spool_error("the printer's driver gave a paper without a size (a roll?); nothing was sent"))?;
     if let Some((w, h)) = paper {
         let same = |a: (u32, u32), b: (u32, u32)| a.0.abs_diff(b.0) <= PAPER_TOLERANCE && a.1.abs_diff(b.1) <= PAPER_TOLERANCE;
@@ -265,6 +264,59 @@ mod tests {
         assert!(!ticket_xml(&Job { printer: Some("Office \"east\" <1>".into()), ..Job::default() }, false).contains("east"));
     }
 
+    /// A validated ticket for a driver custom media size, as the driver returns it: the option's
+    /// dimensions are parameter references, resolved from the parameter initializers.
+    fn custom_validated(paper: (u32, u32), orientation: &str) -> String {
+        format!(
+            r#"<psf:PrintTicket version="1" xmlns:psf="{NS_PSF}" xmlns:psk="{NS_PSK}" xmlns:media="{NS_PSK}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+<psf:ParameterInit name="media:PageMediaSizeMediaSizeWidth"><psf:Value xsi:type="xsd:integer">{}</psf:Value></psf:ParameterInit>
+<psf:ParameterInit name="media:PageMediaSizeMediaSizeHeight"><psf:Value xsi:type="xsd:integer">{}</psf:Value></psf:ParameterInit>
+<psf:Feature name="psk:PageMediaSize"><psf:Option name="psk:CustomMediaSize">
+<psf:ScoredProperty name="psk:MediaSizeWidth"><psf:ParameterRef name="psk:PageMediaSizeMediaSizeWidth"/></psf:ScoredProperty>
+<psf:ScoredProperty name="psk:MediaSizeHeight"><psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight"/></psf:ScoredProperty>
+</psf:Option></psf:Feature>
+<psf:Feature name="psk:PageOrientation"><psf:Option name="psk:{orientation}"/></psf:Feature>
+</psf:PrintTicket>"#,
+            paper.0, paper.1
+        )
+    }
+
+    #[test]
+    fn a_custom_media_ticket_resolves_its_parameters() {
+        let custom = (1_067_000, 480_000);
+        let xml = custom_validated(custom, "Portrait");
+        assert_eq!(verify_ticket(&xml, Some(custom), false), Ok(custom));
+        assert_eq!(verify_ticket(&xml, Some((custom.1, custom.0)), false), Ok(custom), "either way round");
+        assert!(verify_ticket(&xml, Some((1_000_000, 480_000)), false).is_err(), "another size");
+        assert!(verify_ticket(&xml, Some(custom), true).is_err(), "orientation");
+    }
+
+    #[test]
+    fn custom_media_ranges_come_from_the_capabilities() {
+        let caps = format!(
+            r#"<psf:PrintCapabilities version="1" xmlns:psf="{NS_PSF}" xmlns:psk="{NS_PSK}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+<psf:Feature name="psk:PageMediaSize"><psf:Option name="psk:CustomMediaSize">
+<psf:ScoredProperty name="psk:MediaSizeWidth"><psf:ParameterRef name="psk:PageMediaSizeMediaSizeWidth"/></psf:ScoredProperty>
+<psf:ScoredProperty name="psk:MediaSizeHeight"><psf:ParameterRef name="psk:PageMediaSizeMediaSizeHeight"/></psf:ScoredProperty>
+</psf:Option></psf:Feature>
+<psf:ParameterDef name="psk:PageMediaSizeMediaSizeWidth"><psf:Property name="psf:MinValue"><psf:Value xsi:type="xsd:integer">78994</psf:Value></psf:Property><psf:Property name="psf:MaxValue"><psf:Value xsi:type="xsd:integer">1123950</psf:Value></psf:Property></psf:ParameterDef>
+<psf:ParameterDef name="psk:PageMediaSizeMediaSizeHeight"><psf:Property name="psf:MinValue"><psf:Value xsi:type="xsd:integer">139996</psf:Value></psf:Property><psf:Property name="psf:MaxValue"><psf:Value xsi:type="xsd:integer">91000008</psf:Value></psf:Property></psf:ParameterDef>
+</psf:PrintCapabilities>"#
+        );
+        let custom = custom_media(&caps).expect("custom media");
+        assert_eq!(custom, CustomMedia { min_width: 78_994, max_width: 1_123_950, min_height: 139_996, max_height: 91_000_008 });
+        assert!(custom.allows(1_067_000, 480_000));
+        assert!(!custom.allows(1_200_000, 480_000));
+        assert!(!custom.allows(1_067_000, 100));
+        assert_eq!(custom.fit(1_067_000, 480_000), Some((1_067_000, 480_000)));
+        assert_eq!(custom.fit(1_200_000, 480_000), Some((480_000, 1_200_000)), "the other way round fits");
+        assert_eq!(custom.fit(2_000_000, 2_000_000), None);
+        assert!(
+            custom_media("<psf:PrintCapabilities xmlns:psf=\"http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework\"/>")
+                .is_none()
+        );
+    }
+
     /// A validated ticket as a driver returns it.
     fn validated(paper: (u32, u32), orientation: &str, scaling: Option<&str>) -> String {
         let scaling = scaling.map_or(String::new(), |s| format!(r#"<psf:Feature name="psk:PageScaling"><psf:Option name="{s}"/></psf:Feature>"#));
@@ -405,3 +457,86 @@ mod tests {
         assert_eq!(job_filename("Отчёт"), "Отчёт.pdf");
     }
 }
+/// The driver's custom-media support and the bounds its `psk:CustomMediaSize` width and height
+/// parameters take, in micrometres. Only a driver that offers the option with both bounds can
+/// take a size no named paper covers.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) struct CustomMedia {
+    pub min_width: u32,
+    pub max_width: u32,
+    pub min_height: u32,
+    pub max_height: u32,
+}
+
+impl CustomMedia {
+    /// Whether a sheet of `width` � `height` micrometres is inside the driver's custom-media range.
+    pub(crate) fn allows(self, width: u32, height: u32) -> bool {
+        (self.min_width..=self.max_width).contains(&width) && (self.min_height..=self.max_height).contains(&height)
+    }
+
+    /// The sheet's dimensions in the order this driver can take, either way round; `None` when
+    /// neither order is inside its ranges.
+    pub(crate) fn fit(self, width: u32, height: u32) -> Option<(u32, u32)> {
+        [(width, height), (height, width)].into_iter().find(|&(w, h)| self.allows(w, h))
+    }
+}
+
+/// The driver's `psk:CustomMediaSize` option and the ranges of its width and height parameters,
+/// from its PrintCapabilities XML. `None` when the driver offers no custom media, or gives no
+/// bounds for a side.
+pub(crate) fn custom_media(xml: &str) -> Option<CustomMedia> {
+    let document = roxmltree::Document::parse(xml).ok()?;
+    let root = document.root_element();
+    let feature = root.children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, "PageMediaSize"))?;
+    feature.children().find(|n| n.has_tag_name((NS_PSF, "Option")) && named(*n, "CustomMediaSize"))?;
+    let range = |name: &str| {
+        let def = root.descendants().find(|n| n.has_tag_name((NS_PSF, "ParameterDef")) && named_ns(*n, name, NS_PSK))?;
+        let bound = |property: &str| {
+            def.descendants()
+                .find(|n| n.has_tag_name((NS_PSF, "Property")) && named_ns(*n, property, NS_PSF))
+                .and_then(|n| n.descendants().find(|v| v.has_tag_name((NS_PSF, "Value"))))
+                .and_then(|v| v.text())
+                .and_then(|v| v.trim().parse::<u32>().ok())
+        };
+        Some((bound("MinValue")?, bound("MaxValue")?))
+    };
+    let (min_width, max_width) = range("PageMediaSizeMediaSizeWidth")?;
+    let (min_height, max_height) = range("PageMediaSizeMediaSizeHeight")?;
+    (min_width <= max_width && min_height <= max_height).then_some(CustomMedia { min_width, max_width, min_height, max_height })
+}
+
+fn named_ns(node: roxmltree::Node<'_, '_>, local: &str, namespace: &str) -> bool {
+    node.attribute("name")
+        .and_then(|v| v.split_once(':'))
+        .is_some_and(|(prefix, name)| name == local && node.lookup_namespace_uri(Some(prefix)) == Some(namespace))
+}
+
+/// A `name` attribute's local part and the namespace URI its prefix resolves to in this node.
+/// Prefixes are only XML spelling; the same namespace under two prefixes names the same thing.
+fn qualified_name(node: roxmltree::Node<'_, '_>, attribute: &str) -> Option<(String, String)> {
+    let value = node.attribute(attribute)?;
+    let (prefix, local) = value.split_once(':').map_or((None, value), |(prefix, local)| (Some(prefix), local));
+    let namespace = node.lookup_namespace_uri(prefix)?;
+    Some((local.to_owned(), namespace.to_owned()))
+}
+
+/// The integer a scored property carries, directly or through its parameter initializer.
+fn scored_integer(root: roxmltree::Node<'_, '_>, media: roxmltree::Node<'_, '_>, name: &str) -> Option<u32> {
+    let scored = media.descendants().find(|n| n.has_tag_name((NS_PSF, "ScoredProperty")) && named(*n, name))?;
+    if let Some(value) =
+        scored.children().find(|n| n.has_tag_name((NS_PSF, "Value"))).and_then(|n| n.text()).and_then(|v| v.trim().parse::<u32>().ok())
+    {
+        return Some(value);
+    }
+    let reference = qualified_name(scored.children().find(|n| n.has_tag_name((NS_PSF, "ParameterRef")))?, "name")?;
+    let init = root.descendants().find(|n| {
+        n.has_tag_name((NS_PSF, "ParameterInit")) && qualified_name(*n, "name").is_some_and(|name| name.0 == reference.0 && name.1 == reference.1)
+    })?;
+    init.children().find(|n| n.has_tag_name((NS_PSF, "Value"))).and_then(|n| n.text()).and_then(|v| v.trim().parse::<u32>().ok())
+}
+
+fn resolved_media_size(root: roxmltree::Node<'_, '_>, media: roxmltree::Node<'_, '_>) -> Option<(u32, u32)> {
+    let width = scored_integer(root, media, "MediaSizeWidth")?;
+    let height = scored_integer(root, media, "MediaSizeHeight")?;
+    (width > 0 && height > 0).then_some((width, height))
+}
diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs
--- a/crates/print/src/windows.rs
+++ b/crates/print/src/windows.rs
@@ -13,7 +13,7 @@
 use std::io::Write as _;
 
 use winprint::printer::{PrinterDevice, WinPdfPrinter};
-use winprint::ticket::document::{NS_PSK, OwnedName, PrintFeature, PrintTicketDocument, reader::ParsableXmlDocument as _};
+use winprint::ticket::document::{NS_PSK, OwnedName, PrintFeature, PrintTicketDocument, PropertyValue, reader::ParsableXmlDocument as _};
 use winprint::ticket::{FeatureOptionPack as _, PageMediaSize, PrintCapabilities, PrintTicket, PrintTicketBuilder};
 
 use crate::sheets::{self, Fit, SAME};
@@ -64,6 +64,9 @@ fn select_device(devices: Vec<PrinterDevice>, requested: Option<&str>, default:
 /// job at the wrong size.
 fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape: bool) -> Result<(PrintTicket, (u32, u32)), Error> {
     let capabilities = PrintCapabilities::fetch(device).map_err(spool_error)?;
+    let capabilities_xml = PrintCapabilities::fetch_xml(device).map_err(spool_error)?;
+    let capabilities_xml = std::str::from_utf8(&capabilities_xml).map_err(spool_error)?;
+    let custom = ticket::custom_media(capabilities_xml);
     let medias: Vec<PageMediaSize> = capabilities.page_media_sizes().collect();
     let papers: Vec<Paper<'_>> = medias
         .iter()
@@ -75,16 +78,25 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape
         })
         .collect();
     let media = ticket::matching_paper(&papers, sheet.0, sheet.1).and_then(|i| medias.get(i));
+    let mut custom_size = None;
     let wanted = match (media, job.scaling) {
         (Some(m), _) => Some((m.size().width_in_micron(), m.size().height_in_micron())),
         (None, Scaling::ShrinkToFit) => None,
         (None, Scaling::Exact) => {
-            return Err(spool_error(format!(
-                "{} has no {:.2} × {:.2} inch paper; choose a paper size it has, or add this one in its driver",
-                device.name(),
-                sheet.0 / 72.0,
-                sheet.1 / 72.0
-            )));
+            let width = ticket::micrometres(sheet.0).ok_or_else(|| spool_error("the PDF sheet size is not usable"))?;
+            let height = ticket::micrometres(sheet.1).ok_or_else(|| spool_error("the PDF sheet size is not usable"))?;
+            let Some((width, height)) = custom.and_then(|range| range.fit(width, height)) else {
+                return Err(spool_error(format!(
+                    "{} has no {:.2} x {:.2} inch paper and no matching custom media size; choose a paper size it has, or add this one in its driver",
+                    device.name(),
+                    sheet.0 / 72.0,
+                    sheet.1 / 72.0
+                )));
+            };
+            // A wide custom sheet takes Landscape, as a wide sheet on a named portrait paper
+            // does; the driver normalises the custom form to its own portrait dimensions.
+            custom_size = Some((width, height));
+            Some((width, height))
         }
     };
     let requested = PrintTicket::from_xml(ticket::ticket_xml(job, landscape));
@@ -94,6 +106,45 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape
         let media_document = PrintTicketDocument::parse_from_bytes(media_ticket.get_xml()).map_err(spool_error)?;
         document.features.extend(media_document.features);
         document.parameter_inits.extend(media_document.parameter_inits);
+    } else if let Some((width, height)) = custom_size {
+        let Some(mut custom) = medias
+            .iter()
+            .find(|m| m.option().name.as_ref().is_some_and(|n| n.namespace_ref() == Some(NS_PSK) && n.local_name == "CustomMediaSize"))
+            .cloned()
+        else {
+            return Err(spool_error("the printer offers a custom media size but does not list it; nothing was sent"));
+        };
+        let (mut width_set, mut height_set) = (false, false);
+        for parameter in custom.parameters_mut() {
+            if parameter.name.namespace_ref() == Some(NS_PSK) && parameter.name.local_name == "PageMediaSizeMediaSizeWidth" {
+                parameter.value = PropertyValue::Integer(width as i32);
+                width_set = true;
+            } else if parameter.name.namespace_ref() == Some(NS_PSK) && parameter.name.local_name == "PageMediaSizeMediaSizeHeight" {
+                parameter.value = PropertyValue::Integer(height as i32);
+                height_set = true;
+            }
+        }
+        if !(width_set && height_set) {
+            return Err(spool_error("the printer does not report the custom size parameters; nothing was sent"));
+        }
+        // The HP PCL3 driver reads the custom media dimensions from the option's direct scored
+        // properties; with the capability's parameter references it emits PAPERLENGTH=width
+        // (a square page). Put the two integers in the option itself.
+        for property in &mut custom.option_mut().scored_properties {
+            let Some(name) = property.name.as_ref() else { continue };
+            let value = match name.local_name.as_str() {
+                "MediaSizeWidth" => width,
+                "MediaSizeHeight" => height,
+                _ => continue,
+            };
+            property.parameter_ref = None;
+            property.value = Some(PropertyValue::Integer(value as i32));
+        }
+        custom.parameters_mut().clear();
+        let media_ticket: PrintTicket = custom.into();
+        let media_document = PrintTicketDocument::parse_from_bytes(media_ticket.get_xml()).map_err(spool_error)?;
+        document.features.extend(media_document.features);
+        document.parameter_inits.extend(media_document.parameter_inits);
     }
     let scaling_name = OwnedName::qualified("PageScaling", NS_PSK, Some("psk"));
     if let Some(no_scaling) = capabilities.options_for_feature(scaling_name.clone()).find(|o| o.name.as_ref().is_some_and(|n| n.local_name == "None"))
@@ -101,6 +152,21 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape
         document.features.retain(|f| !(f.name.local_name == "PageScaling" && f.name.namespace_ref() == Some(NS_PSK)));
         document.features.push(PrintFeature { name: scaling_name, properties: Vec::new(), options: vec![no_scaling.clone()], features: Vec::new() });
     }
+    // The HP PCL driver inherits PageAutorotate from the queue, so with a user's fixed
+    // "rotate 0" setting it emits a square paper length (PJL PAPERLENGTH = PAPERWIDTH) and the
+    // printer cuts the page at its width. A named paper or the driver's PrintTicket preview
+    // still looks right. Let the printer manage the roll orientation, as Affinity does.
+    if let Some(autorotate) = capabilities.document.features.iter().find(|f| f.name.local_name == "PageAutorotate")
+        && let Some(option) = autorotate.options.iter().find(|o| o.name.as_ref().is_some_and(|n| n.local_name == "Printer" || n.local_name == "On"))
+    {
+        document.features.retain(|f| f.name.local_name != "PageAutorotate");
+        document.features.push(PrintFeature {
+            name: autorotate.name.clone(),
+            properties: Vec::new(),
+            options: vec![option.clone()],
+            features: Vec::new(),
+        });
+    }
     let mut builder = PrintTicketBuilder::new(device).map_err(spool_error)?;
     builder.merge(PrintTicket::from(document)).map_err(spool_error)?;
     let validated = builder.build().map_err(spool_error)?;
@@ -223,6 +289,30 @@ mod tests {
         }
     }
 
+    /// A real driver's custom media: a 1067 x 480 mm sheet, no named paper of that size. The
+    /// driver must accept the custom PrintTicket and keep the requested paper; sends no job.
+    /// `CRAFT_PRINT_TEST_PRINTER` names the queue; `CRAFT_PRINT_TEST_TICKET_OUTPUT` keeps the
+    /// validated ticket.
+    #[test]
+    #[ignore = "needs CRAFT_PRINT_TEST_PRINTER naming an installed printer; sends no job"]
+    fn an_installed_driver_validates_a_custom_media_ticket() {
+        let name = std::env::var("CRAFT_PRINT_TEST_PRINTER").unwrap();
+        let device = select_device(PrinterDevice::all().unwrap(), Some(&name), None).unwrap();
+        let sheet = (3024.566929133858, 1360.6299212598426);
+        let (ticket, paper) = match driver_ticket(&device, &Job::default(), sheet, true) {
+            Ok(value) => value,
+            Err(e) => panic!("{e}"),
+        };
+        let ticket_xml = std::str::from_utf8(ticket.get_xml()).unwrap();
+        assert!(ticket_xml.contains("PageAutorotate"), "missing PageAutorotate: {ticket_xml}");
+        assert!(ticket_xml.contains("Printer"), "missing Printer autorotate: {ticket_xml}");
+        let expected = (1_067_000, 480_000);
+        assert!(paper == expected || paper == (expected.1, expected.0), "driver paper {paper:?}");
+        if let Ok(path) = std::env::var("CRAFT_PRINT_TEST_TICKET_OUTPUT") {
+            std::fs::write(path, ticket.get_xml()).unwrap();
+        }
+    }
+
     /// A real job, for checking a printer by hand (paper, or a paused queue's spool file):
     /// `CRAFT_PRINT_TEST_FILE` printed on `CRAFT_PRINT_TEST_PRINTER`, shrink-to-fit when
     /// `CRAFT_PRINT_TEST_SCALING=shrink`, two-sided on the long edge when
