Try harder to find QR codes in image files

And refactor a bit by moving some of the QR scanning related logic to a
separate helper class.
This commit is contained in:
Alexander Bakker 2022-08-07 17:05:08 +02:00
parent 5f12eae678
commit bd3697659f
6 changed files with 184 additions and 82 deletions

View file

@ -9,19 +9,11 @@ import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
@ -59,14 +51,8 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
false
);
MultiFormatReader reader = new MultiFormatReader();
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
hints.put(DecodeHintType.ALSO_INVERTED, true);
Result result = reader.decode(bitmap, hints);
Result result = QrCodeHelper.decodeFromSource(source);
if (_listener != null) {
_listener.onQrCodeDetected(result);
}

View file

@ -0,0 +1,96 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class QrCodeHelper {
private QrCodeHelper() {
}
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
hints.put(DecodeHintType.ALSO_INVERTED, true);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
MultiFormatReader reader = new MultiFormatReader();
return reader.decode(bitmap, hints);
}
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
if (bitmap == null) {
throw new DecodeError("Unable to decode stream to bitmap");
}
// If ZXing is not able to decode the image on the first try, we try a couple of
// more times with smaller versions of the same image.
for (int i = 0; i <= 2; i++) {
if (i != 0) {
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
}
try {
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
return decodeFromSource(source);
} catch (NotFoundException ignored) {
}
}
throw new DecodeError(NotFoundException.getNotFoundInstance());
}
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
public static class DecodeError extends Exception {
public DecodeError(String message) {
super(message);
}
public DecodeError(Throwable cause) {
super(cause);
}
}
}

View file

@ -5,8 +5,6 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
@ -26,32 +24,19 @@ import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.LuminanceSource;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -326,37 +311,25 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void onScanImageResult(Intent intent) {
decodeQrCodeImage(intent.getData());
startDecodeQrCodeImage(intent.getData());
}
private void decodeQrCodeImage(Uri inputFile) {
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
private void startDecodeQrCodeImage(Uri uri) {
QrDecodeTask task = new QrDecodeTask(this, (result) -> {
if (result.getException() != null) {
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, result.getException());
return;
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new QRCodeReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getResult().getText());
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
} catch (GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
});
task.execute(getLifecycle(), uri);
}
private void updateSortCategoryMenu() {
@ -471,7 +444,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_STREAM);
decodeQrCodeImage(uri);
startDecodeQrCodeImage(uri);
}
}

View file

@ -15,12 +15,10 @@ import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.util.ArrayList;
import java.util.List;
@ -104,22 +102,12 @@ public class TransferEntriesActivity extends AegisActivity {
return true;
}
private void generateQR() {
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
_issuer.setText(selectedEntry.getIssuer());
_accountName.setText(selectedEntry.getAccountName());
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix;
try {
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
} catch (WriterException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
@ -127,18 +115,14 @@ public class TransferEntriesActivity extends AegisActivity {
backgroundColor = typedValue.data;
}
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
Bitmap bitmap;
try {
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
} catch (WriterException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
_qrImage.setImageBitmap(bitmap);
}
}

View file

@ -0,0 +1,62 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
import com.google.zxing.Result;
import java.io.IOException;
import java.io.InputStream;
public class QrDecodeTask extends ProgressDialogTask<Uri, QrDecodeTask.Response> {
private final Callback _cb;
public QrDecodeTask(Context context, Callback cb) {
super(context, context.getString(R.string.analyzing_qr));
_cb = cb;
}
@Override
protected Response doInBackground(Uri... params) {
Context context = getDialog().getContext();
Uri uri = params[0];
try (InputStream inStream = context.getContentResolver().openInputStream(uri)) {
Result result = QrCodeHelper.decodeFromStream(inStream);
return new Response(result, null);
} catch (QrCodeHelper.DecodeError | IOException e) {
e.printStackTrace();
return new Response(null, e);
}
}
@Override
protected void onPostExecute(Response result) {
super.onPostExecute(result);
_cb.onTaskFinished(result);
}
public interface Callback {
void onTaskFinished(Response result);
}
public static class Response {
private final Result _result;
private final Exception _e;
public Response(Result result, Exception e) {
_result = result;
_e = e;
}
public Result getResult() {
return _result;
}
public Exception getException() {
return _e;
}
}
}

View file

@ -156,6 +156,7 @@
<string name="encrypting_vault">Encrypting the vault</string>
<string name="exporting_vault">Exporting the vault</string>
<string name="reading_file">Reading file</string>
<string name="analyzing_qr">Analyzing QR code</string>
<string name="importing_icon_pack">Importing icon pack</string>
<string name="delete_entry">Delete entry</string>
<string name="delete_entry_description">Are you sure you want to delete this entry?</string>