CATiledLayer drawing with incorrect scale

Originator:chris
Number:rdar://8503490 Date Originated:01-Oct-2010 03:38 PM
Status:Open Resolved:
Product:iOS Product Version:4
Classification:Serious Reproducible:Always
 
Summary:

Certain sizes will cause CATiledLayer to ask for its tiles to be drawn with an incorrect scale and rect. The error appears to be caused by loss of accuracy in floating point calculations. For example, with a size of {width = 4509, height = 7282}, the CATiledLayer will provide CTM scales of [0.124861, 0.249945, 0.499889, 1.000000] instead of [0.125, 0.25, 0.5, 1.0]. 

Steps to Reproduce:

1. Set a CATiledLayer's size to something like {4509, 7282}. Many other values cause the same behavior.

Expected Results:

When asked to draw, the CTM's scale be set to a reasonable number like 0.125, 0.25, 0.5, or 1.0.

Actual Results:

When asked to draw, the CTM's scale is a number like 0.124861 instead of 0.125. Additionally, the rect you are asked to draw in is slightly off. This results in the tiles not correctly matching up, usually causing lines (gaps) to appear between rows of tiles.

Regression:

I have tested on iOS 4.0 and 4.1, both versions exhibit this problem. This issue arises based on the size of the layer, particularly with larger sizes.

Notes:

I have made two minimal changes to the PhotoScroller sample code (http://chrisfarber.net/CATiledLayerBug.zip) that will cause it to show this problem. They are:
1. In the ImageData.plist file, I changed the size of the first photo to {width = 4509, height = 7282} and left all other data the same.
2. In -[TilingView drawRect:] I commented the two lines that would load and draw the tile image. Instead, I added two lines to fill the rect that the image would have been drawn into with solid red.

For the first photo that I changed the size of, black lines appear on the screen where there are gaps between the red tiles. The remaining photos whose sizes were not changed do not exhibit this issue as they are being provided correct scales when drawing.

To the best of my ability I have not been able to devise a workaround that fully ameliorates this behavior without side effects.

Comments

Solution

Below is code that solves this problem. This code would go in Apple's PhotoScroller Demo app, in TilingView.m, replacing the two methods: drawRect: and tileForScale:row:col:

The substance of the problem is as has been pointed out that the scale value is slightly below the expected value. The double inversion with an int cast solves that. And the second part of the problem is that the x and y ("a" and "d" values in the affine matrix) scale values are not always both slightly off, so using just the x ("a") value results horizontal banding when the y ("d") value is not the same. The solution is simply to use both the x and y scale values to determine the CGRect to draw in to.

Zephyr

- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext();

// get the scale from the context by getting the current transform matrix, then asking for
// its "a" component, which is one of the two scale components. We need to also ask for the "d" component as it might not be precisely the same as the "a" component, even at the "same" scale.
CGFloat _scaleX = CGContextGetCTM(context).a;
CGFloat _scaleY = CGContextGetCTM(context).d;

CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
CGSize tileSize = tiledLayer.tileSize;

// Even at scales lower than 100%, we are drawing into a rect in the coordinate system of the full
// image. One tile at 50% covers the width (in original image coordinates) of two tiles at 100%. 
// So at 50% we need to stretch our tiles to double the width and height; at 25% we need to stretch 
// them to quadruple the width and height; and so on.
// (Note that this means that we are drawing very blurry images as the scale gets low. At 12.5%, 
// our lowest scale, we are stretching about 6 small tiles to fill the entire original image area. 
// But this is okay, because the big blurry image we're drawing here will be scaled way down before 
// it is displayed.)

tileSize.width /= _scaleX;
tileSize.height /= -_scaleY;

// calculate the rows and columns of tiles that intersect the rect we have been asked to draw
int firstCol = floorf(CGRectGetMinX(rect) / tileSize.width);
int lastCol = floorf((CGRectGetMaxX(rect)-1) / tileSize.width);
int firstRow = floorf(CGRectGetMinY(rect) / tileSize.height);
int lastRow = floorf((CGRectGetMaxY(rect)-1) / tileSize.height);

for (int row = firstRow; row <= lastRow; row++) {
    for (int col = firstCol; col <= lastCol; col++) {
        UIImage *tile = [self tileForScale:_scaleX row:row col:col];
        CGRect tileRect = CGRectMake(tileSize.width * col, tileSize.height * row,
                                     tileSize.width, tileSize.height);

        // if the tile would stick outside of our bounds, we need to truncate it so as to avoid
        // stretching out the partial tiles at the right and bottom edges
        tileRect = CGRectIntersection(self.bounds, tileRect);

        [tile drawInRect:tileRect];

        /// change this to yes to annotate
        if (NO) {
            [[UIColor whiteColor] set];
            CGContextSetLineWidth(context, 6.0 / _scaleX);
            CGContextStrokeRect(context, tileRect);
        }
    }
}

}

- (UIImage *)tileForScale:(CGFloat)_scale row:(int)row col:(int)col { //this accounts for a bug somewhere upstream that returns the scale as a floating point number just below the required value: 0.249... instead of 0.2500

_scale = (1 / (float)((int)(1/scale))) * 1000;  //This gets scale numbers: 1000, 500, 250 etc. which is what Apple uses in PhotoScroller

//_scale = (int)(1/_scale); // this gets scale numbers 1, 2, 4, 8 etc.

 // we use "imageWithContentsOfFile:" instead of "imageNamed:" here because we don't want UIImage to cache our tiles
NSString *tileName = [NSString stringWithFormat:@"%@_%d_%d_%d", imageName, (int)_scale, col, row];
NSString *path = [[NSBundle mainBundle] pathForResource:tileName ofType:imageNameExtension];
UIImage *image = [UIImage imageWithContentsOfFile:path];
return image;

}

By zephyr.renner at July 6, 2011, 11:27 p.m. (reply...)

maybe a solution?

Not sure if you're still working on this. I'm having my own zooming problems setting up CATiledLayer in my app (zoomscales drawing on top of each other during paging.......any ideas?), but I think I might have a fix for this part. I'm still a bit of a noob, so sorry if this isn't 100% right.

Basically, I've set up my tiling view implementation to switch with the inverse zoom factor (1, 2, 4, 8) instead of the scale (1, .5, .25, .125). Taking the inverse of scale creates a float "slightly larger" than what you want (ie 1/.249 = 4.016) and then casting as an int truncates the fractional part according to ANSI C standards. Later on, I use this scale string with the row and column strings to fetch the right tile according to my naming convention ZZZCC_RR.jpg

enum { kZoom100 = 1, kZoom050 = 2, kZoom025 = 4, kZoom125 = 8 };

CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat scale = CGContextGetCTM(context).a;
int tempScaleInverse = 1/scale;

NSString *scaleString = [[[NSString alloc] init] autorelease];

switch (tempScaleInverse) {
    case kZoom100:
        scaleString = [NSString stringWithFormat:@"_100"];
        break;
    case kZoom050:
        scaleString = [NSString stringWithFormat:@"_050"];
        break;
    case kZoom025:
        scaleString = [NSString stringWithFormat:@"_025"];
        break;
    case kZoom125:
        scaleString = [NSString stringWithFormat:@"_125"];
        break;                  
    default:
        NSLog(@"bad zoom scale passed.");
}
NSLog(@"drawRect will ask for tile at zoom scale: %@", scaleString);

Hope this helps, Nick

By norenstein at March 16, 2011, 7:17 p.m. (reply...)

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!