1

I am building an app that overlays views drawn with code (output from PaintCode) onto photos. I have added gesture recognizers to rotate and scale the views drawn with code.

There is some mild pixelation on the views drawn on top. If I do any rotation or scale the image larger (even a slight bit), there is a lot more pixelation.

Here is a comparison of the images:

No rotating or scaling: No rotation or scaling

A small amount of rotation/scaling: A small amount of rotation/scaling

Here is the UIView extension I'm using to output the composited view:

extension UIView {

    func printViewToImage() -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 2.0

        let renderer = UIGraphicsImageRenderer(bounds: self.bounds, format: format)
        return renderer.image { rendererContext in
            self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        }
    }
}

Even if I set the scale to something like 4.0, there is no difference.

Here is the code I'm using for the scale/rotation gesture recognizers:

@IBAction func handlePinch(recognizer: UIPinchGestureRecognizer) {
    guard let view = recognizer.view else {
        return
    }

    view.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
    recognizer.scale = 1
}

@IBAction func handleRotate(recognizer: UIRotationGestureRecognizer) {
    guard let view = recognizer.view else {
        return
    }

    view.transform = view.transform.rotated(by: recognizer.rotation)
    recognizer.rotation = 0
}

I have experimented with making the canvasses very large in PaintCode (3000x3000), and there is no difference, so I don't think it has to do with that.

How can I draw/export these views so that they are not pixelated?

Edit: Here's what some of the drawing code looks like...

public dynamic class func drawCelebrateDiversity(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 3000, height: 3000), resizing: ResizingBehavior = .aspectFit, color: UIColor = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)) {
    //// General Declarations
    let context = UIGraphicsGetCurrentContext()!

    //// Resize to Target Frame
    context.saveGState()
    let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 3000, height: 3000), target: targetFrame)
    context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
    context.scaleBy(x: resizedFrame.width / 3000, y: resizedFrame.height / 3000)


    //// Bezier 13 Drawing
    let bezier13Path = UIBezierPath()
    bezier13Path.move(to: CGPoint(x: 2915.18, y: 2146.51))
    bezier13Path.addCurve(to: CGPoint(x: 2925.95, y: 2152.38), controlPoint1: CGPoint(x: 2919.93, y: 2147.45), controlPoint2: CGPoint(x: 2924.05, y: 2147.91))
aaronfalls
  • 201
  • 1
  • 13
  • Is there an approach with PaintCode that would allow you to avoid rasterization entirely? – Chris Droukas Mar 23 '17 at 14:09
  • Hey, just a short question, as you have a static scale defined (set to 2). Do you take the [UIScreen mainScreen].scale into consideration for this? I think your raster does not fit to the retina resolution? Just a rough guess, tho. – Lepidopteron Mar 23 '17 at 14:11
  • @ChrisDroukas As I understand it, PaintCode uses coordinates for drawing, but is not rasterized. Adding a bit of a drawing function above... – aaronfalls Mar 23 '17 at 14:12
  • @Lepidopteron If I set the scale to 4.0, for example, the output is larger, but the pixelation looks the same. – aaronfalls Mar 23 '17 at 14:15
  • You need to scale the bounds of your UIGraphicsImageRenderer. Probably times the screen scale – Leo Dabus Mar 23 '17 at 14:18
  • Have you tried to rasterize the view's layer? view.layer.shouldRasterize = true – Lepidopteron Mar 23 '17 at 14:28
  • @LeoDabus Thanks for the reply. In the beginning of the UIView extension, I am creating the UIGraphicsImageRendererFormat, setting that scale to 2, and passing that into the renderer. I just tried setting the scale to 6, and the pixelation is the same. Is there a different way to scale the bounds of the UIGraphicsImageRenderer? – aaronfalls Mar 23 '17 at 14:28
  • @Lepidopteron Thanks, I tried that (setting it in viewDidLoad), but the results are not less pixelated. – aaronfalls Mar 23 '17 at 14:35
  • Do you use somewhere "UIGraphicsBeginImageContext* instead of "UIGraphicsBeginImageContextWithOptions" with correct options? Because this would result in a scale factor of 1 instead of the factor being adjusted to the scale of the display. Have a detailed look here: http://stackoverflow.com/questions/4334233/how-to-capture-uiview-to-uiimage-without-loss-of-quality-on-retina-display – Lepidopteron Mar 23 '17 at 14:39
  • @Lepidopteron I just searched in the project, and I am not using UIGraphicsBeginImageContext anywhere. – aaronfalls Mar 23 '17 at 14:48
  • The pixelation is visible in live views or only after creating the image? Do you use drawing method or image method from PaintCode? Are you using `shouldRasterize` on any of those views? – Tricertops Mar 24 '17 at 14:37
  • And just an obvious check: Is the overlay created in PaintCode using image or using bezier paths? – Tricertops Mar 24 '17 at 14:40
  • @Tricertops The overlay is created using bezier paths (the drawing methods from the PaintCode output, not images). I am not calling shouldRasterize, should I be calling it on any of them? – aaronfalls Mar 24 '17 at 14:59
  • @aaronfalls No, don’t enable rasterization, but it has it’s own rasterizationScale that could be set wrong. I think I know what’s the issue, I’m going to write an answer. – Tricertops Mar 24 '17 at 15:07

1 Answers1

1

When scaling UIViews (or custom CALayers), you should set their contentsScale to match the desired density of their content. UIViews set their layer contentsScale to screen scale (2 on retina), and you need to multiply this with the extra scale you do via transform.

view.layer.contentsScale = UIScreen.main.scale * gesture.scale;

Even if the drawing code is resolution independent, everything on screen must be converted to bitmap at some time. UIView allocates bitmap with size of bounds.size * contentsScale and then invokes -drawRect:/draw(_ rect:) method.

It is important to set contentsScale on that view that draws, even if that view is not scaled (but some of its parent is). A common solution is to recursively set contentsScale on all sublayers of the scaled view.

– PaintCode Support

Tricertops
  • 8,492
  • 1
  • 39
  • 41
  • Thanks for the reply! For some reason, `view.layer.contentsScale = UIScreen.main.scale * recognizer.scale` did not work for me in the gesture recognizer function. Based on your explanation, I experimented a bit, and `view.contentScaleFactor = view.contentScaleFactor * recognizer.scale` works perfectly—crisp no matter how scaled it is. I had also tried using `view.contentScaleFactor = UIScreen.main.scale * recognizer.scale`, but that did not work. – aaronfalls Mar 24 '17 at 19:44
  • @aaronfalls That’s probably because you reset the scale value after every change using `recognizer.scale = 1`. I wouldn’t do that. – Tricertops May 03 '17 at 07:05