1
votes

I'm trying to draw a NSImage proportionally scaled and rotated 90 degrees but keep getting a clipped resulting image.

In the NSView subclass, Here's what I currently do:

- (void) drawRect:(NSRect)dirtyRect
{
    [super drawRect:dirtyRect];

    NSImage* image = /* get image */;

    CGRect drawRect = dirtyRect;
    CGRect imageRect = [self proportionallyScale:image.size toSize:drawRect.size];

    NSAffineTransform* rotation = [[NSAffineTransform alloc] init];
    [rotation translateXBy:NSWidth(drawRect) / 2 yBy:NSHeight(drawRect) / 2];
    [rotation rotateByDegrees:90];
    [rotation translateXBy:-NSWidth(drawRect) / 2 yBy:-NSHeight(drawRect) / 2];

    NSGraphicsContext* context = [NSGraphicsContext currentContext];
    [context saveGraphicsState];
    [rotation concat];
    [image drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
    [context restoreGraphicsState];
}

And the scaling logic:

- (CGRect) proportionallyScale:(CGSize)fromSize toSize:(CGSize)toSize
{
    CGPoint origin = CGPointZero;
    CGFloat width = fromSize.width, height = fromSize.height;
    CGFloat targetWidth = toSize.width, targetHeight = toSize.height;

    float widthFactor = targetWidth / width;
    float heightFactor = targetHeight / height;

    CGFloat scaleFactor = std::min(widthFactor, heightFactor);

    CGFloat scaledWidth = width * scaleFactor;
    CGFloat scaledHeight = height * scaleFactor;

    if (widthFactor < heightFactor)
        origin.y = (targetHeight - scaledHeight) / 2.0;
    else if (widthFactor > heightFactor)
        origin.x = (targetWidth - scaledWidth) / 2.0;
    return {origin, {scaledWidth, scaledHeight}};
}

Here's the result with no rotation applied (the image is not square, the red background is rendered to show where the view is):

No rotation applied

And when I apply the transform by enabling [rotation concat]:

Current code with 90 deg rotation applied

The problem being that the scaling of the image and its offset should be computed relative to the rotated viewport size and not the input view bounds. However I can't seem to come up with the logic that computes this correctly.

Can anyone please help with the logic that properly computes the correct image drawing rect for the scaled and rotated image? Or is there a better approach to the whole problem?

1

1 Answers

4
votes

Short Version

Add:

imageRect.origin.x += NSWidth(drawRect) / 2 - NSHeight(drawRect) / 2;
imageRect.origin.y += NSHeight(drawRect) / 2 - NSWidth(drawRect) / 2;

after the call to proportionallyScale in drawRect

Long Version

I took this in two steps: bounding and scaling the image, and then repositioning it within viewport. You can probably combine them if you want to get all fancy. I think this should work for various view sizes.

Let this be our original view:

********************
*                  *
*       View       * 6
*                  *
*                  *
X*******************
        20

The origin in the lower left is marked X.

Let's say the image is 6 x 8 (before being rotated).

The first part is easy, determine how big we should draw the image when its rotated. Just pass in flipped dimensions to proportionallyScale:

CGRect imageRect = [self
    proportionallyScale:image.size
    toSize:CGSizeMake(
         drawRect.size.height, // 6
         drawRect.size.width)  // 20
];

This results in a rect:

imageRect = { origin=(x=0, y=6) size=(width=6, height=8) };

But this doesn't make much sense at the moment:

  6
$$$$$$ 
$    $
$    $
$ IM $ 8
$    $
$    $
$    $
X$$$$$**************
*                  *
*       View       * 6
*                  *
*                  *
X*******************
         20

Then you perform the clockwise 90 degree rotation of the view about the center:

                        8
               X++++X$$$$$$$$       -
               +    +       $       |
Rot'd View >-- +    $  IM   $       |
               +    +       $ 6     | 7
               +    $       $       |
               +    +$$$$$$$$       |
        *******+****+*******        -
        * VP   +    +      *        |
        *      +    +      *        | 6
        *      +    +      *        |
        *      +    +      *        |
        X******+****+*******        -
               +    +               |
               +    +               |
               +    +               |
               +    +               | 7
               +    +               |
               ++++++               -

        |------|----|------|
           7     6     7

VP is the original view's location. We need to transform the origin from rotated space to the original viewport space.

This transform can be calculated as:

// note that drawRect hasn't been flipped here
CGFloat transX = NSWidth(drawRect) / 2 - NSHeight(drawRect) / 2 == 7;
CGFloat transY = NSHeight(drawRect) / 2 - NSWidth(drawRect) / 2 == -7

So we apply the transform to the current image:

imageRect.origin.x += transX;
imageRect.origin.y += transY;

This transform produces:

               X++++X
               +    +
               +    +
               +    +
               +    +
               +    +
        ******$+$$$$+$******
        * VP  $+    +$     *
        *     $+    +$     *
        *     $+    +$     *
        *     $+    +$     * 
        X*****$+$$$$+$******
               +    +
               +    +
               +    + 
               +    +
               +    +
               ++++++

And now everything is nicely positioned and scaled:

           6     8      6
        |-----|------|-----|

        ******$$$$$$$$******  -
        *     $      $     *  |
        *     $      $     *  | 6
        *     $      $     *  |
        *     $      $     *  |
        X*****$$$$$$$$******  -

at least until the next Vile Force of Darkness arrives...