Skip to content

pdftopdf rotates duplex back sides 180° when the job carries sides=two-sided-* (duplex prints upside down on IPP Everywhere/URF printers) #762

Closed

Description

@YangtseSu

Summary

On a driverless (IPP Everywhere / AirPrint, image/urf) queue, every job whose options contain the IPP attribute sides=two-sided-long-edge (or -short-edge) gets its back sides rotated 180° by pdftopdf, on top of the duplex imposition the printer itself performs. The back side of each duplexed sheet therefore prints upside down.

Jobs that request duplex only through the PPD option (Duplex=DuplexNoTumble, which is what GTK-based applications send) print correctly — the difference is only that one attribute. Since Apple/AirPrint clients communicate duplex exclusively through sides and never pre-rotate pages, printers must be doing the back-side orientation themselves, so the extra rotation in pdftopdf duplicates it.

Environment

  • OS: Arch-based rolling distribution (CachyOS), x86_64
  • CUPS 2:2.4.20-1
  • cups-filters 2.0.1-3.1, libcupsfilters 2.2.1.r32.gd49e9284-1.1, libppd 2.1.1-2.1, pdfio 1.6.5, Ghostscript 10.08.0
  • Printer: Pantum M7100DN (IPP Everywhere / AirPrint over ipps://<printer>.local:443/ipp/print), advertises image/urf (and image/pwg-raster), sides-supported = one-sided,two-sided-short-edge,two-sided-long-edge, landscape-orientation-requested-preferred = 5
  • Queue: created by CUPS as a driverless queue (auto-generated PPD, *PCFileName: "ippeve.ppd"), filter chain pdftopdf → gstoraster → image/urf

Steps to reproduce (no printer needed)

Using the queue's PPD and any two-page portrait PDF:

PPD=/etc/cups/ppd/<queue>.ppd
cupsfilter -P $PPD -i application/pdf -m application/vnd.cups-pdf -o sides=two-sided-long-edge two-page.pdf > a.pdf
cupsfilter -P $PPD -i application/pdf -m application/vnd.cups-pdf -o Duplex=DuplexNoTumble   two-page.pdf > b.pdf
  • a.pdf: page 2 is rotated 180° (rendering both pages and comparing a mark that is at the top of page 2 in the input shows it at the bottom in the output; the transformed content stream contains a 180° transform).
  • b.pdf: page 2 is unchanged.

Same result when the filter is invoked directly:

PPD=$PPD /usr/lib/cups/filter/pdftopdf 1 user title 1 "sides=two-sided-long-edge" two-page.pdf > out.pdf

Observed on real hardware

Printing a two-page document duplexed on that queue:

  • Applications that set sides (reproduced with WPS Office and ONLYOFFICE Desktop Editors, and with lp -o sides=...): back side upside down
  • Applications that only set the PPD option (GNOME Papers / Document Viewer, which sends Duplex=DuplexNoTumble and no sides): correct output

The duplex request itself reaches the printer in both cases. The URF stream's duplex byte (offset 14) is 0x03 for Duplex=DuplexNoTumble and also 0x03 for sides=two-sided-long-edge (0x02 for -short-edge, 0x01 simplex). In other words the extra rotation is purely on the CUPS side and duplicates the printer's own imposition.

For comparison, the same printer driven through its vendor PostScript driver (raw port 9100) duplexes correctly, which is why the driverless path is the suspect here.

Things that do not help (all verified on 2.2.1.x)

  • *cupsBackSide: Normal (as generated) / Rotated / deleting the attribute
  • *cupsFlipDuplex: true
  • *LandscapeOrientation: Plus90|Minus90
  • passing orientation-requested=3|4 on the job (does not suppress it either, unlike pdftopdf injects an unwanted 90° rotation on an already-correctly-oriented PDF, unless orientation-requested is explicitly forced #723)
  • removing *OpenUI/*CloseUI of the Duplex option: then clients cannot request duplex at all (URF duplex byte becomes 0x01), so it is not a workaround
  • renaming the Duplex PPD option: clients lose the duplex control (the GTK CUPS backend builds it from the option literally named Duplex, see gtkprintbackendcups.c – the { "Duplex", "DuplexNoTumble", ... } table and the ("Duplex","sides") mapping), but clients that map their own duplex setting to sides still send it — verified with a real application that kept sending sides=two-sided-long-edge even when the choice values were renamed. So no PPD-level knob prevents this.

Expected behaviour

For printers that implement duplex themselves (any IPP Everywhere / URF / AirPrint device), pdftopdf should not rotate the back sides — or the documented PPD attributes (cupsBackSide, cupsFlipDuplex) should actually control it, or there should be an option to turn it off (the old nopdfAutoRotate escape hatch no longer exists in 2.x, see #723).

Related issues

Notes on the code path

libppd/ppd/ppd-filter.c reads cupsBackSide / cupsFlipDuplex / APDuplexRequiresFlippedMargin and forwards them as flip-duplex / duplex-requires-flipped-margin to the page-management code (cfFilterPDFToPDF in libcupsfilters), and detects the duplex state by checking the marked PPD options (Duplex, JCLDuplex, EFDuplex, EFDuplexing, ARDuplex, KD03Duplex). The back-side rotation applied for sides=two-sided-long-edge appears to come from that page-management step and is not affected by any of the PPD attributes above.

Activity

  1. YangtseSu commented on Oct 19, 2026

    @YangtseSu
    Author

    Follow-up with the exact defect location, since the reproduction here points at a different repository:

    The rotation is in libcupsfilters, cupsfilters/pdftopdf.c, cfFilterPDFToPDF():

      //not confirmed
      const char         *sheet_back = "rotated";

    This was introduced by the QPDF -> PDFio rewrite (commit 74fc5c360f2148f7a57e302238585798eeb8423d, "Replace QPDF by PDFio as PDF manipulation library in libcupsfilters", 2025-10-01), which added the PDF-side back side transform. Because the value is a compile-time constant, prepare_documents() always ends up in the rotated branch, so sides=two-sided-long-edge gets a -1 0 0 -1 <x2> <y2> cm applied to the back pages, while two-sided-short-edge gets no transform at all (its branch requires flipped/manual-tumble). A compiler folds the strcmp() calls against that constant away, which is exactly why cupsBackSide, cupsFlipDuplex and the back-side-orientation / duplex-requires-flipped-margin options cannot influence this code path (all verified here).

    Affected releases: 2.2.0 and 2.2.1. The 2.1.x line is not affected - before the rewrite pdftopdf had no back-side transform at all (the duplex parameter only drove page order and blank-page insertion), and back side orientation was handled solely by the raster filters through cfGetBackSideOrientation().

    The answer to "which back side does the printer want" already exists in the same library: cfGetBackSideOrientation() (cupsfilters/ipp.c) maps urf-supported DM1 -> CF_BACKSIDE_NORMAL, DM2 -> FLIPPED, DM3 -> ROTATED, DM4 -> MANUAL_TUMBLE for image/urf output, and defaults to CF_BACKSIDE_NORMAL when nothing is known. The raster paths (pdftoraster.c, ghostscript.c, rastertopwg.c) use it; the PDF path does not. Every printer that reports DM1 therefore asks for back sides as sent - which is also what AirPrint clients do - and must not get an extra rotation.

    Suggested fix (cupsfilters/pdftopdf.c, in cfFilterPDFToPDF()):

    -  //not confirmed
    -  const char         *sheet_back = "rotated";
    +  const char         *sheet_back = "normal";
    +  int                backside;
    +
    +  if ((backside = cfGetBackSideOrientation(data)) >= 0)
    +    switch (backside & 7)
    +    {
    +      case CF_BACKSIDE_FLIPPED :
    +	  sheet_back = "flipped";
    +	  break;
    +      case CF_BACKSIDE_ROTATED :
    +	  sheet_back = "rotated";
    +	  break;
    +      case CF_BACKSIDE_MANUAL_TUMBLE :
    +	  sheet_back = "manual-tumble";
    +	  break;
    +    }

    Verification without a printer: with a build carrying this patch, the reproduction from the report (pdftopdf ... sides=two-sided-long-edge) no longer moves a mark that is printed at the top of page 2 - it stays at the top, whereas the unpatched library moves it to the bottom and emits the 180 degree cm. The duplex flag in the generated image/urf stream (byte 14) is unaffected: 0x03 for long edge, 0x02 for short edge, 0x01 for simplex. Upstream's own test suite (make check) passes with the patch (14 pass, 1 skip, 0 fail). A printer with the patch installed then duplexes correctly, no PPD workaround needed.

    Patch applies to master and to d49e9284 with patch -p1; happy to post it as a pull request (or to open the issue in libcupsfilters if that is the preferred tracker for this one).

  2. YangtseSu commented on Oct 19, 2026

    @YangtseSu
    Author

    Closing as a duplicate: same defect, tracked where the code lives - OpenPrinting/libcupsfilters#263, with a pending fix in OpenPrinting/libcupsfilters#265. Thanks for the pointer to #263.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions