objective-cxcodemacosnsprintoperationnsprintpanel

Different page orientations in default print preview in NSPrintPanel


I want to print a document which is consists of multiple pages with different orientations. Let's say every odd page is in portrait orientation and every even page is in landscape orientation. Below is the code for my PrintView class:

// PrintView.h
@interface PrintView : NSView
{
    NSPrintInfo* printInfo;
    int nPages;
}
- (id)initWithParams:(CGRect)frame pages:(int)pagesCount;
- (void)setPrintInfo:(NSPrintInfo *)pi;
@end
// PrintView.m
#import "PrintView.h"

@implementation PrintView
- (id)initWithParams:(CGRect)frame pages:(int)pagesCount
{
    self = [super initWithFrame:frame];
    if (self)
    {
        nPages = pagesCount;
    }
    return self;
}

- (void)setPrintInfo:(NSPrintInfo *)pi
{
    if (printInfo != pi)
    {
        printInfo = pi;
    }
}

// print support

- (BOOL)knowsPageRange:(NSRangePointer)range
{
    range->location = 1;
    range->length   = nPages;

    return YES;
}

- (NSRect)rectForPage:(NSInteger)page
{
    if (page % 2)
        [printInfo setOrientation:NSPaperOrientationPortrait];
    else
        [printInfo setOrientation:NSPaperOrientationLandscape];

    NSRect r = NSMakeRect(0, 0, printInfo.paperSize.width, printInfo.paperSize.height);
    [self setFrame:r];
    return r;
}
@end

This code simply produces blank pages without any drawings. Page orientations are applied in rectForPage:.

When I run print operation (more details in function onBtnClick in MRE below) in built-in preview I see that all pages are shown in portrait orientation:

Print window

though actually the second page should be in landscape orientation. Moreover, if I print this to PDF I can see that in PDF all page orientations are correct:

enter image description here

So the problem is in built-in Cocoa preview as I think. If the last page is in landscape orientation then all pages in preview will be shown in landscape orientation too. Seems that only last page orientation is applied to preview.

Is there any workarounds for showing pages with different orientations in default Cocoa printing preview? Or am I doing something wrong?


Minimal Reproducible Example

Below is the code for simple application with just one button labeled "Print". Pressing on that button starts printing process. You can run and compile it in Xcode (I use Xcode version 15.2). Files PrintView.m and PrintView.h are listed above so here are the rest:

// main.m
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"

int main(int argc, const char *argv[])
{
    @autoreleasepool
    {
        AppDelegate *delegate = [[AppDelegate alloc] init];
        NSApplication *app = [NSApplication sharedApplication];
        app.delegate = delegate;

        [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
        [NSApp activateIgnoringOtherApps:YES];

        NSApplicationMain(argc, argv);
    }
    return 0;
}
// AppDelegate.h
#import <Cocoa/Cocoa.h>
#import "ViewController.h"

@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) NSWindow* window;
@property (nonatomic, strong) NSViewController* controller;
@end
// AppDelegate.m
#import "AppDelegate.h"

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    int mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
    _window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 200, 200) styleMask:mask backing:2 defer:NO];
    _controller = [[ViewController alloc] init];

    [_window.contentView addSubview:_controller.view];

    [_window makeKeyAndOrderFront:nil];
}

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
    return YES;
}
@end
// ViewController.h
#import <Cocoa/Cocoa.h>

@interface ViewController : NSViewController
@property (nonatomic, strong) NSButton* button;
-(void) onBtnClick;
@end
// ViewController.m
#import "ViewController.h"
#import "PrintView.h"

@implementation ViewController

- (void)loadView
{
    self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    _button = [NSButton buttonWithTitle:@"Print" target:self action:@selector(onBtnClick)];
    [_button setFrame:NSMakeRect(50, 50, 100, 100)];

    [self.view addSubview:_button];
}

- (void)onBtnClick
{
    int nPages = 3;     // here you can change the number of page being printed

    NSLog(@"printint %d pages...", nPages);

    PrintView* pView = [[PrintView alloc] initWithParams:NSMakeRect(0, 0, 100, 100) pages:nPages];

    NSPrintInfo* printInfo = [NSPrintInfo sharedPrintInfo];

    NSPrintPanelOptions options = NSPrintPanelShowsCopies;
    options |= NSPrintPanelShowsPageRange;
    options |= NSPrintPanelShowsPaperSize;
    options |= NSPrintPanelShowsPrintSelection;
    options |= NSPrintPanelShowsPreview;

    NSPrintOperation* pro;
    if (printInfo)
        pro = [NSPrintOperation printOperationWithView:pView printInfo:printInfo];
    else
        pro = [NSPrintOperation printOperationWithView:pView];

    [pView setPrintInfo:[pro printInfo]];

    [pro setShowsPrintPanel:YES];
    [[pro printPanel] setOptions:options];
    [pro runOperation];
}

@end

Solution

  • After doing some research I have figured out that there is actually no way to apply landscape orientation only to some pages while leaving other pages as portrait in default printing preview. One either can apply landscape orientation or portrait orientation to ALL pages.


    So I have solved my problem by rotating content inside of pages instead. That it is the way Preview.app works, for example.

    You can distinguish between printing to preview and printing to file using printInfo.jobDisposition which is NSPrintSpoolJob when printing to a screen or an actual printer and NSPrintSaveJob when saving to PDF. That way you can save your actual page orientations while printing to PDF.

    Here is modified rectForPage: function:

    - (NSRect)rectForPage:(NSInteger)page
    {
        currentPage = (int)page - 1;
    
        if (printInfo.jobDisposition == NSPrintSaveJob)
        {
            // apply my page orientation only when saving to file
            [printInfo setOrientation:[self getCurrentPageOrientation]];
        }
    
        NSRect r = NSMakeRect(0, 0, printInfo.paperSize.width, printInfo.paperSize.height);
        [self setFrame:r];
        return r;
    }
    

    Function getCurrentPageOrientation simply return 0 for odd pages and 1 for even pages as before.

    And here is overriden drawRect: function:

    - (void)drawRect:(NSRect)dirtyRect
    {
        CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] CGContext];
    
        if (printInfo.jobDisposition == NSPrintSpoolJob && (int)printInfo.orientation != [self getCurrentPageOrientation])
        {
            // rotate content only when drawing on preview AND current page is in wrong orientation
            CGAffineTransform transform = CGAffineTransformMake(0, -1, 1, 0, 0, dirtyRect.size.height);
            CGContextConcatCTM(context, transform);
        }
    
        // rect sizes
        int rectW = 200;
        int rectH = 100;
        // bottom left rectangle
        CGRect rect1 = NSMakeRect(0, 0, rectW, rectH);
        CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextFillRect(context, rect1);
    }
    

    I draw a red rectangle 100 x 200 at the left bottom corner of every page. For every page that is in wrong orientation I apply transform matrix to rotate it by 90 degrees clockwise (like in Preview.app) and move it so it fits in view rect.


    Here the screenshot of how print preview looks now: print preview in portrait orientation

    And since I've added NSPrintPanelShowsOrientation to NSPrintPanelOptions you can even see how it looks in landscape orientation: enter image description here

    And if I print it to PDF I get all pages in specified orientation: enter image description here


    It's really weird and unintuitive that Apple didn't add functionality of rotating some pages in default print preview. Hope they'll add it sometime. And I also hope this answer help those who are facing similar problem.