Skip to content

Commit 4ca5cb2

Browse files
author
Yevhen Babiichuk (DustDFG)
committed
Convert newpipe/util/image/ImageStrategy to kotlin
1 parent 4ea9147 commit 4ca5cb2

2 files changed

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

0 commit comments

Comments
 (0)