I am trying to generate HEIF output files but get strange diagonal lines when I do so. None of the other output formats exhibit these strange lines. See the image below. The problem appears to be cropping but the errors only show in the HEIF output files.

I can replicate this with the following playground code which you can download from here https://duncangroenewald.com/files/SampleApps/CoreImageHEIFExporterBug-2.zip

I have a bug report with Apple and they mentioned "sub-pixel errors" - I have no idea what that is or why these would manifest only with the heif output format or how to prevent them - any ideas or does this just seem like a bug in the Apple code?

EDIT. To be clear this is not a playground issue since the problem occurs in a macOS application.

I suspect this must be a CIContext bug when generating HEIF output since it only manifests with heif output files. However I know nothing about sub-pixel errors or why Apple think they should be relevant for this issue. Perhaps someone with more Core Image knowledge might be able to suggest some possible solutions.

import Cocoa

let files = ["/Users/duncangroenewald/Development/Playgrounds/CoreImageHEIFExporterBug/DSC02022.ARW",
     "/Users/duncangroenewald/Development/Playgrounds/CoreImageHEIFExporterBug/DSC02018.ARW"
]

let destFilePath = "/Users/duncangroenewald/Development/Playgrounds/CoreImageHEIFExporterBug"

let destUrl = URL(fileURLWithPath: destFilePath)

let exporter = Exporter()

var crops = [CGRect(x: 0, y: 0, width: 5310, height: 3212),
             CGRect(x: 0, y: 0, width: 5311, height: 3212),
             CGRect(x: 0, y: 0, width: 5312, height: 3212),
             CGRect(x: 0, y: 0, width: 5313, height: 3212)]


for file in files {
    exporter.processRAWFile(file, destPath: destFilePath, crops: crops)
}

print("Done!")

import AppKit
import CoreImage
import CoreImage.CIFilterBuiltins
import OSLog

public class Exporter {
    
    var formats: [String] = ["heif", "png"]
    
    // Get the CIContext
    let ciContext = CIContext()
    
    var imageOrientation: Int = 0
    var imageNativeSize: CGSize = .zero

    var crops: [CGRect] = [.zero]

    public init() {
        
    }
    
    public func processRAWFile(_ path: String, destPath: String, crops: [CGRect]) {
        
        print("processRAWFile XXX")
        self.crops = crops
        
        let url = URL(fileURLWithPath: path)
        
        let filename = url.deletingPathExtension().lastPathComponent
        
        let destDirUrl = URL(fileURLWithPath: destPath)
        print("destDirUrl: \(destDirUrl)")
        
        for crop in crops {
        
            let cropName = "\(filename)_\(crop.width)x\(crop.height)_\(useDefaultRAW ? "default" : "")"
        
            for format in formats {
            let destUrl = destDirUrl.appendingPathComponent(cropName).appendingPathExtension(format)
            print("destFile: \(destUrl)")
        
                self.exportFile(url: url, destUrl: destUrl, crop: crop, format: format)
                
            }
          
        }
    }
    
    func exportFile(url: URL, destUrl: URL, crop: CGRect, format: String){
        print("exportFile \(url)")
        print("destFile: \(destUrl)")
        
        guard let rawFilter = CIFilter(imageURL: url, options: nil), let outputImage = rawFilter.outputImage else {
            print("Failed to load \(url.lastPathComponent) for exporting")
            return
        }
        
        let processImage = self.crop(outputImage, rect: crop)
        
        
        let options = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0 as CGFloat]
        let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
        let imgFormat = CIFormat.RGBA16
        
        do {
            
            switch format {
            case "tiff":
                try ciContext.writeTIFFRepresentation(of: processImage, to: destUrl, format: imgFormat, colorSpace: colorSpace, options: options)
            case "jpeg":
                try ciContext.writeJPEGRepresentation(of: processImage, to: destUrl, colorSpace: colorSpace, options: options)
            case "png":
                try ciContext.writePNGRepresentation(of: processImage, to: destUrl, format: imgFormat, colorSpace: colorSpace, options: options)
            case "heif":
                try ciContext.writeHEIFRepresentation(of: processImage, to: destUrl, format: imgFormat, colorSpace: colorSpace, options: options)
            default:
                try ciContext.writePNGRepresentation(of: processImage, to: destUrl, format: imgFormat, colorSpace: colorSpace, options: options)
            }
            
        } catch {
            print("Error exporting \(format): \(error.localizedDescription)")
        }
        
    }
    var useDefaultRAW: Bool = true

    func crop(_ ciImage: CIImage, rect: CGRect)->CIImage {
        if let cropped = self.cropFilter(ciImage, rect: rect) {
            return cropped
        } else {
            print("Error cropping image !")
            return ciImage
        }
    }
  
    
    func cropFilter(_ input: CIImage, rect: CGRect) -> CIImage? {
        
        
        // Getting a dashed border !!
        guard let cropFilter = CIFilter(name: "CICrop") else {
            print("Error no CICrop filter found !")
            return input
        }
        let ciVect = CIVector(cgRect: rect)
        cropFilter.setValue(input, forKey: kCIInputImageKey)

        cropFilter.setValue(ciVect, forKey: "inputRectangle")

        return cropFilter.value(forKey: kCIOutputImageKey) as? CIImage
        
    }
}

enter image description here

1

There are 1 best solutions below

1
Ilya Myakotin On

The only way I could find to work it around was to render CGImage through the context first and then re-create CIImage from CGImage.

EDIT

let context = CIContext()
let fileURL = <URL to save location>
let ciImage = <Here is the image you got as output of filter>
let options = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0 as CGFloat]
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
let imgFormat = CIFormat.RGBA16

// First we create a CGImage
let cgImage = context.createCGImage(ciImage, 
                                    from: ciImage.extent,
                                    format: imgFormat,
                                    colorSpace: colorSpace)

// Then we use this CGImage to re-create CIImage and save it
try context.writeHEIFRepresentation(
                of: CIImage(cgImage: cgImage),
                to: fileURL,
                format: imgFormat,
                colorSpace: colorSpace,
                options: options
            )