bmp experiments

This commit is contained in:
Thibault Deckers 2021-04-20 19:32:37 +09:00
parent f34ed2f985
commit fba090ae1f
7 changed files with 148 additions and 26 deletions

View file

@ -3,7 +3,6 @@ package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.graphics.Rect
import android.net.Uri
import android.util.Size
import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus

View file

@ -20,6 +20,7 @@ import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BmpWriter
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp
@ -112,6 +113,7 @@ abstract class ImageProvider {
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) {
MimeTypes.BMP -> ".bmp"
MimeTypes.JPEG -> ".jpg"
MimeTypes.PNG -> ".png"
MimeTypes.WEBP -> ".webp"
@ -157,6 +159,11 @@ abstract class ImageProvider {
}
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
@Suppress("BlockingMethodInNonBlockingContext")
destinationDocFile.openOutputStream().use {
if (exportMimeType == MimeTypes.BMP) {
BmpWriter.writeRGB24(bitmap, it)
} else {
val quality = 100
val format = when (exportMimeType) {
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
@ -173,11 +180,9 @@ abstract class ImageProvider {
}
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
@Suppress("BlockingMethodInNonBlockingContext")
destinationDocFile.openOutputStream().use {
bitmap.compress(format, quality, it)
}
}
} finally {
Glide.with(context).clear(target)
}

View file

@ -29,9 +29,11 @@ object BitmapUtils {
}
}
try {
// we compress the bitmap because Flutter cannot decode the raw bytes
// the Bitmap raw bytes are not decodable by Flutter
// we need to format them (compress, or add a BMP header) before sending them
// `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency
if (canHaveAlpha) {
// the BMP format allows an alpha channel, but Android decoding seems to ignore it
if (canHaveAlpha && hasAlpha()) {
this.compress(Bitmap.CompressFormat.PNG, quality, stream)
} else {
this.compress(Bitmap.CompressFormat.JPEG, quality, stream)
@ -43,7 +45,7 @@ object BitmapUtils {
freeBaos.add(stream)
}
return byteArray
} catch (e: IllegalStateException) {
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to get bytes from bitmap", e)
}
return null

View file

@ -0,0 +1,111 @@
package deckers.thibault.aves.utils
import android.graphics.Bitmap
import java.io.OutputStream
import java.nio.ByteBuffer
object BmpWriter {
private const val FILE_HEADER_SIZE = 14
private const val INFO_HEADER_SIZE = 40
private const val BYTE_PER_PIXEL = 3
private val pad = ByteArray(3)
// file header
private val bfType = byteArrayOf('B'.toByte(), 'M'.toByte())
private val bfReserved1 = intToWord(0)
private val bfReserved2 = intToWord(0)
private val bfOffBits = intToDWord(FILE_HEADER_SIZE + INFO_HEADER_SIZE)
// info header
private val biSize = intToDWord(INFO_HEADER_SIZE)
private val biPlanes = intToWord(1)
private val biBitCount = intToWord(BYTE_PER_PIXEL * 8)
private val biCompression = intToDWord(0)
private val biXPelsPerMeter = intToDWord(0)
private val biYPelsPerMeter = intToDWord(0)
private val biClrUsed = intToDWord(0)
private val biClrImportant = intToDWord(0)
// converts an int to a word (2-byte array)
private fun intToWord(v: Int): ByteArray {
val retValue = ByteArray(2)
retValue[0] = (v and 0xFF).toByte()
retValue[1] = (v shr 8 and 0xFF).toByte()
return retValue
}
// converts an int to a double word (4-byte array)
private fun intToDWord(v: Int): ByteArray {
val retValue = ByteArray(4)
retValue[0] = (v and 0xFF).toByte()
retValue[1] = (v shr 8 and 0xFF).toByte()
retValue[2] = (v shr 16 and 0xFF).toByte()
retValue[3] = (v shr 24 and 0xFF).toByte()
return retValue
}
fun writeRGB24(
bitmap: Bitmap,
outputStream: OutputStream
) {
// init
val biWidth = bitmap.width
val biHeight = bitmap.height
val padPerRow = (4 - (biWidth * BYTE_PER_PIXEL) % 4) % 4
val biSizeImage = (biWidth * BYTE_PER_PIXEL + padPerRow) * biHeight
val bfSize = FILE_HEADER_SIZE + INFO_HEADER_SIZE + biSizeImage
val buffer = ByteBuffer.allocate(bfSize)
val pixels = IntArray(biWidth * biHeight)
bitmap.getPixels(pixels, 0, biWidth, 0, 0, biWidth, biHeight)
// file header
buffer.put(bfType)
buffer.put(intToDWord(bfSize))
buffer.put(bfReserved1)
buffer.put(bfReserved2)
buffer.put(bfOffBits)
// info header
buffer.put(biSize)
buffer.put(intToDWord(biWidth))
buffer.put(intToDWord(biHeight))
buffer.put(biPlanes)
buffer.put(biBitCount)
buffer.put(biCompression)
buffer.put(intToDWord(biSizeImage))
buffer.put(biXPelsPerMeter)
buffer.put(biYPelsPerMeter)
buffer.put(biClrUsed)
buffer.put(biClrImportant)
// pixels
val rgb = ByteArray(BYTE_PER_PIXEL)
var value: Int
var row = biHeight - 1
while (row >= 0) {
var column = 0
while (column < biWidth) {
/*
alpha: (value shr 24 and 0xFF).toByte()
red: (value shr 16 and 0xFF).toByte()
green: (value shr 8 and 0xFF).toByte()
blue: (value and 0xFF).toByte()
*/
value = pixels[row * biWidth + column]
// blue: [0], green: [1], red: [2]
rgb[0] = (value and 0xFF).toByte()
rgb[1] = (value shr 8 and 0xFF).toByte()
rgb[2] = (value shr 16 and 0xFF).toByte()
buffer.put(rgb)
column++
}
if (padPerRow > 0) {
buffer.put(pad, 0, padPerRow)
}
row--
}
// write to output stream
outputStream.write(buffer.array())
}
}

View file

@ -6,7 +6,7 @@ object MimeTypes {
private const val IMAGE = "image"
// generic raster
private const val BMP = "image/bmp"
const val BMP = "image/bmp"
private const val DJVU = "image/vnd.djvu"
const val GIF = "image/gif"
const val HEIC = "image/heic"

View file

@ -75,7 +75,7 @@ abstract class ImageFileService {
Stream<ExportOpEvent> export(
Iterable<AvesEntry> entries, {
String mimeType = MimeTypes.jpeg,
@required String mimeType,
@required String destinationAlbum,
});
@ -316,7 +316,7 @@ class PlatformImageFileService implements ImageFileService {
@override
Stream<ExportOpEvent> export(
Iterable<AvesEntry> entries, {
String mimeType = MimeTypes.jpeg,
@required String mimeType,
@required String destinationAlbum,
}) {
try {

View file

@ -5,6 +5,7 @@ import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_op_events.dart';
import 'package:aves/services/services.dart';
@ -183,7 +184,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final selectionCount = selection.length;
showOpReport<ExportOpEvent>(
context: context,
opStream: imageFileService.export(selection, destinationAlbum: destinationAlbum),
opStream: imageFileService.export(
selection,
mimeType: MimeTypes.jpeg,
destinationAlbum: destinationAlbum,
),
itemCount: selectionCount,
onDone: (processed) {
final movedOps = processed.where((e) => e.success);