Drawing in CATiledLayer with CoreGraphics CGContextDrawImage

GorillaPatch picture GorillaPatch · Nov 1, 2010 · Viewed 8.6k times · Source

I would like to use a CATiledLayer in iPhone OS 3.1.3 and to do so all drawing in -(void)drawLayer:(CALayer *)layer inContext:(CGContext)context has to be done with coregraphics only.

Now I run into the problems of the flipped coordinate system on the iPhone and there are some suggestions how to fix it using transforms:

My problem is that I cannot get it to work. I started using the PhotoScroller sample code and replacing the drawing method with coregraphics calls only. It looks like this

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
CGContextSaveGState(context);
CGRect rect = CGContextGetClipBoundingBox(context);
CGFloat scale = CGContextGetCTM(context).a;
CGContextConcatCTM(context, CGAffineTransformMakeTranslation(0.f, rect.size.height));
CGContextConcatCTM(context, CGAffineTransformMakeScale(1.f, -1.f));

CATiledLayer *tiledLayer = (CATiledLayer *)layer;
CGSize tileSize = tiledLayer.tileSize;


tileSize.width /= scale;
tileSize.height /= scale;
// 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++) {
     //   if (row == 0 ) continue;
        UIImage *tile = [self tileForScale:scale row:row col:col];
        CGImageRef tileRef = [tile CGImage];
        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);
        NSLog(@"row:%d, col:%d, x:%.0f y:%.0f, height:%.0f, width:%.0f", row, col,tileRect.origin.x, tileRect.origin.y, tileRect.size.height, tileRect.size.width);

        //[tile drawInRect:tileRect];
        CGContextDrawImage(context, tileRect, tileRef);
// just to draw the row and column index in the tile and mark the origin of the tile with a red line        
        if (self.annotates) {
            CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor]CGColor]);
            CGContextSetLineWidth(context, 6.0 / scale);
            CGContextStrokeRect(context, tileRect);
            CGContextSetStrokeColorWithColor(context, [[UIColor redColor]CGColor]);
            CGContextMoveToPoint(context, tileRect.origin.x, tileRect.origin.y);
            CGContextAddLineToPoint(context, tileRect.origin.x+100.f, tileRect.origin.y+100.f);
            CGContextStrokePath(context);
            CGContextSetStrokeColorWithColor(context, [[UIColor redColor]CGColor]);
            CGContextSetFillColorWithColor(context, [[UIColor whiteColor]CGColor]);
            CGContextSelectFont(context, "Courier", 128, kCGEncodingMacRoman);
            CGContextSetTextDrawingMode(context, kCGTextFill);
            CGContextSetShouldAntialias(context, true);
            char text[30]; 
            int length = sprintf(text,"row:%d col:%d",row,col);
            CGContextSaveGState(context);
            CGContextShowTextAtPoint(context, tileRect.origin.x+110.f,tileRect.origin.y+100.f, text, length);
            CGContextRestoreGState(context);
        }
    }
}

CGContextRestoreGState(context);

}

As you can see I am using a Scale transform to invert the coordinate system and a translation transform to shift the origin to the lower left corner. The images draw correctly but only the first row of tiles is being drawn. I think there is a problem with the translation operation or the way the coordinates of the tiles are computed.

This is how it looks like:

I am a bit confused with all this transformations.

Bonus question: How would one handle the retina display pictures in core graphics?

EDIT: To get it working on the retina display I just took the original method from the sample code to provide the images:

- (UIImage *)tileForScale:(CGFloat)scale row:(int)row col:(int)col
{
// 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 * 1000), col, row];
NSString *path = [[NSBundle mainBundle] pathForResource:tileName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
return image;
}

In principle the scale of the display is ignored since Core Graphics is working in pixels not points so when asked to draw more pixels, more CATiledLayers (or sublayers) are used to fill the screen.

Thanks alot Thomas

Answer

Jessedc picture Jessedc · Nov 2, 2010

Thomas, I started by following the WWDC 2010 ScrollView talk and there is little or no documentation on working within drawLayer:inContext for iOS 3.x. I had the same issues as you do when I moved the demo code from drawRect: across to drawLayer:inContext:.

Some investigation showed me that within drawLayer:inContext: the size and offset of rect returned from CGContextGetClipBoundingBox(context) is exactly what you want to draw in. Where drawRect: gives you the whole bounds.

Knowing this you can remove the row and column iteration, as well as the CGRect intersection for the edge tiles and just draw to the rect once you've translated the context.

Here's what I've ended up with:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
  CGRect rect = CGContextGetClipBoundingBox(ctx);
  CGFloat scale = CGContextGetCTM(ctx).a;

  CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
  CGSize tileSize = tiledLayer.tileSize;
  tileSize.width /= scale;
  tileSize.height /= scale;

  int col = floorf((CGRectGetMaxX(rect)-1) / tileSize.width);
  int row = floorf((CGRectGetMaxY(rect)-1) / tileSize.height);

  CGImageRef image = [self imageForScale:scale row:row col:col];

  if(NULL != image) {
    CGContextTranslateCTM(ctx, 0.0, rect.size.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    rect = CGContextGetClipBoundingBox(ctx);

    CGContextDrawImage(ctx, rect, image);
    CGImageRelease(image);
  }
}

Notice that rect is redefined after the TranslateCTM and ScaleCTM.

And for reference here is my imageForScale:row:col function:

- (CGImageRef) imageForScale:(CGFloat)scale row:(int)row col:(int)col {
  CGImageRef image = NULL;
  CGDataProviderRef provider = NULL;
  NSString *filename = [NSString stringWithFormat:@"img_name_here%0.0f_%d_%d",ceilf(scale * 100),col,row];
  NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@"png"];

  if(path != nil) {
    NSURL *imageURL = [NSURL fileURLWithPath:path];

    provider = CGDataProviderCreateWithURL((CFURLRef)imageURL);

    image = CGImageCreateWithPNGDataProvider(provider,NULL,FALSE,kCGRenderingIntentDefault); 
    CFRelease(provider);
  }
  return image;
}

There's still a bit of work to be done on these two functions in order to support high resolution graphics properly, but it does look nice on everything but an iPhone 4.