[WIP] Implements ImageDecoders to decouple decoding from downloading.#1890
[WIP] Implements ImageDecoders to decouple decoding from downloading.#1890rharter wants to merge 16 commits intosquare:masterfrom
Conversation
| BASE + "P5ZRSvT" + EXT, BASE + "jbemFzr" + EXT, BASE + "8B7haIK" + EXT, | ||
| BASE + "aSeTYQr" + EXT, BASE + "OKvWoTh" + EXT, BASE + "zD3gT4Z" + EXT, | ||
| BASE + "z77CaIt" + EXT, | ||
| BASE + "z77CaIt" + EXT, "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg" |
There was a problem hiding this comment.
Was looking for a Picasso related sample SVG to use, but couldn't find much. This should, perhaps, be replaced with something we can guarantee.
| BufferedSource source = Okio.buffer(Okio.source(assetManager.open(getFilePath(request)))); | ||
| try { | ||
| Bitmap bitmap = decodeStream(source, request); | ||
| ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); |
There was a problem hiding this comment.
This code is repeated in each RequestHandler and should probably be extracted into the abstract base class.
| import java.io.IOException; | ||
| import okio.BufferedSource; | ||
|
|
||
| public interface ImageDecoder { |
There was a problem hiding this comment.
This should be renamed so we don't conflict with android.graphics.ImageDecoder.
|
|
||
| public interface ImageDecoder { | ||
|
|
||
| public class Image { |
| * @return The first ImageDecoder that can decode the source, or null. | ||
| */ | ||
| @Nullable ImageDecoder getImageDecoderForSource(BufferedSource source) { | ||
| for (ImageDecoder decoder : decoders) { |
There was a problem hiding this comment.
I wasn't sure if we should shortcut and just return if there is only one decoder, as in a user added .asBitmap() to a request. That would remove the need to check if the decoder can handle the source, but might also make the error message more confusing since it will move the error from here into the decode method.
There was a problem hiding this comment.
it should always call canHandleSource, at least to avoid surprise.
|
|
||
| /** Add an decoder that can decode custom image formats. */ | ||
| @NonNull | ||
| public Builder addImageDecoder(@NonNull ImageDecoder imageDecoder) { |
There was a problem hiding this comment.
Allows system wide addition of new ImageDecoders, so you could add your own MP4->Drawable decoder, for example. By default the included image decoders will be added, so upgrades don't require any code changes.
| } | ||
|
|
||
| @NonNull | ||
| public Builder asBitmap() { |
There was a problem hiding this comment.
This is to make it easier to specify the exact image decoder, avoiding checking all of the default ones. This is completely optional, so no code changes are required.
Usage:
Picasso.get()
.load(myImageUrl)
.asBitmap()
.into(myImageView)lThere was a problem hiding this comment.
umm, that's the RequestCreator one :)
There was a problem hiding this comment.
This is fine for now, but I want to nuke this whole API from orbit. I'd like requests to be separate from how they're fulfilled.
|
Woah, I had no idea but just found issue #1049 that I filed back in 2015 asking for exactly this. |
|
@rharter Sorry for the delay in getting to this. Looks like this PR has a few conflicts, plus I've pulled out the BitmapUtils stuff in #1985. Mind rebasing? I've gone ahead and pushed a branch with what that might look like: 05c9983 And also added some cleanup here: c53f667 to make rebasing less painful. |
|
Re: the SVG dependency, maybe we pull the SVG decoder out into a separate artifact? That can be a followup PR. |
|
Who's ready for an update!?! Sorry for the delay, but I've rebased this using your commits, then rebased on master, then extracted the SVG bits into an external artifact, like we discussed. I think the structure of how decoders (I'm thinking we'll have multiples, like another for Animated GIF) are included is up for debate, but got this in to start. I'm having errorprone problems locally, so let me know if something is off. |
|
|
||
| @Override public boolean canHandleSource(BufferedSource source) { | ||
| try { | ||
| SVG.getFromInputStream(source.peek().inputStream()); |
There was a problem hiding this comment.
is it sufficient to check for xml header bytes?
There was a problem hiding this comment.
The library internally handles gzipped content here, so we'd have to move that logic out into this, and we'd also have to validate the SVG. It could be done but I think that should live in the SVG library.
There was a problem hiding this comment.
OkHttp should be transparently un-gzipping. Or are you worried about reading local files?
There was a problem hiding this comment.
That's a fair point. My point was simply that determining if the stream is an SVG is a little more complex that check that it (1) is an XML file and (2) has a DOCTYPE or something. Taking out the GZipInputStream case (which I guess is for http streams), the existing SVG library will validate the SVG as well as load it, which is something that makes sense in an SVG parser, as opposed to reimplementing here.
One example of the added complexity, from what I can see, the existing library checks the DOCTYPE, which might contain <!ENTITY in which case it falls back to a different parser. I think understanding the complexities around XML, and SVG in particular, make sense to be handled in the parser.
Ideally the library had another method that would simply check if the stream is an SVG without parsing the whole thing, but this one doesn't.
| * @return The first ImageDecoder that can decode the source, or null. | ||
| */ | ||
| @Nullable ImageDecoder getImageDecoderForSource(BufferedSource source) { | ||
| for (ImageDecoder decoder : decoders) { |
There was a problem hiding this comment.
it should always call canHandleSource, at least to avoid surprise.
| return decoder; | ||
| } | ||
| } | ||
| return null; |
There was a problem hiding this comment.
In the current implementation the ISE is thrown one level up, where the RequestHandler has the information to create a useful error message, which includes the URI or file path of the request.
We could throw one here, then catch it one level up and wrap it with a useful message, or just let the caller handle it.
| final ImageDecoderFactory imageDecoderFactory; | ||
| final Map<Object, Action> targetToAction; | ||
| final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator; | ||
| final List<RequestHandler> extraRequestHandlers; |
| this.closeableCache = closeableCache; | ||
| this.cache = cache; | ||
| this.listener = listener; | ||
| this.imageDecoderFactory = imageDecoderFactory; |
There was a problem hiding this comment.
when you call newBuilder().addImageDecoder(...).build(), are the old image decoders retained and appended to? (add a test?)
| // we successfully decoded the bounds | ||
| return options.outWidth > 0 && options.outHeight > 0; | ||
| } catch (IOException e) { | ||
| return false; |
There was a problem hiding this comment.
mmm, i think this whole method should just always true. an I/O problem and all the other things are different from not being able to handle the request.
There was a problem hiding this comment.
The IOException is just because that's required to be handled, the main idea here is that the stream might not be something Bitmap factory can handle (an SVG, an animated GIF, a fat jpeg, a live photo), so this needs to test that it can decode a bitmap from the stream.
The fastest way I could think of, without needing to load the entire bitmap into memory, was but simply decoding the bounds.
| private final @Nullable okhttp3.Cache closeableCache; | ||
| final PlatformLruCache cache; | ||
| final Stats stats; | ||
| final ImageDecoderFactory imageDecoderFactory; |
There was a problem hiding this comment.
where is this used? it should get passed to the RequestCreate/Builder thing.
There was a problem hiding this comment.
This is used by the RequestCreator and Request.Builder. The convention seems to be passing the whole Picasso instance into the RequestCreator and having it access the properties on it, like defaultBitmapConfig, dispatcher, cache, etc.
|
|
||
| class SvgImageDecoder implements ImageDecoder { | ||
|
|
||
| @Override public boolean canHandleSource(BufferedSource source) { |
There was a problem hiding this comment.
Feels like we should hand a peeked source here rather than let consumers worry about it
|
|
||
| @Override public boolean canHandleSource(BufferedSource source) { | ||
| try { | ||
| SVG.getFromInputStream(source.peek().inputStream()); |
There was a problem hiding this comment.
OkHttp should be transparently un-gzipping. Or are you worried about reading local files?
| } | ||
| } | ||
|
|
||
| @Override public Image decodeImage(BufferedSource source, Request request) throws IOException { |
There was a problem hiding this comment.
I wonder if we'd be better off with a design where canHandleSource returned either:
- An opaque
Object(or maybeT) which is forwarded to this method. In this case it would be theSVGso it doesn't need re-decoded. Returnnullif you can't handle. - The actual
Decoderand this would change to aDecoder.Factory. That way you could propagate as much information as you wanted. Returnnullif you can't handle. I guess technically you can already do this with case Start actual website content. #1 by just using some data class as your type.
There was a problem hiding this comment.
Would that simplify things?
In the SVG case it would remove a parse, since we can't currently detect an SVG without parsing it, but I think that's sort of a special case. Bitmap, where we just decode the bounds to identify if it's parseable, doesn't have that limitation, and also nothing useful to return. In those cases I think this dead simple API is nice, though I'll admit I'm not entirely sure what case #2 would look like.
There was a problem hiding this comment.
The bounds are useful to propagate for Bitmap so we can avoid re-reading them to apply transformations during decode.
Case #2 would work like Retrofit Converter.Factory or CallAdapter.Factory or Moshi JsonAdapter.Factory. If you can handle, return a handler.
| } | ||
|
|
||
| @NonNull | ||
| public Builder asBitmap() { |
There was a problem hiding this comment.
This is fine for now, but I want to nuke this whole API from orbit. I'd like requests to be separate from how they're fulfilled.
This is a POC to demonstrate what I was thinking to support more image types than just the built in bitmap types. Currently, the tasks of loading (from network, disk, etc.) and decoding images are conflated, which means that the type of image you're loading (e.g. jpeg, png, svg) is tightly coupled with where it comes from (e.g. network, filesystem, content resolver).
To solve this, I've extracted the image decoding bits from
RequestHandlerinto anImageDecodertype, which could use a different name. EachPicassoinstance can be configured with a set ofImageDecoders that it will use to attempt to decode images.Furthermore, the user can specify an
ImageDecoderFactoryper request so that, if they know what type of image is going to be loaded, they can explicitly state whichImageDecoderto use, reducing the amount of searching that's required.Still to do: