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