fix(native): cache widget artwork at its real pixel size

UIGraphicsImageRenderer(size:) uses the screen scale by default, so the cached JPEGs came out about 1395px wide instead of the intended 465px. WidgetKit refuses to render images whose pixel area is far beyond the widget's, and the extension has a tight memory limit, so oversized art could blank the widget.

Render at scale 1 into a center-cropped 510px square (systemSmall is at most 170pt, or 510px at 3x) and bump JPEG quality to 0.8 now that the files are much smaller.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-04 17:38:04 -04:00
1 parent 9bbe36b2d4
commit b4fdf6164f
1 file changed
+20 -15
@@ -6,6 +6,10 @@ private let imageDirectory = "widget_images"
private let widgetIconKey = "sofa_icon.png"
private let infoPlistAppGroupKey = "ExpoWidgetsAppGroupIdentifier"
private let logPrefix = "[SofaWidgetsSupport]"
// systemSmall widgets are at most 170pt square (510px at 3x). WidgetKit refuses to
// render images whose pixel area is far beyond the widget's, so artwork is cropped
// to that square.
private let widgetImagePixelSize: CGFloat = 510
public class SofaWidgetsSupportModule: Module {
public func definition() -> ModuleDefinition {
@@ -34,11 +38,9 @@ public class SofaWidgetsSupportModule: Module {
return nil
}
// Resize to fit widget dimensions (systemSmall ~155pt = ~465px at 3x)
let maxDimension: CGFloat = 465
let resized = resizeImage(image, maxDimension: maxDimension)
let resized = squareThumbnail(image, side: widgetImagePixelSize)
guard let jpegData = resized.jpegData(compressionQuality: 0.7) else {
guard let jpegData = resized.jpegData(compressionQuality: 0.8) else {
return nil
}
@@ -182,20 +184,23 @@ public class SofaWidgetsSupportModule: Module {
print("\(logPrefix) \(message)")
}
private func resizeImage(_ image: UIImage, maxDimension: CGFloat) -> UIImage {
/// Scales `image` to cover a `side`×`side` square (never upscaling) and center-crops it.
/// Renders at scale 1 so the output's pixel size equals its point size; the default
/// renderer format uses the screen scale and would triple the pixel dimensions.
private func squareThumbnail(_ image: UIImage, side maxSide: CGFloat) -> UIImage {
let size = image.size
let scale = min(maxDimension / size.width, maxDimension / size.height, 1.0)
guard size.width > 0, size.height > 0 else { return image }
let side = min(maxSide, size.width, size.height).rounded(.down)
let scale = side / min(size.width, size.height)
let drawSize = CGSize(width: size.width * scale, height: size.height * scale)
let origin = CGPoint(x: (side - drawSize.width) / 2, y: (side - drawSize.height) / 2)
if scale >= 1.0 { return image }
let newSize = CGSize(
width: round(size.width * scale),
height: round(size.height * scale)
)
let renderer = UIGraphicsImageRenderer(size: newSize)
let format = UIGraphicsImageRendererFormat()
format.scale = 1
format.opaque = true
let renderer = UIGraphicsImageRenderer(size: CGSize(width: side, height: side), format: format)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
image.draw(in: CGRect(origin: origin, size: drawSize))
}
}
}