YYImage's Wrong Row Stride Turns EXIF-Rotated JPEGs into Stripes on iOS
Deep understanding instead of rote memory
The problem: original image is normal, app's large image is distorted
A user posts a landscape JPEG. Downloading the original or opening it in a browser shows completely normal content. But the iOS app's large-image page displays horizontal stripes and stretching.
The sample's raw pixels are 4000 × 3000, with EXIF Orientation 6 (Right). That means the pixel data is stored horizontally and must be rotated 90° to the right before display.
Therefore the problem is not in the CDN, the download process, or the upload result. It lies in how the client decodes and normalizes the orientation of this JPEG that needs rotation.
What actually happens: src, tmp, dest
How does YYImage rotate images?
Here we use a downscaled photo made up of parts like plants, a cup, an apple, and a phone as an example.
Each grid cell represents a region of the photo: after rotation, the content has been righted in tmp; dest contains the same photo content, but it is another block of memory vImage allocates for the final display format. YYImage eventually converts this block into a usable CGImage.
Why is tmp needed? Rotation cannot read and write in place on the original image memory: a newly written position may overwrite original pixels that have not yet been read. So tmp is needed as the new rotation canvas.
Why is dest needed? tmp uses the ARGB format that is convenient for rotation functions; if the final display needs a different byte order or alpha format, vImage creates dest for format conversion. If the formats match, the code returns tmpImage directly and does not create dest.
How the error becomes stripes
destis avImage_Bufferproduced by vImage. To make the next row start at a memory address more suitable for CPU/SIMD reads, vImage may pad the bytes actually occupied by each row to 16, 32, or another alignment boundary.- The gray part in the image below is that alignment padding: it takes up memory but is not image pixels, and normally does not show.
The error happens when dest is converted to a
CGImage(insideYYImageCoder.m'sYYCGImageCreateAffineTransformCopymethod).When creating the
CGImage, it mistakenly usesdestBytesPerRowinstead ofdest.rowBytes. This makes theCGImageinterpret the samedest.datawith the wrong row stride: the first row is usually fine, but from the second row onward it reads pixels from wrong offsets, eventually drawing stripes/stretching.
The image below shows the error process:
// Here dest is reallocated by vImageBuffer_Init inside the helper
YYCGImageDecodeToBitmapBufferWith32BitFormat(tmpImage, &dest, destBitmapInfo);
// Wrong: actual row stride of dest.data should be dest.rowBytes
destImage = CGImageCreate(destWidth, destHeight, 8, 32,
destBytesPerRow, ...);
Why does YYImage use the wrong destBytesPerRow? destBytesPerRow is what YYImage derives from the width; it is the number of bytes it assumes each row of the final dest should have, not the actual byte count.
How to fix: use vImage's actual row stride
Only two spots need to change: the provider's total byte count and the CGImage's bytes-per-row should both use dest.rowBytes.
// dest.rowBytes is the actual stride of vImage's output buffer
destProvider = CGDataProviderCreateWithData(dest.data, dest.data,
dest.height * dest.rowBytes,
YYCGDataProviderReleaseDataCallback);
destImage = CGImageCreate(destWidth, destHeight, 8, 32,
dest.rowBytes, // consistent with dest.data's real memory layout
YYCGColorSpaceGetDeviceRGB(), destBitmapInfo,
destProvider, NULL, false,
kCGRenderingIntentDefault);
This is a root-cause fix at the component level: no need for business logic to disable EXIF, and no need to redraw and re-encode each image first. Every path that goes through this rotation function will use the correct memory layout.
Which scenarios reproduce it more easily
| Feature | Why it is worth prioritizing |
|---|---|
| JPEG whose EXIF Orientation is not Up | Orientation rotation is needed to enter this code; Right (6) and Left (8) are especially typical. |
| Landscape photos from Android / Samsung phones | The sample in this bug is one such case. Not Samsung-specific, but landscape JPEGs from these sources are more likely to carry EXIF metadata that requires rotation. |
| Original and browser render fine, only the iOS app is broken | The file content is most likely fine; check first whether the app lets YYImage perform orientation normalization. |
| Issue appears intermittently rather than every time | Whether the actual stride differs from the expected stride depends on image size, pixel format, system, and memory alignment; this is why the problem seems rare. |
Extra: YYImage Issue #131 reports garbled output after orientation correction, and it ultimately reaches this same rotation function; that issue's workaround manually rotated the image first to avoid the problem, whereas this fix corrects the underlying row stride.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
[赞]