Skip to content

Commit fef8a24

Browse files
author
Yevhen Babiichuk (DustDFG)
committed
Convert newpipe/util/image/ImageStrategy to kotlin
1 parent 3398b4c commit fef8a24

2 files changed

Lines changed: 191 additions & 195 deletions

File tree

app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java

Lines changed: 0 additions & 195 deletions
This file was deleted.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2023-2025 NewPipe contributors <https://newpipe.net>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package org.schabi.newpipe.util.image
7+
8+
import org.schabi.newpipe.extractor.Image
9+
import org.schabi.newpipe.extractor.Image.ResolutionLevel
10+
import kotlin.math.abs
11+
12+
object ImageStrategy {
13+
// when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred
14+
// image quality is to these values (H stands for "Height")
15+
private const val BEST_LOW_H = 75
16+
private const val BEST_MEDIUM_H = 250
17+
18+
private var preferredImageQuality = PreferredImageQuality.MEDIUM
19+
20+
@JvmStatic
21+
fun setPreferredImageQuality(preferredImageQuality: PreferredImageQuality) {
22+
ImageStrategy.preferredImageQuality = preferredImageQuality
23+
}
24+
25+
@JvmStatic
26+
fun shouldLoadImages(): Boolean {
27+
return preferredImageQuality != PreferredImageQuality.NONE
28+
}
29+
30+
@JvmStatic
31+
fun estimatePixelCount(image: Image, widthOverHeight: Double): Double {
32+
if (image.height == Image.HEIGHT_UNKNOWN) {
33+
if (image.width == Image.WIDTH_UNKNOWN) {
34+
// images whose size is completely unknown will be in their own subgroups, so
35+
// any one of them will do, hence returning the same value for all of them
36+
return 0.0
37+
} else {
38+
return image.width * image.width / widthOverHeight
39+
}
40+
} else if (image.width == Image.WIDTH_UNKNOWN) {
41+
return image.height * image.height * widthOverHeight
42+
} else {
43+
return (image.height * image.width).toDouble()
44+
}
45+
}
46+
47+
/**
48+
* [choosePreferredImage] contains the description for this function's logic.
49+
*
50+
* @param images the images from which to choose
51+
* @param nonNoneQuality the preferred quality (must NOT be [PreferredImageQuality.NONE])
52+
* @return the chosen preferred image, or `null` if the list is empty
53+
* @see [choosePreferredImage]
54+
*/
55+
@JvmStatic
56+
fun choosePreferredImage(images: List<Image>, nonNoneQuality: PreferredImageQuality): String? {
57+
// this will be used to estimate the pixel count for images where only one of height or
58+
// width are known
59+
val widthOverHeight = images
60+
.filter { image ->
61+
image.height != Image.HEIGHT_UNKNOWN && image.width != Image.WIDTH_UNKNOWN
62+
}
63+
.map { image -> (image.width.toDouble()) / image.height }
64+
.elementAtOrNull(0) ?: 1.0
65+
66+
val preferredLevel = nonNoneQuality.toResolutionLevel()
67+
// TODO: rewrite using kotlin collections API `groupBy` will be handy
68+
val initialComparator =
69+
Comparator // the first step splits the images into groups of resolution levels
70+
.comparingInt { i: Image ->
71+
return@comparingInt when (i.estimatedResolutionLevel) {
72+
// avoid unknowns as much as possible
73+
ResolutionLevel.UNKNOWN -> 3
74+
75+
// prefer a matching resolution level
76+
preferredLevel -> 0
77+
78+
// the preferredLevel is only 1 "step" away (either HIGH or LOW)
79+
ResolutionLevel.MEDIUM -> 1
80+
81+
// the preferredLevel is the furthest away possible (2 "steps")
82+
else -> 2
83+
}
84+
}
85+
// then each level's group is further split into two subgroups, one with known image
86+
// size (which is also the preferred subgroup) and the other without
87+
.thenComparing { image -> image.height == Image.HEIGHT_UNKNOWN && image.width == Image.WIDTH_UNKNOWN }
88+
89+
// The third step chooses, within each subgroup with known image size, the best image based
90+
// on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups
91+
// without known image size will be left untouched since estimatePixelCount always returns
92+
// the same number for those.
93+
val finalComparator = when (nonNoneQuality) {
94+
PreferredImageQuality.NONE -> initialComparator
95+
PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image ->
96+
val pixelCount = estimatePixelCount(image, widthOverHeight)
97+
abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight)
98+
}
99+
100+
PreferredImageQuality.MEDIUM -> initialComparator.thenComparingDouble { image ->
101+
val pixelCount = estimatePixelCount(image, widthOverHeight)
102+
abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight)
103+
}
104+
105+
PreferredImageQuality.HIGH -> initialComparator.thenComparingDouble { image ->
106+
// this is reversed with a - so that the highest resolution is chosen
107+
-estimatePixelCount(image, widthOverHeight)
108+
}
109+
}
110+
111+
return images.stream() // using "min" basically means "take the first group, then take the first subgroup,
112+
// then choose the best image, while ignoring all other groups and subgroups"
113+
.min(finalComparator)
114+
.map(Image::getUrl)
115+
.orElse(null)
116+
}
117+
118+
/**
119+
* Chooses an image amongst the provided list based on the user preference previously set with
120+
* [setPreferredImageQuality]. `null` will be returned in
121+
* case the list is empty or the user preference is to not show images.
122+
* <br>
123+
* These properties will be preferred, from most to least important:
124+
*
125+
* 1. The image's [Image.estimatedResolutionLevel] is not unknown and is close to [preferredImageQuality]
126+
* 2. At least one of the image's width or height are known
127+
* 3. The highest resolution image is finally chosen if the user's preference is
128+
* [PreferredImageQuality.HIGH], otherwise the chosen image is the one that has the height
129+
* closest to [BEST_LOW_H] or [BEST_MEDIUM_H]
130+
*
131+
* <br>
132+
* Use [imageListToDbUrl] if the URL is going to be saved to the database, to avoid
133+
* saving nothing in case at the moment of saving the user preference is to not show images.
134+
*
135+
* @param images the images from which to choose
136+
* @return the chosen preferred image, or `null` if the list is empty or the user disabled
137+
* images
138+
* @see [imageListToDbUrl]
139+
*/
140+
@JvmStatic
141+
fun choosePreferredImage(images: List<Image>): String? {
142+
if (preferredImageQuality == PreferredImageQuality.NONE) {
143+
return null // do not load images
144+
}
145+
146+
return choosePreferredImage(images, preferredImageQuality)
147+
}
148+
149+
/**
150+
* Like [choosePreferredImage], except that if [preferredImageQuality] is
151+
* [PreferredImageQuality.NONE] an image will be chosen anyway (with preferred quality
152+
* [PreferredImageQuality.MEDIUM].
153+
* <br></br>
154+
* To go back to a list of images (obviously with just the one chosen image) from a URL saved in
155+
* the database use [dbUrlToImageList].
156+
*
157+
* @param images the images from which to choose
158+
* @return the chosen preferred image, or `null` if the list is empty
159+
* @see [choosePreferredImage]
160+
* @see [dbUrlToImageList]
161+
*/
162+
@JvmStatic
163+
fun imageListToDbUrl(images: List<Image>): String? {
164+
val quality = when (preferredImageQuality) {
165+
PreferredImageQuality.NONE -> PreferredImageQuality.MEDIUM
166+
else -> preferredImageQuality
167+
}
168+
169+
return choosePreferredImage(images, quality)
170+
}
171+
172+
/**
173+
* Wraps the URL (coming from the database) in a `List<Image>` so that it is usable
174+
* seamlessly in all of the places where the extractor would return a list of images, including
175+
* allowing to build info objects based on database objects.
176+
* <br></br>
177+
* To obtain a url to save to the database from a list of images use [imageListToDbUrl].
178+
*
179+
* @param url the URL to wrap coming from the database, or `null` to get an empty list
180+
* @return a list containing just one [Image] wrapping the provided URL, with unknown
181+
* image size fields, or an empty list if the URL is `null`
182+
* @see [imageListToDbUrl]
183+
*/
184+
@JvmStatic
185+
fun dbUrlToImageList(url: String?): List<Image> {
186+
return when (url) {
187+
null -> listOf()
188+
else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN))
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)