Skip to content

Instantly share code, notes, and snippets.

@jcayzac
Last active November 17, 2020 12:06
Show Gist options
  • Save jcayzac/fc8444e3df88fbe59e15a2f0c2d48a83 to your computer and use it in GitHub Desktop.
Save jcayzac/fc8444e3df88fbe59e15a2f0c2d48a83 to your computer and use it in GitHub Desktop.
Image size fitting computations
data class RenderSize(val width: Int, val height: Int) {
companion object {
// 0x0 is an alias for "any"
val ANY = RenderSize(0, 0)
}
}
/**
* Scores an image resource based on the set of sizes it supports and on the
* size the caller wants to render it inside of.
*
* The result is a value between 0 and 1, inclusive. The higher the fitter.
*
* The function favors images that support the requested size exactly. Images
* that support "any" size come next. Images that only support sizes either
* immensely too large or immensely too small are filtered out. The remaining
* images get a score based on their difference in size and aspect ratio.
*
* The smallest images larger than the requested size are treated more
* favorably than the largest images smaller than it. Furthermore, a good
* aspect ratio is treated more importantly than the need to downscale, although
* the penalty might be less than the one incurred by a need to upscale.
*
* For other, more limited implementations, see:
*
* - [`ManifestIconSelector::FindBestMatchingIcon`](https://github.com/chromium/chromium/blob/master/third_party/blink/common/manifest/manifest_icon_selector.cc)
* in Blink.
* - [`GetCandidateIndexWithBestScore`](https://github.com/chromium/chromium/blob/master/components/favicon_base/select_favicon_frames.cc)
* in Chromium.
*/
fun score(supportedSizes: Set<RenderSize>, requestedSize: RenderSize): Double {
// Give maximum score to exact size matches
if (supportedSizes.contains(requestedSize))
return 1.0
// An image supporting "any" size is a near-perfect match
if (supportedSizes.contains(RenderSize.ANY))
return .99999999
// Images with huge differences in scale generally aren't suitable.
// We reject anything smaller or larger than a up/downscale factor of 8.
val maxWidth: Int = requestedSize.width.shl(3)
val maxHeight: Int = requestedSize.height.shl(3)
val minWidth: Int = requestedSize.width.ushr(3)
val minHeight: Int = requestedSize.height.ushr(3)
// Compute some fixed values outside of the iteration below
val idealWidth: Double = requestedSize.width.toDouble()
val idealHeight: Double = requestedSize.height.toDouble()
val idealRatio: Double = idealWidth / idealHeight
return supportedSizes
.filter { (width: Int, height: Int) ->
// If the size is too large it should be rejected
width <= maxWidth && height <= maxHeight
}
.filter { (width: Int, height: Int) ->
// If the size is too small it should be rejected
width > minWidth && height > minHeight
}
.map { ( width: Int, height: Int ) ->
// Convert to double
Pair(width.toDouble(), height.toDouble())
}
.maxOfOrNull { (width: Double, height: Double) ->
val widthDiff: Double = width - idealWidth
val heightDiff: Double = height - idealHeight
var score = 1.0
// Bigger sizes get a penalty up to 25% per dimension
if (widthDiff > 0) score *= 1.0 - (widthDiff * 3) / (idealWidth * 28)
if (heightDiff > 0) score *= 1.0 - (heightDiff * 3) / (idealHeight * 28)
// Smaller sizes get a penalty of at least 25%, up to 75% per dimension
if (widthDiff < 0) score *= .25 - (widthDiff / idealWidth) * .5
if (heightDiff < 0) score *= .25 - (heightDiff / idealHeight) * .5
// Different aspect ratios get a penalty up to 50%
val ratio = width / height
score *= .5 * (min(ratio, idealRatio) / max(ratio, idealRatio) + 1.0)
score
} ?: /* rejected */ .0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment