bmp experiments
This commit is contained in:
parent
f34ed2f985
commit
fba090ae1f
7 changed files with 148 additions and 26 deletions
|
@ -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
|
||||
|
|
|
@ -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,26 +159,29 @@ abstract class ImageProvider {
|
|||
}
|
||||
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
|
||||
|
||||
val quality = 100
|
||||
val format = when (exportMimeType) {
|
||||
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
|
||||
MimeTypes.PNG -> Bitmap.CompressFormat.PNG
|
||||
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (quality == 100) {
|
||||
Bitmap.CompressFormat.WEBP_LOSSLESS
|
||||
} else {
|
||||
Bitmap.CompressFormat.WEBP_LOSSY
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Bitmap.CompressFormat.WEBP
|
||||
}
|
||||
else -> throw Exception("unsupported export MIME type=$exportMimeType")
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
destinationDocFile.openOutputStream().use {
|
||||
bitmap.compress(format, quality, it)
|
||||
if (exportMimeType == MimeTypes.BMP) {
|
||||
BmpWriter.writeRGB24(bitmap, it)
|
||||
} else {
|
||||
val quality = 100
|
||||
val format = when (exportMimeType) {
|
||||
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
|
||||
MimeTypes.PNG -> Bitmap.CompressFormat.PNG
|
||||
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (quality == 100) {
|
||||
Bitmap.CompressFormat.WEBP_LOSSLESS
|
||||
} else {
|
||||
Bitmap.CompressFormat.WEBP_LOSSY
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Bitmap.CompressFormat.WEBP
|
||||
}
|
||||
else -> throw Exception("unsupported export MIME type=$exportMimeType")
|
||||
}
|
||||
bitmap.compress(format, quality, it)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Glide.with(context).clear(target)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue