-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implements ImageDecoders to decouple decoding from downloading. #1890
base: master
Are you sure you want to change the base?
Conversation
@@ -15,7 +15,7 @@ | |||
BASE + "Q54zMKT" + EXT, BASE + "9t6hLbm" + EXT, BASE + "F8n3Ic6" + EXT, | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
try { | ||
Bitmap bitmap = decodeStream(source, request); | ||
ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be renamed so we don't conflict with android.graphics.ImageDecoder
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ImageFormatDecoder?
|
||
public interface ImageDecoder { | ||
|
||
public class Image { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
* @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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should always call canHandleSource, at least to avoid surprise.
@@ -745,6 +748,17 @@ public Builder listener(@NonNull Listener listener) { | |||
return this; | |||
} | |||
|
|||
/** Add an decoder that can decode custom image formats. */ | |||
@NonNull | |||
public Builder addImageDecoder(@NonNull ImageDecoder imageDecoder) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -573,6 +579,24 @@ public Builder priority(@NonNull Priority priority) { | |||
return this; | |||
} | |||
|
|||
@NonNull | |||
public Builder asBitmap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)l
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
umm, that's the RequestCreator
one :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
picasso/src/main/java/com/squareup/picasso3/SourceBufferingInputStream.java
Outdated
Show resolved
Hide resolved
picasso/src/main/java/com/squareup/picasso3/SourceBufferingInputStream.java
Outdated
Show resolved
Hide resolved
picasso/src/main/java/com/squareup/picasso3/SourceBufferingInputStream.java
Outdated
Show resolved
Hide resolved
picasso/src/main/java/com/squareup/picasso3/SourceBufferingInputStream.java
Outdated
Show resolved
Hide resolved
picasso/src/test/java/com/squareup/picasso3/SourceBufferingInputStreamTest.java
Outdated
Show resolved
Hide resolved
picasso/src/main/java/com/squareup/picasso3/SourceBufferingInputStream.java
Outdated
Show resolved
Hide resolved
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it sufficient to check for xml header bytes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OkHttp should be transparently un-gzipping. Or are you worried about reading local files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
decoders/svg/src/main/java/com/squareup/picasso3/decoder/svg/SvgImageDecoder.java
Outdated
Show resolved
Hide resolved
* @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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should always call canHandleSource, at least to avoid surprise.
return decoder; | ||
} | ||
} | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw the ISE here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
picasso/src/main/java/com/squareup/picasso3/ImageDecoderFactory.java
Outdated
Show resolved
Hide resolved
final Map<Object, Action> targetToAction; | ||
final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator; | ||
final List<RequestHandler> extraRequestHandlers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9aa2877
@@ -156,7 +159,9 @@ void onImageLoadFailed(@NonNull Picasso picasso, @NonNull Uri uri, | |||
this.closeableCache = closeableCache; | |||
this.cache = cache; | |||
this.listener = listener; | |||
this.imageDecoderFactory = imageDecoderFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when you call newBuilder().addImageDecoder(...).build(), are the old image decoders retained and appended to? (add a test?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7efb773
// we successfully decoded the bounds | ||
return options.outWidth > 0 && options.outHeight > 0; | ||
} catch (IOException e) { | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -136,8 +136,10 @@ void onImageLoadFailed(@NonNull Picasso picasso, @NonNull Uri uri, | |||
private final @Nullable okhttp3.Cache closeableCache; | |||
final PlatformLruCache cache; | |||
final Stats stats; | |||
final ImageDecoderFactory imageDecoderFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is this used? it should get passed to the RequestCreate/Builder thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like we should hand a peeked source here rather than let consumers worry about it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 89081a3
|
||
@Override public boolean canHandleSource(BufferedSource source) { | ||
try { | ||
SVG.getFromInputStream(source.peek().inputStream()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 theSVG
so it doesn't need re-decoded. Returnnull
if you can't handle. - The actual
Decoder
and this would change to aDecoder.Factory
. That way you could propagate as much information as you wanted. Returnnull
if 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -573,6 +579,24 @@ public Builder priority(@NonNull Priority priority) { | |||
return this; | |||
} | |||
|
|||
@NonNull | |||
public Builder asBitmap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
RequestHandler
into anImageDecoder
type, which could use a different name. EachPicasso
instance can be configured with a set ofImageDecoder
s that it will use to attempt to decode images.Furthermore, the user can specify an
ImageDecoderFactory
per request so that, if they know what type of image is going to be loaded, they can explicitly state whichImageDecoder
to use, reducing the amount of searching that's required.Still to do: