diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java b/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java index 17418e3c517..673f5f61fed 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java @@ -178,66 +178,26 @@ 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; } /** @@ -245,10 +205,12 @@ private String guessAdditionalContentTypeFromStream(InputStream is) */ 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. diff --git a/tests/Resources/Logo.webp b/tests/Resources/Logo.webp new file mode 100644 index 00000000000..30d10c55fc6 Binary files /dev/null and b/tests/Resources/Logo.webp differ diff --git a/tests/Resources/ti.blob.test.js b/tests/Resources/ti.blob.test.js index 93180bd6f18..18affe45ee4 100644 --- a/tests/Resources/ti.blob.test.js +++ b/tests/Resources/ti.blob.test.js @@ -128,6 +128,14 @@ 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(); @@ -135,6 +143,14 @@ describe('Titanium.Blob', function () { 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', () => { @@ -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(); @@ -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(); @@ -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'); }); diff --git a/tests/Resources/ti.ui.imageview.test.js b/tests/Resources/ti.ui.imageview.test.js index fec05502062..226bf65553b 100644 --- a/tests/Resources/ti.ui.imageview.test.js +++ b/tests/Resources/ti.ui.imageview.test.js @@ -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) {