summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
blob: d57147f3636866d55b4b4f5ac688783aa7a23e32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * vim: ts=4 sw=4 expandtab:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.util;

import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.mozilla.geckoview.GeckoResult;

/**
 * Represents an Web API image resource as used in web app manifests and media session metadata.
 *
 * @see <a href="https://www.w3.org/TR/image-resource">Image Resource</a>
 */
@AnyThread
public class ImageResource {
  private static final String LOGTAG = "ImageResource";
  private static final boolean DEBUG = false;

  /** Represents the size of an image resource option. */
  public static class Size {
    /** The width in pixels. */
    public final int width;

    /** The height in pixels. */
    public final int height;

    /**
     * Size contructor.
     *
     * @param width The width in pixels.
     * @param height The height in pixels.
     */
    public Size(final int width, final int height) {
      this.width = width;
      this.height = height;
    }
  }

  /** The URI of the image resource. */
  public final @NonNull String src;

  /** The MIME type of the image resource. */
  public final @Nullable String type;

  /** A {@link Size} array of supported images sizes. */
  public final @Nullable Size[] sizes;

  /**
   * ImageResource constructor.
   *
   * @param src The URI string of the image resource.
   * @param type The MIME type of the image resource.
   * @param sizes The supported images {@link Size} array.
   */
  public ImageResource(
      final @NonNull String src, final @Nullable String type, final @Nullable Size[] sizes) {
    this.src = src.toLowerCase(Locale.ROOT);
    this.type = type != null ? type.toLowerCase(Locale.ROOT) : null;
    this.sizes = sizes;
  }

  /**
   * ImageResource constructor.
   *
   * @param src The URI string of the image resource.
   * @param type The MIME type of the image resource.
   * @param sizes The supported images sizes string.
   * @see <a href="https://html.spec.whatwg.org/multipage/semantics.html#dom-link-sizes">Attribute
   *     spec for sizes</a>
   */
  public ImageResource(
      final @NonNull String src, final @Nullable String type, final @Nullable String sizes) {
    this(src, type, parseSizes(sizes));
  }

  private static @Nullable Size[] parseSizes(final @Nullable String sizesStr) {
    if (sizesStr == null || sizesStr.isEmpty()) {
      return null;
    }

    final String[] sizesStrs = sizesStr.toLowerCase(Locale.ROOT).split(" ");
    final List<Size> sizes = new ArrayList<Size>();

    for (final String sizeStr : sizesStrs) {
      if (sizesStr.equals("any")) {
        // 0-width size will always be favored.
        sizes.add(new Size(0, 0));
        continue;
      }
      final String[] widthHeight = sizeStr.split("x");
      if (widthHeight.length != 2) {
        // Not spec-compliant size.
        continue;
      }
      try {
        sizes.add(new Size(Integer.valueOf(widthHeight[0]), Integer.valueOf(widthHeight[1])));
      } catch (final NumberFormatException e) {
        Log.e(LOGTAG, "Invalid image resource size", e);
      }
    }
    if (sizes.isEmpty()) {
      return null;
    }
    return sizes.toArray(new Size[0]);
  }

  public static @NonNull ImageResource fromBundle(final GeckoBundle bundle) {
    return new ImageResource(
        bundle.getString("src"), bundle.getString("type"), bundle.getString("sizes"));
  }

  @Override
  public String toString() {
    final StringBuilder builder = new StringBuilder("ImageResource {");
    builder
        .append("src=")
        .append(src)
        .append("type=")
        .append(type)
        .append("sizes=")
        .append(sizes)
        .append("}");
    return builder.toString();
  }

  /**
   * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
   * cache the result of this method keyed with this instance.
   *
   * @param size pixel size at which this image will be displayed at.
   * @return A {@link GeckoResult} that resolves to the bitmap when ready.
   */
  @NonNull
  public GeckoResult<Bitmap> getBitmap(final int size) {
    return ImageDecoder.instance().decode(src, size);
  }

  /**
   * Represents a collection of {@link ImageResource} options. Image resources are often used in a
   * collection to provide multiple image options for various sizes. This data structure can be used
   * to retrieve the best image resource for any given target image size.
   */
  public static class Collection {
    private static class SizeIndexPair {
      public final int width;
      public final int idx;

      public SizeIndexPair(final int width, final int idx) {
        this.width = width;
        this.idx = idx;
      }
    }

    // The individual image resources, usually each with a unique src.
    private final List<ImageResource> mImages;

    // A sorted size-index list. The list is sorted based on the supported
    // sizes of the images in ascending order.
    private final List<SizeIndexPair> mSizeIndex;

    /* package */ Collection() {
      mImages = new ArrayList<>();
      mSizeIndex = new ArrayList<>();
    }

    /** Builder class for the construction of a {@link Collection}. */
    public static class Builder {
      final Collection mCollection;

      public Builder() {
        mCollection = new Collection();
      }

      /**
       * Add an image resource to the collection.
       *
       * @param image The {@link ImageResource} to be added.
       * @return This builder instance.
       */
      public @NonNull Builder add(final ImageResource image) {
        final int index = mCollection.mImages.size();

        if (image.sizes == null) {
          // Null-sizes are handled the same as `any`.
          mCollection.mSizeIndex.add(new SizeIndexPair(0, index));
        } else {
          for (final Size size : image.sizes) {
            mCollection.mSizeIndex.add(new SizeIndexPair(size.width, index));
          }
        }
        mCollection.mImages.add(image);
        return this;
      }

      /**
       * Finalize the collection.
       *
       * @return The final collection.
       */
      public @NonNull Collection build() {
        Collections.sort(mCollection.mSizeIndex, (a, b) -> Integer.compare(a.width, b.width));
        return mCollection;
      }
    }

    @Override
    public String toString() {
      final StringBuilder builder = new StringBuilder("ImageResource.Collection {");
      builder.append("images=[");

      for (final ImageResource image : mImages) {
        builder.append(image).append(", ");
      }
      builder.append("]}");
      return builder.toString();
    }

    /**
     * Returns the best suited {@link ImageResource} for the given size. This is usually determined
     * based on the minimal difference between the given size and one of the supported widths of an
     * image resource.
     *
     * @param size The target size for the image in pixels.
     * @return The best {@link ImageResource} for the given size from this collection.
     */
    public @Nullable ImageResource getBest(final int size) {
      if (mSizeIndex.isEmpty()) {
        return null;
      }
      int bestMatchIdx = mSizeIndex.get(0).idx;
      int lastDiff = size;
      for (final SizeIndexPair sizeIndex : mSizeIndex) {
        final int diff = Math.abs(sizeIndex.width - size);
        if (lastDiff <= diff) {
          // With increasing widths, the difference can only grow now.
          // 0-width means "any", so we're finished at the first
          // entry.
          break;
        }
        lastDiff = diff;
        bestMatchIdx = sizeIndex.idx;
      }
      return mImages.get(bestMatchIdx);
    }

    /**
     * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
     * cache the result of this method keyed with this instance.
     *
     * @param size pixel size at which this image will be displayed at.
     * @return A {@link GeckoResult} that resolves to the bitmap when ready.
     */
    @NonNull
    public GeckoResult<Bitmap> getBitmap(final int size) {
      final ImageResource image = getBest(size);
      if (image == null) {
        return GeckoResult.fromValue(null);
      }
      return image.getBitmap(size);
    }

    public static Collection fromSizeSrcBundle(final GeckoBundle bundle) {
      final Builder builder = new Builder();

      for (final String key : bundle.keys()) {
        final Integer intKey = Integer.valueOf(key);
        if (intKey == null) {
          Log.e(LOGTAG, "Non-integer image key: " + intKey);

          if (DEBUG) {
            throw new RuntimeException("Non-integer image key: " + key);
          }
          continue;
        }

        final String src = getImageValue(bundle.get(key));
        if (src != null) {
          // Given the bundle structure, we don't have insight on
          // individual image resources so we have to create an
          // instance for each size entry.
          final ImageResource image =
              new ImageResource(src, null, new Size[] {new Size(intKey, intKey)});
          builder.add(image);
        }
      }
      return builder.build();
    }

    private static String getImageValue(final Object value) {
      // The image value can either be an object containing images for
      // each theme...
      if (value instanceof GeckoBundle) {
        // We don't support theme_images yet, so let's just return the
        // default value.
        final GeckoBundle themeImages = (GeckoBundle) value;
        final Object defaultImages = themeImages.get("default");

        if (!(defaultImages instanceof String)) {
          if (DEBUG) {
            throw new RuntimeException("Unexpected themed_icon value.");
          }
          Log.e(LOGTAG, "Unexpected themed_icon value.");
          return null;
        }

        return (String) defaultImages;
      }

      // ... or just a URL.
      if (value instanceof String) {
        return (String) value;
      }

      // We never expect it to be something else, so let's error out here.
      if (DEBUG) {
        throw new RuntimeException("Unexpected image value: " + value);
      }

      Log.e(LOGTAG, "Unexpected image value.");
      return null;
    }
  }
}