Skip to content

Commit

Permalink
fix(android): blob fails to read WebP image info (#13069)
Browse files Browse the repository at this point in the history
* fix(android): blob fails to read WebP image info

Fixes TIMOB-28535

* test: made ".image wtih TI.Blob" tests more reliable

Co-authored-by: Gary Mathews <contact@garymathews.com>
  • Loading branch information
jquick-axway and garymathews authored Oct 20, 2021
1 parent e525889 commit acc561a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 77 deletions.
84 changes: 23 additions & 61 deletions android/titanium/src/java/org/appcelerator/titanium/TiBlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,77 +178,39 @@ public static TiBlob blobFromData(byte[] data, String mimetype)
*/
public String guessContentTypeFromStream()
{
String mt = null;
InputStream is = getInputStream();
// We shouldn't try and sniff content type if mark isn't supported by this
// input stream! Otherwise we'll read bytes that we can't stuff back anymore
// so the stream will have been modified for future reads.
if (is != null && is.markSupported()) {
try {
mt = URLConnection.guessContentTypeFromStream(is);
if (mt == null) {
mt = guessAdditionalContentTypeFromStream(is);
String mimeType = null;
try (var inputStream = getInputStream()) {
if ((inputStream != null) && inputStream.markSupported()) {
// First, attempt to fetch mime-type via BitmapFactory. Will only work for image formats.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
mimeType = options.outMimeType;

// If above failed, then try to guess mime-type via WebKit.
// Note: This returns wrong mime-type for WebP images, which is why we use BitmapFactory 1st.
if (mimeType == null) {
inputStream.reset();
mimeType = URLConnection.guessContentTypeFromStream(inputStream);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e, Log.DEBUG_MODE);
}
}
return mt;
}

/**
* Check for additional content type reading first few characters from the given input stream.
*
* @return the guessed MIME-type or null if the type could not be determined.
*/
private String guessAdditionalContentTypeFromStream(InputStream is)
{
String mt = null;

if (is != null) {
try {

// Look ahead up to 12 bytes (highest number of bytes we care about for now)
is.mark(12);
byte[] bytes = new byte[12];
int length = is.read(bytes);
is.reset();
if (length == -1) {
return null;
}

// This is basically exactly what the normal JDK sniffs for, but Android's fork does not
if (bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F' && bytes[3] == '8') {
mt = "image/gif";
} else if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50 && bytes[2] == (byte) 0x4E
&& bytes[3] == (byte) 0x47 && bytes[4] == (byte) 0x0D && bytes[5] == (byte) 0x0A
&& bytes[6] == (byte) 0x1A && bytes[7] == (byte) 0x0A) {
mt = "image/png";
} else if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF) {
if ((bytes[3] == (byte) 0xE0)
|| (bytes[3] == (byte) 0xE1 && bytes[6] == 'E' && bytes[7] == 'x' && bytes[8] == 'i'
&& bytes[9] == 'f' && bytes[10] == 0)) {
mt = "image/jpeg";
} else if (bytes[3] == (byte) 0xEE) {
mt = "image/jpg";
}
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
} catch (Exception ex) {
Log.e(TAG, ex.getMessage(), ex, Log.DEBUG_MODE);
}
return mt;
return mimeType;
}

/**
* Update width and height if the file / data can be decoded into a bitmap successfully.
*/
public void loadBitmapInfo()
{
String mt = guessContentTypeFromStream();
// Update mimetype based on the guessed MIME-type.
if (mt != null && !mt.equals(mimetype)) {
mimetype = mt;
// If assigned mime-type is null or generic, then attempt to guess it.
if ((this.mimetype == null) || this.mimetype.equals(TiMimeTypeHelper.MIME_TYPE_OCTET_STREAM)) {
String newMimeType = guessContentTypeFromStream();
if (newMimeType != null) {
this.mimetype = newMimeType;
}
}

// If the MIME-type is "image/*" or undetermined, try to decode the file / data into a bitmap.
Expand Down
Binary file added tests/Resources/Logo.webp
Binary file not shown.
47 changes: 43 additions & 4 deletions tests/Resources/ti.blob.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,29 @@ describe('Titanium.Blob', function () {
[ 'text/javascript', 'application/javascript' ].should.containEql(blob.mimeType);
});

it('image/jpeg', () => {
const blob = Ti.Filesystem.getFile('ExifRotate90.jpg').read();
should(blob.mimeType).be.a.String();
should(blob.mimeType.length).be.above(0);
should(blob.mimeType).be.eql('image/jpeg');
// TODO Test that it's read-only
});

it('image/png', () => {
const blob = Ti.Filesystem.getFile('Logo.png').read();
should(blob.mimeType).be.a.String();
should(blob.mimeType.length).be.above(0);
should(blob.mimeType).be.eql('image/png');
// TODO Test that it's read-only
});

it('image/webp', () => {
const blob = Ti.Filesystem.getFile('Logo.webp').read();
should(blob.mimeType).be.a.String();
should(blob.mimeType.length).be.above(0);
should(blob.mimeType).be.eql('image/webp');
// TODO Test that it's read-only
});
});

it('.length', () => {
Expand Down Expand Up @@ -173,6 +189,15 @@ describe('Titanium.Blob', function () {
// TODO Test that it's read-only
});

it('returns pixel count for WebP', () => {
const blob = Ti.Filesystem.getFile('Logo.webp').read();
should(blob.width).be.a.Number();
should(blob.width).be.eql(150);
should(blob.uprightWidth).be.a.Number();
should(blob.uprightWidth).be.eql(blob.width);
// TODO Test that it's read-only
});

it('returns 0 for non-image (JS file)', function () {
var blob = Ti.Filesystem.getFile('app.js').read();
should(blob.width).be.a.Number();
Expand All @@ -191,6 +216,15 @@ describe('Titanium.Blob', function () {
// TODO Test that it's read-only
});

it('returns pixel count for WebP', () => {
const blob = Ti.Filesystem.getFile('Logo.webp').read();
should(blob.height).be.a.Number();
should(blob.height).be.eql(150);
should(blob.uprightHeight).be.a.Number();
should(blob.uprightHeight).be.eql(blob.height);
// TODO Test that it's read-only
});

it('returns 0 for non-image (JS file)', () => {
const blob = Ti.Filesystem.getFile('app.js').read();
should(blob.height).be.a.Number();
Expand Down Expand Up @@ -219,12 +253,17 @@ describe('Titanium.Blob', function () {
const blob = Ti.Filesystem.getFile('Logo.png').read();
const b = blob.imageAsCompressed(0.5);
should(b).be.an.Object();
// width and height should remain the same
should(b.width).be.eql(blob.width);
should(b.height).be.eql(blob.height);
// Ideally, the byte size should drop - though that's not guranteed!
// should(b.length).be.below(blob.length);
// becomes a JPEG, so I guess we could test mimeType?
should(b.mimeType).be.eql('image/jpeg');
});

it('with WebP', function () {
const blob = Ti.Filesystem.getFile('Logo.webp').read();
const b = blob.imageAsCompressed(0.5);
should(b).be.an.Object();
should(b.width).be.eql(blob.width);
should(b.height).be.eql(blob.height);
should(b.mimeType).be.eql('image/jpeg');
});

Expand Down
31 changes: 19 additions & 12 deletions tests/Resources/ti.ui.imageview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,23 +168,30 @@ describe('Titanium.UI.ImageView', function () {
imageView.image = fromFile;
});

// Windows: TIMOB-24985
// FIXME Android and iOS don't fire the 'load' event! Seems like android only fires load if image isn't in cache
it.allBroken('with Ti.Blob', finish => {
const fromFile = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'Logo.png');
const blob = fromFile.read();
const imageView = Ti.UI.createImageView();
imageView.addEventListener('load', function () {
function doBlobTest(blob, finish) {
win = Ti.UI.createWindow();
const imageView = Ti.UI.createImageView({ image: blob });
imageView.addEventListener('postlayout', function listener(e) {
try {
should(imageView.image).be.an.Object();
should(imageView.toBlob()).eql(blob);
imageView.removeEventListener(e.type, listener);
should(imageView.size.width > 0).be.true();
finish();
} catch (err) {
return finish(err);
finish(err);
}
finish();
});
win.add(imageView);
win.open();
}

it('with Ti.Blob PNG', function (finish) {
const blob = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'Logo.png').read();
doBlobTest(blob, finish);
});

imageView.image = blob;
it('with Ti.Blob WebP', function (finish) {
const blob = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'Logo.webp').read();
doBlobTest(blob, finish);
});

it.windowsBroken('with redirected URL and autorotate set to true', function (finish) {
Expand Down

0 comments on commit acc561a

Please sign in to comment.