Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve animated webp detection #1510

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,49 @@ import SDWebImageSwiftUI
import SDWebImageWebPCoder
import UIKit

/// Raw values of "ANMF", which we can use to identify whether a webp is animated or not without decoding it
/// https://stackoverflow.com/questions/45190469/how-to-identify-whether-webp-image-is-static-or-animated
let animatedWebpHeader: [UInt8] = [65, 78, 77, 70]

/// Custom Nuke decoder that processes the image using SDWebImage. The resulting ImageContainer will have the following properties:
/// `image`: the first frame of the decoded webp
/// `type`: `.webp`
/// `data`: the raw webp data if the webp is animated, nil otherwise
struct NukeWebpBridgeDecoder: ImageDecoding {
public init?(context: ImageDecodingContext) {
guard let type = AssetType(context.data), type == .webp else { return nil }
guard
let type = AssetType(context.data),
type == .webp,
context.data.isAnimatedWebp() // only use this for animated webp, fall back on Nuke default for non-animated
else { return nil }
}

func decode(_ data: Data) throws -> ImageContainer {
// decode the first frame to use as thumbnail
let decoded = SDImageWebPCoder().decodedImage(with: data, options: [.decodeFirstFrameOnly: true])

// use magic numbers to check if animated
let animated = data.contains(animatedWebpHeader)

if let ret = decoded?.cgImage {
return .init(image: .init(cgImage: ret), type: .webp, data: animated ? data : nil)
return .init(image: .init(cgImage: ret), type: .webp, data: data)
} else {
return .init(image: UIImage.blank)
}
}
}

/// Raw values of "ANIM", which we can use to identify whether a webp is animated or not without decoding it
/// https://stackoverflow.com/questions/45190469/how-to-identify-whether-webp-image-is-static-or-animated
private let animHeader: Data = Data([65, 78, 73, 77])

private extension Data {
/// If the given data is a webp, returns true if that webp is animated and false otherwise.
/// - Warning: This function's behavior is undefined if the provided data is not an animated webp
func isAnimatedWebp() -> Bool {
// This function is built to run fast, banking on the fact that it's being passed in after Nuke checks that
// the image is a webp to guarantee safety. The check itself therefore only targets the bytes that, if the
// data is a webp, indicate an animated webp; it is assumed that the data is long enough and correctly formatted.

// Sanity checks that the data conforms to the webp spec
assert(self.count >= 33, "Invalid data (too short)")
assert(self[..<4] == Data([82, 73, 70, 70]), "Invalid data (no RIFF header)")
assert(self[8..<12] == Data([87, 69, 66, 80]), "Invalid data (no WEBP header)")
assert(self[12..<15] == Data([86, 80, 56]), "Invalid data (no VP8X header)")

return self[30..<34] == animHeader
}
}
Loading