PicTapGo’s crop tool is a unique beast. The tool I envisioned was inspired by Lightroom’s crop/rotate tool, but needed to work within the limitations of a mobile / touch device interface. The requirements for the this feature were essentially:
- Allow the user to simultaneously manipulate rotation and crop;
- Behave the way the user expects (i.e. if you think it should work a certain way, then it will);
- Automatically adjust the image to ensure the crop rectangle is entirely contained within the the image canvas (no “fill pixels” at the edges or corners);
- Work non-destructively;
- Maximize the displayed size of the image; and
- Provide continuous controls, as well as discrete ones (i.e. sliders and buttons) to facilitate quick and fine adjustments, and to increase precision.
The resulting solution is, I think, still the best crop and rotate tool around, period. There are a lot of subtle bits that make it an intuitive, easy, powerful tool.
The actual pixel-pushing, both for display and final renders, is handled by applying a CGAffineTransform to the source image. No surprises there. The two big problems are knowing whether two boxes, one of which may be rotated, scaled, or translated, intersects the other, and if so, how to adjust the transformation so it fits, while preserving the user’s expectations as they manipulate the UI controls. The basic use case for this is the case where the user want to rotate their image. In order to ensure we don’t have any “fill” pixels, the crop rectangle needs to shrink relative to the image. The idea is to find the minimum change in scale that would allow the crop rectangle to be completely contained within the image.
A similar case is where the user wants to translate their crop rectangle – we need to keep the crop within the bounds of the image we’re cropping into.
Conceptually, this means we have the crop rectangle the user wants, and then we have the crop rectangle the user is allowed to have, such that the resulting crop rectangle meets the “no fill pixels” criteria. My solution was to create some basic geometry helper classes for bounds checking, and a “TRTransform” class that would act as the model object, while abstracting away the calculation of the actual CGAffineTransforms we need for rendering images. You tell TRTransform you want to rotate the image by N degrees, and translate the image by some amount (for instance). TRTransform then bounds-checks its property changes to keep everything sane, and produces a CGAffineTransform appropriate for display or rendering. If you don’t explicitly request a scale change by zooming the image, then the resulting CGAffineTransform will only be scaled to the minimum necessary to create a valid crop.
Note that another complicating factor here is that in the UI, we’re creating a CGAffineTransform that enlarges the image relative to its original size. This allows for our “image-under” UI paradigm, which keeps the largest possible screen real estate for the crop rectangle itself. However, when we’re applying the CGAffineTransform to create a rendered image (say, for final output), we actually want to shrink the crop rectangle. A user that crops into an image should have an image with smaller absolute pixel dimensions, otherwise we’re just increasing the size of the file with no increase in image quality (or actually, I’d argue, a degradation of image quality due to needless resampling). So the resulting CGAffineTransform is different for the UI vs the final render (and furthermore, the UI transforms depend on the size of the presented UI, as any translation values are relative to screen size).
Additionally, there’s some fun custom UI happening in the rotate screen, which does its own drawing for the rotation scale. In hindsight, this should have been a UIScrollView, but it’s performant enough to redraw it all for each display update with Core Graphics in the drawRect: call, so I’ve never had good reason to redesign it (the neat fade on the left and right means that it never would have been straightforward to do a UIScrollView anyway).
This took about a month of development time, all told, including implementing the UI, and integrating all the crop code into the app’s rendering engine and document model classes. Back before I took over development of PicTapGo, one of my engineers took a stab at this same problem, and we ultimately had to ship without the rotation feature, as he wasn’t able to get it all quite right. I’m proud to say that I was able to deliver the feature in the same time, but prettier, feature-complete, and better architected (I ultimately tossed out all the existing crop / rotate code and rolled this from scratch).
Some code highlights are available on GitHub here.