From 0f373cee43ed7d1ca62d04acfc9374deb2138cbc Mon Sep 17 00:00:00 2001 From: Devin Bileck <603793+devinbileck@users.noreply.github.com> Date: Thu, 29 Feb 2024 22:25:25 -0800 Subject: [PATCH 1/3] Restore QR code scanner feature for mobile notification pairing This restores the functionality that was removed in b5beea58. However, this implementation utilizes the JavaCV library rather than the webcam-capture library as discussed in #4940. As a result, this should now provide macOS support. --- .../resources/i18n/displayStrings.properties | 1 + desktop/build.gradle | 1 + .../MobileNotificationsView.java | 82 ++- .../notifications/qr/FrameConverter.java | 28 + .../notifications/qr/FrameProcessor.java | 33 + .../qr/FrameToBitmapConverter.java | 118 ++++ .../qr/FrameToImageConverter.java | 70 +++ .../notifications/qr/FrameToMatConverter.java | 43 ++ .../qr/ImageCaptureDeviceFinder.java | 80 +++ .../ImageCaptureDeviceNotFoundException.java | 28 + .../ImageCaptureException.java} | 8 +- .../notifications/qr/ImageCaptureReader.java | 136 +++++ .../notifications/qr/QrCodeProcessor.java | 73 +++ .../main/overlays/windows/WebCamWindow.java | 4 +- .../content/notifications/qr/FrameUtil.java | 67 ++ .../qr/ImageCaptureReaderTest.java | 161 +++++ .../notifications/qr/QrCodeProcessorTest.java | 74 +++ desktop/src/test/resources/qr/Test_QR.png | Bin 0 -> 407 bytes gradle/libs.versions.toml | 2 + gradle/verification-metadata.xml | 574 ++++++++++++++++++ 20 files changed, 1575 insertions(+), 8 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameConverter.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameProcessor.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToBitmapConverter.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToImageConverter.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToMatConverter.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceFinder.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceNotFoundException.java rename desktop/src/main/java/bisq/desktop/main/account/content/notifications/{NoWebCamFoundException.java => qr/ImageCaptureException.java} (77%) create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessor.java create mode 100644 desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java create mode 100644 desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReaderTest.java create mode 100644 desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessorTest.java create mode 100644 desktop/src/test/resources/qr/Test_QR.png diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0fccb4d6f79..4fb0b29e4da 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2052,6 +2052,7 @@ account.notifications.priceAlert.message.title=Price alert for {0} account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2} account.notifications.noWebCamFound.warning=No webcam found.\n\n\ Please use the email option to send the token and encryption key from your mobile phone to the Bisq application. +account.notifications.webcam.error=An error has occurred with the webcam. account.notifications.priceAlert.warning.highPriceTooLow=The higher price must be larger than the lower price. account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must be lower than the higher price. diff --git a/desktop/build.gradle b/desktop/build.gradle index c0abdc7c2dc..1e6d3bc2f26 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation libs.fontawesomefx.commons implementation libs.fontawesomefx.materialdesign.font implementation libs.qrgen + implementation libs.javacv implementation libs.apache.commons.lang3 implementation libs.bouncycastle.bcpg.jdk15on implementation libs.fxmisc.easybind diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java index da4190307ad..282005e1d10 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java @@ -21,7 +21,14 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.InfoInputTextField; import bisq.desktop.components.InputTextField; +import bisq.desktop.main.account.content.notifications.qr.FrameToBitmapConverter; +import bisq.desktop.main.account.content.notifications.qr.FrameToImageConverter; +import bisq.desktop.main.account.content.notifications.qr.ImageCaptureDeviceFinder; +import bisq.desktop.main.account.content.notifications.qr.ImageCaptureDeviceNotFoundException; +import bisq.desktop.main.account.content.notifications.qr.ImageCaptureReader; +import bisq.desktop.main.account.content.notifications.qr.QrCodeProcessor; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.WebCamWindow; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; @@ -89,12 +96,15 @@ public class MobileNotificationsView extends ActivatableView { private final MarketAlerts marketAlerts; private final MobileNotificationService mobileNotificationService; + private WebCamWindow webCamWindow; + private ImageCaptureReader qrCodeReader; + private TextField tokenInputTextField; private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField; private ToggleButton useSoundToggleButton, tradeToggleButton, marketToggleButton, priceToggleButton; private ComboBox currencyComboBox; private ComboBox paymentAccountsComboBox; - private Button downloadButton, eraseButton, setPriceAlertButton, + private Button downloadButton, webCamButton, noWebCamButton, eraseButton, setPriceAlertButton, removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/; private ChangeListener useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener, @@ -145,6 +155,8 @@ protected void activate() { // setup tokenInputTextField.textProperty().addListener(tokenInputTextFieldListener); downloadButton.setOnAction(e -> onDownload()); + webCamButton.setOnAction(e -> onOpenWebCam()); + noWebCamButton.setOnAction(e -> onNoWebCam()); // testMsgButton.setOnAction(e -> onSendTestMsg()); eraseButton.setOnAction(e -> onErase()); @@ -197,6 +209,8 @@ protected void deactivate() { // setup tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener); downloadButton.setOnAction(null); + webCamButton.setOnAction(null); + noWebCamButton.setOnAction(null); //testMsgButton.setOnAction(null); eraseButton.setOnAction(null); @@ -235,6 +249,63 @@ private void onDownload() { GUIUtil.openWebPage("https://bisq.network/downloads"); } + private void onOpenWebCam() { + webCamButton.setDisable(true); + new ImageCaptureDeviceFinder(imageCaptureDevice -> { + imageCaptureDevice.setImageWidth(640); + imageCaptureDevice.setImageHeight(480); + webCamWindow = new WebCamWindow( + imageCaptureDevice.getImageWidth(), + imageCaptureDevice.getImageHeight() + ).onClose(() -> { + webCamButton.setDisable(false); + qrCodeReader.close(); + }); + webCamWindow.show(); + + qrCodeReader = new ImageCaptureReader<>( + imageCaptureDevice, + new FrameToImageConverter(), + new QrCodeProcessor(new FrameToBitmapConverter()), + webCamWindow.getImageView(), + qrCode -> { + webCamWindow.hide(); + webCamButton.setDisable(false); + reset(); + tokenInputTextField.setText(qrCode); + updateMarketAlertFields(); + updatePriceAlertFields(); + }, exception -> { + if (exception instanceof ImageCaptureDeviceNotFoundException) { + new Popup().warning(Res.get("account.notifications.noWebCamFound.warning")).show(); + webCamWindow.hide(); + webCamButton.setDisable(false); + onNoWebCam(); + } else { + log.error("{0}", exception); + new Popup().error(Res.get("account.notifications.webcam.error")).show(); + webCamWindow.hide(); + webCamButton.setDisable(false); + } + }); + }, exception -> { + if (exception instanceof ImageCaptureDeviceNotFoundException) { + new Popup().warning(Res.get("account.notifications.noWebCamFound.warning")).show(); + webCamButton.setDisable(false); + onNoWebCam(); + } else { + log.error("{0}", exception); + new Popup().error(Res.get("account.notifications.webcam.error")).show(); + } + }); + } + + private void onNoWebCam() { + setPairingTokenFieldsVisible(); + noWebCamButton.setManaged(false); + noWebCamButton.setVisible(false); + } + private void onErase() { try { mobileNotificationService.sendEraseMessage(); @@ -354,10 +425,18 @@ private void createSetupFields() { Res.get("account.notifications.download.label"), Layout.TWICE_FIRST_ROW_DISTANCE); + Tuple3 tuple = addTopLabel2Buttons(root, ++gridRow, + Res.get("account.notifications.webcam.label"), + Res.get("account.notifications.webcam.button"), Res.get("account.notifications.noWebcam.button"), 0); + webCamButton = tuple.second; + noWebCamButton = tuple.third; + tokenInputTextField = addInputTextField(root, ++gridRow, Res.get("account.notifications.email.label")); tokenInputTextField.setPromptText(Res.get("account.notifications.email.prompt")); tokenInputTextFieldListener = (observable, oldValue, newValue) -> applyKeyAndToken(newValue); + tokenInputTextField.setManaged(false); + tokenInputTextField.setVisible(false); /*testMsgButton = FormBuilder.addTopLabelButton(root, ++gridRow, Res.get("account.notifications.testMsg.label"), Res.get("account.notifications.testMsg.title")).second; @@ -718,4 +797,3 @@ private void fillPaymentAccounts() { } } - diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameConverter.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameConverter.java new file mode 100644 index 00000000000..780a206936c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameConverter.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import org.bytedeco.javacv.Frame; + +/** + * Converts a JavaCV {@link Frame} to another object of type {@link T}. + * @param The object type to convert to. + */ +public interface FrameConverter { + T convert(Frame frame); +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameProcessor.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameProcessor.java new file mode 100644 index 00000000000..bb4f8e77d51 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameProcessor.java @@ -0,0 +1,33 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.util.Optional; + + + +import org.bytedeco.javacv.Frame; + +/** + * Processes a JavaCV {@link Frame} and returns an {@link Optional} containing the + * result if successful, or {@link Optional#empty()} if there is no result. + * @param The object type of the result. + */ +public interface FrameProcessor { + Optional process(Frame frame); +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToBitmapConverter.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToBitmapConverter.java new file mode 100644 index 00000000000..476a7b39757 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToBitmapConverter.java @@ -0,0 +1,118 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.util.Objects; + +import lombok.Getter; + +import org.jetbrains.annotations.NotNull; + + + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.LuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import org.bytedeco.javacv.Frame; +import org.bytedeco.opencv.global.opencv_imgproc; +import org.bytedeco.opencv.opencv_core.Mat; + +/** + * Used for converting a JavaCV {@link Frame} into a ZXing {@link BinaryBitmap}. + * It is specifically designed for integration with the ZXing library, enabling + * QR code detection and decoding from OpenCV images. + */ +public class FrameToBitmapConverter implements FrameConverter { + private final FrameConverter frameToMatConverter = new FrameToMatConverter(); + + @Override + public BinaryBitmap convert(@NotNull final Frame frame) { + Objects.requireNonNull(frame, "Frame must not be null"); + + final Mat mat = frameToMatConverter.convert(frame); + final OpenCVMatToGrayscaleSource source = new OpenCVMatToGrayscaleSource(mat); + return new BinaryBitmap(new HybridBinarizer(source)); + } + + /** + * A luminance source that facilitates the conversion of an OpenCV {@link Mat} object, + * typically in BGR format, into a grayscale image suitable for QR scanning. + * + *

This approach allows us to eliminate our dependency on + * {@code java.awt.BufferedImage} which is the typical intermediary. + */ + private static class OpenCVMatToGrayscaleSource extends LuminanceSource { + private final byte[] luminances; + + OpenCVMatToGrayscaleSource(@NotNull final Mat mat) { + super(mat.cols(), mat.rows()); + Objects.requireNonNull(mat, "Mat must not be null"); + try (AutoCloseableMat autoCloseableMat = new AutoCloseableMat()) { + Mat grayMat = autoCloseableMat.getMat(); + + if (mat.channels() == 3) { + // Convert BGR to Grayscale + opencv_imgproc.cvtColor(mat, grayMat, opencv_imgproc.COLOR_BGR2GRAY); + } else if (mat.channels() == 1) { + grayMat = mat; + } else { + throw new IllegalArgumentException( + "Unsupported Mat format with " + mat.channels() + " channels"); + } + + this.luminances = new byte[grayMat.cols() * grayMat.rows()]; + grayMat.data().get(this.luminances); + } + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (row == null || row.length < getWidth()) { + row = new byte[getWidth()]; + } + System.arraycopy(luminances, y * getWidth(), row, 0, getWidth()); + return row; + } + + @Override + public byte[] getMatrix() { + return luminances; + } + } + + /** + * A wrapper class for OpenCV's {@link Mat} that implements {@link AutoCloseable}, + * facilitating the use of try-with-resources for automatic resource management. + * This class ensures that the native memory allocated by the {@link Mat} object is + * properly released when the {@link AutoCloseableMat} instance goes out of scope or + * is otherwise no longer needed. + */ + @Getter + private static class AutoCloseableMat implements AutoCloseable { + private final Mat mat; + + public AutoCloseableMat() { + this.mat = new Mat(); + } + + @Override + public void close() { + mat.release(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToImageConverter.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToImageConverter.java new file mode 100644 index 00000000000..9ac2f37393b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToImageConverter.java @@ -0,0 +1,70 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import javafx.scene.image.Image; +import javafx.scene.image.PixelBuffer; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.WritableImage; +import javafx.scene.image.WritablePixelFormat; + +import java.nio.ByteBuffer; + +import java.util.Objects; + +import org.jetbrains.annotations.NotNull; + +import static org.opencv.imgproc.Imgproc.COLOR_BGR2BGRA; + + + +import org.bytedeco.javacv.Frame; +import org.bytedeco.opencv.global.opencv_imgproc; +import org.bytedeco.opencv.opencv_core.Mat; + +/** + * Used for converting a JavaCV {@link Frame} into a JavaFx {@link Image} to be shown + * within an {@link javafx.scene.image.ImageView}. + */ +public class FrameToImageConverter implements FrameConverter { + private final FrameConverter frameToMatConverter = new FrameToMatConverter(); + private final WritablePixelFormat pixelFormatByteBgra = + PixelFormat.getByteBgraPreInstance(); + private final Mat destMat = new Mat(); + private ByteBuffer buffer; + + @Override + public Image convert(@NotNull final Frame frame) { + Objects.requireNonNull(frame, "Frame must not be null"); + + final Mat srcMat = frameToMatConverter.convert(frame); + opencv_imgproc.cvtColor(srcMat, destMat, COLOR_BGR2BGRA); + + if (buffer == null) { + buffer = destMat.createBuffer(); + } + + final PixelBuffer pb = new PixelBuffer<>( + frame.imageWidth, + frame.imageHeight, + buffer, + pixelFormatByteBgra + ); + return new WritableImage(pb); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToMatConverter.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToMatConverter.java new file mode 100644 index 00000000000..566555cc0cf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/FrameToMatConverter.java @@ -0,0 +1,43 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.util.Objects; + +import org.jetbrains.annotations.NotNull; + + + +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.OpenCVFrameConverter; +import org.bytedeco.opencv.opencv_core.Mat; + +/** + * Used for converting a JavaCV {@link Frame} into a OpenCV {@link Mat}, primarily as an + * intermediary to then be converted into another format. + */ +public class FrameToMatConverter implements FrameConverter { + @Override + public Mat convert(@NotNull final Frame frame) { + Objects.requireNonNull(frame, "Frame must not be null"); + + try (OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat()) { + return converter.convert(frame); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceFinder.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceFinder.java new file mode 100644 index 00000000000..9e9958925ec --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceFinder.java @@ -0,0 +1,80 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import bisq.common.UserThread; +import bisq.common.handlers.ExceptionHandler; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; + + + +import org.bytedeco.javacv.FrameGrabber; +import org.bytedeco.javacv.OpenCVFrameGrabber; + +/** + * Finds the first available image capture device as a JavaCV {@link FrameGrabber}. + */ +public class ImageCaptureDeviceFinder extends Thread { + private final Consumer resultHandler; + private final ExceptionHandler exceptionHandler; + + /** + * @param resultHandler The action to perform if an available image capture device is + * available. + * @param exceptionHandler The action to perform if no available image capture devices + * are available. + */ + public ImageCaptureDeviceFinder(@NotNull final Consumer resultHandler, + @NotNull final ExceptionHandler exceptionHandler) { + this.resultHandler = resultHandler; + this.exceptionHandler = exceptionHandler; + + start(); + } + + @Override + public void run() { + getFirstAvailableCaptureDevice().ifPresentOrElse( + frameGrabber -> + UserThread.execute(() -> resultHandler.accept(frameGrabber)), + () -> + UserThread.execute(() -> exceptionHandler.handleException( + new ImageCaptureDeviceNotFoundException()))); + } + + /** + * JavaCV doesn't provide a direct, high-level API for determining available capture + * devices. Therefore, just try to start a device at index 0 and if it fails then no + * capture devices are available. + * + * @return An {@link Optional} containing the first available + * capture device, or {@link Optional#empty()} if none are available. + */ + private Optional getFirstAvailableCaptureDevice() { + try (FrameGrabber frameGrabber = new OpenCVFrameGrabber(0)) { + frameGrabber.start(); + return Optional.of(frameGrabber); + } catch (FrameGrabber.Exception ignored) { + return Optional.empty(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceNotFoundException.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceNotFoundException.java new file mode 100644 index 00000000000..d5dab782aa1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureDeviceNotFoundException.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +public class ImageCaptureDeviceNotFoundException extends Exception { + public ImageCaptureDeviceNotFoundException() { + super(); + } + + public ImageCaptureDeviceNotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureException.java similarity index 77% rename from desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java rename to desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureException.java index 25428cde1b4..5639b64222e 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/NoWebCamFoundException.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureException.java @@ -15,10 +15,10 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.account.content.notifications; +package bisq.desktop.main.account.content.notifications.qr; -public class NoWebCamFoundException extends Throwable { - public NoWebCamFoundException(String msg) { - super(msg); +public class ImageCaptureException extends RuntimeException { + public ImageCaptureException(Throwable cause) { + super(cause); } } diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java new file mode 100644 index 00000000000..f667822d198 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java @@ -0,0 +1,136 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import bisq.common.UserThread; +import bisq.common.handlers.ExceptionHandler; + +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +import java.util.Objects; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; + + + +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.FrameGrabber; + +/** + * Continuously reads from the provided image capture device, a JavaCV + * {@link FrameGrabber}, processing captured frames until the provided processor + * finds a result. + * + * @param The object type that will be returned from the processor once it finds + * a result. + */ +public class ImageCaptureReader extends Thread implements AutoCloseable { + private final FrameGrabber frameGrabber; + private final FrameConverter frameToImageConverter; + private final FrameProcessor frameProcessor; + private final ImageView imageView; + private final Consumer resultHandler; + private final ExceptionHandler exceptionHandler; + private boolean isRunning; + + /** + * @param frameGrabber The JavaCV {@link FrameGrabber} to capture frames from. + * @param frameToImageConverter A {@link FrameConverter} that will convert + * captured JavaCV {@link Frame}'s to a JavaFx + * {@link Image} to be shown within the {@link #imageView}. + * @param frameProcessor A {@link FrameProcessor} that will process captured frames + * looking for a result. + * @param imageView The JavaFx {@link ImageView} to show captured frames within. + * @param resultHandler The action to perform once the {@link #frameProcessor} finds + * a result. + * @param exceptionHandler The action to perform if an error is encountered while + * capturing images. + */ + public ImageCaptureReader(@NotNull final FrameGrabber frameGrabber, + @NotNull final FrameConverter frameToImageConverter, + @NotNull final FrameProcessor frameProcessor, + @NotNull final ImageView imageView, + @NotNull final Consumer resultHandler, + @NotNull final ExceptionHandler exceptionHandler) { + this.frameGrabber = Objects.requireNonNull(frameGrabber, + "FrameGrabber must not be null"); + this.frameToImageConverter = Objects.requireNonNull(frameToImageConverter, + "FrameConverter must not be null"); + this.frameProcessor = Objects.requireNonNull(frameProcessor, + "FrameProcessor must not be null"); + this.imageView = Objects.requireNonNull(imageView, + "ImageView must not be null"); + this.resultHandler = Objects.requireNonNull(resultHandler, + "ResultHandler must not be null"); + this.exceptionHandler = Objects.requireNonNull(exceptionHandler, + "ExceptionHandler must not be null"); + + start(); + } + + @Override + public void run() { + try { + frameGrabber.start(); + } catch (FrameGrabber.Exception e) { + UserThread.execute(() -> exceptionHandler.handleException( + new ImageCaptureDeviceNotFoundException(e))); + return; + } + + try { + isRunning = true; + while (isRunning) { + try (Frame capturedFrame = frameGrabber.grabAtFrameRate()) { + if (capturedFrame == null) { + throw new FrameGrabber.Exception("Failed to capture frame"); + } + + final Image image = frameToImageConverter.convert(capturedFrame); + imageView.setImage(image); + + frameProcessor.process(capturedFrame).ifPresent(result -> { + isRunning = false; + UserThread.execute(() -> resultHandler.accept(result)); + }); + } + } + } catch (FrameGrabber.Exception e) { + if (isRunning) { + UserThread.execute(() -> exceptionHandler.handleException( + new ImageCaptureException(e))); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + close(); + } + } + + @Override + public void close() { + isRunning = false; + try { + frameGrabber.close(); + } catch (FrameGrabber.Exception ignored) { + // Don't care if this throws an exception at this point + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessor.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessor.java new file mode 100644 index 00000000000..ef9ab9e7c7f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessor.java @@ -0,0 +1,73 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.jetbrains.annotations.NotNull; + + + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.qrcode.QRCodeReader; +import org.bytedeco.javacv.Frame; + +/** + * Processes JavaCV {@link Frame}'s to detect and decode QR codes within the image. + */ +public class QrCodeProcessor implements FrameProcessor { + private static final Map HINTS = Map.of( + DecodeHintType.TRY_HARDER, Boolean.TRUE + ); + + private final FrameConverter frameToBitmapConverter; + + public QrCodeProcessor( + @NotNull final FrameConverter frameToBitmapConverter) { + this.frameToBitmapConverter = Objects.requireNonNull( + frameToBitmapConverter, + "FrameConverter must not be null" + ); + } + + /** + * Processes the given JavaCV {@link Frame} to detect and decode any QR codes present. + * + * @param frame The JavaCV {@link Frame} to be processed. + * @return An {@link Optional} containing the decoded QR code text if a + * QR code is detected, or {@link Optional#empty()} if no QR code is found. + */ + @Override + public Optional process(final Frame frame) { + if (frame == null || frame.image == null || frame.imageWidth <= 0 || frame.imageHeight <= 0) { + // Ignore the frame if null or has invalid dimensions + return Optional.empty(); + } + + try { + final BinaryBitmap bitmap = frameToBitmapConverter.convert(frame); + return Optional.of(new QRCodeReader().decode(bitmap, HINTS).getText()); + } catch (Exception ignored) { + // There is no QR code in the image + return Optional.empty(); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WebCamWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WebCamWindow.java index c39115465ba..019d53c8b3a 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WebCamWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WebCamWindow.java @@ -42,10 +42,9 @@ public class WebCamWindow extends Overlay { @Getter - private ImageView imageView = new ImageView(); + private final ImageView imageView = new ImageView(); private ChangeListener listener; - public WebCamWindow(double width, double height) { type = Type.Feedback; @@ -53,6 +52,7 @@ public WebCamWindow(double width, double height) { imageView.setFitHeight(height); } + @Override public void show() { headLine = Res.get("account.notifications.webCamWindow.headline"); diff --git a/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java new file mode 100644 index 00000000000..22c20e3aaee --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java @@ -0,0 +1,67 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.nio.ByteBuffer; + +import java.util.Objects; + +import org.jetbrains.annotations.NotNull; + +import static org.bytedeco.opencv.global.opencv_imgcodecs.imread; + + + +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.OpenCVFrameConverter; +import org.bytedeco.opencv.opencv_core.Mat; + +/** + * A utility for managing JavaCV {@link Frame}'s for testing purposes. + */ +public class FrameUtil { + private FrameUtil() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static Frame createRandomFrame(final int width, final int height, final int channels) { + final byte[] imageData = new byte[width * height * channels]; + for (int i = 0; i < imageData.length; i++) { + imageData[i] = (byte) (i % 255); + } + + final Frame frame = new Frame(width, height, Frame.DEPTH_UBYTE, channels); + frame.image[0] = ByteBuffer.wrap(imageData); + return frame; + } + + public static Frame createFrameFromImageResource(@NotNull final String imagePath) { + final String resImagePath = Objects.requireNonNull( + FrameUtil.class.getClassLoader().getResource(imagePath), + "Cannot find resource: " + imagePath + ).getPath(); + final Mat imageMat = imread(resImagePath); + if (imageMat.empty()) { + throw new IllegalArgumentException("Image could not be loaded: " + imagePath); + } + + try (OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat()) { + return converter.convert(imageMat); + } + } +} diff --git a/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReaderTest.java b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReaderTest.java new file mode 100644 index 00000000000..af045c136d2 --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReaderTest.java @@ -0,0 +1,161 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import bisq.common.handlers.ExceptionHandler; + +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static bisq.desktop.main.account.content.notifications.qr.FrameUtil.createFrameFromImageResource; +import static org.mockito.Mockito.*; + + + +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.FrameGrabber; + +@ExtendWith(MockitoExtension.class) +class ImageCaptureReaderTest { + @Mock + private FrameGrabber frameGrabber; + + @Mock + private FrameConverter frameToImageConverter; + + @Mock + private FrameProcessor frameProcessor; + + @Mock + private ImageView imageView; + + @Mock + private Consumer resultHandler; + + @Mock + private ExceptionHandler exceptionHandler; + + @Test + void run_CapturesProcessesAndReturnsResultOfValidQrFrame() throws Exception { + final Frame frame = createFrameFromImageResource("qr/Test_QR.png"); + final Image image = new FrameToImageConverter().convert(frame); + final String processedResult = "QR Code"; + + when(frameGrabber.grabAtFrameRate()).thenReturn(frame); + when(frameToImageConverter.convert(frame)).thenReturn(image); + when(frameProcessor.process(frame)).thenReturn(Optional.of(processedResult)); + + new ImageCaptureReader<>( + frameGrabber, + frameToImageConverter, + frameProcessor, + imageView, + resultHandler, + exceptionHandler); + + verify(imageView, timeout(500).atLeast(1)).setImage(image); + verify(frameProcessor, timeout(500).times(1)).process(frame); + verify(resultHandler, timeout(500).times(1)).accept(processedResult); + verify(frameGrabber, timeout(500).times(1)).close(); + } + + @Test + void run_CapturesAndProcessesNonQrFrame() throws Exception { + final Frame frame = createFrameFromImageResource("qr/Test_QR.png"); + final Image image = new FrameToImageConverter().convert(frame); + + when(frameGrabber.grabAtFrameRate()).thenReturn(frame); + when(frameToImageConverter.convert(frame)).thenReturn(image); + when(frameProcessor.process(frame)).thenReturn(Optional.empty()); + + new ImageCaptureReader<>( + frameGrabber, + frameToImageConverter, + frameProcessor, + imageView, + resultHandler, + exceptionHandler); + + verify(frameProcessor, timeout(500).atLeast(1)).process(frame); + verify(resultHandler, timeout(500).times(0)).accept(any(String.class)); + verify(frameGrabber, timeout(500).times(0)).close(); + } + + @Test + void run_FrameGrabberStartFails_ExceptionThrown() throws Exception { + doThrow(new FrameGrabber.Exception("Failed to start")).when(frameGrabber).start(); + + new ImageCaptureReader<>( + frameGrabber, + frameToImageConverter, + frameProcessor, + imageView, + resultHandler, + exceptionHandler); + + verify(exceptionHandler, timeout(500)) + .handleException(any(ImageCaptureDeviceNotFoundException.class)); + } + + @Test + void run_FrameGrabberInterrupted_ShutsDownGracefully() throws Exception { + // Configure frame grabber to block or delay, simulating long-running operation + when(frameGrabber.grabAtFrameRate()).thenAnswer(invocation -> { + Thread.sleep(Long.MAX_VALUE); + return new Frame(); + }); + + final ImageCaptureReader reader = new ImageCaptureReader<>( + frameGrabber, + frameToImageConverter, + frameProcessor, + imageView, + resultHandler, + exceptionHandler); + + // Interrupt the reader thread, simulating a shutdown request + reader.interrupt(); + + verify(frameGrabber, timeout(1000).times(1)).close(); + } + + @Test + void run_CaptureNullFrame_ExceptionThrown() throws Exception { + when(frameGrabber.grabAtFrameRate()).thenReturn(null); + + new ImageCaptureReader<>( + frameGrabber, + frameToImageConverter, + frameProcessor, + imageView, + resultHandler, + exceptionHandler); + + verify(exceptionHandler, timeout(100)) + .handleException(any(ImageCaptureException.class)); + } +} diff --git a/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessorTest.java b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessorTest.java new file mode 100644 index 00000000000..37198c73c64 --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/QrCodeProcessorTest.java @@ -0,0 +1,74 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.account.content.notifications.qr; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static bisq.desktop.main.account.content.notifications.qr.FrameUtil.createFrameFromImageResource; +import static bisq.desktop.main.account.content.notifications.qr.FrameUtil.createRandomFrame; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + + +import com.google.zxing.BinaryBitmap; +import org.bytedeco.javacv.Frame; + +class QrCodeProcessorTest { + private final FrameConverter converter = new FrameToBitmapConverter(); + private final QrCodeProcessor processor = new QrCodeProcessor(converter); + + @Test + void process_ValidQrCodeFrame_ReturnsDecodedText() { + final Frame validQrFrame = createFrameFromImageResource("qr/Test_QR.png"); + final String expectedResult = "test"; + + final Optional result = processor.process(validQrFrame); + + assertTrue(result.isPresent()); + assertEquals(expectedResult, result.get()); + } + + @Test + void process_NonQrCodeFrame_ReturnsEmptyOptional() { + final Frame randomFrame = createRandomFrame(100, 100, 1); + + final Optional result = processor.process(randomFrame); + + assertFalse(result.isPresent()); + } + + @Test + void process_Null_ReturnsEmptyOptional() { + final Optional result = processor.process(null); + + assertFalse(result.isPresent()); + } + + @Test + void process_InvalidFrame_ReturnsEmptyOptional() { + try (Frame invalidFrame = createRandomFrame(0, 0, 0)) { + final Optional result = processor.process(invalidFrame); + + assertFalse(result.isPresent()); + } + } +} diff --git a/desktop/src/test/resources/qr/Test_QR.png b/desktop/src/test/resources/qr/Test_QR.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b5c49eddc45cd9ae68a4e282501309ceb96207 GIT binary patch literal 407 zcmXYty-&hW6vd00Kx(Qi4T%JF#>A1Oev|?M7E;YC7!3y2#;#q6F%Vyf3!{sRY%e8{ zXH4s4gzioTQ|X|803^C}AqH>J;obL|JqY_qPR5xx2hV4kn6Ml8jpA9jDiSDFqiRxn~dDCCh$ zPJk>{$BzGfAToFX;^dKb?KohF;A|=W_c}xvB_a4_XeK(r#qc(ByDx9}HJgWlGlgt1 I)0e8vUtn^rM*si- literal 0 HcmV?d00001 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8e62518c5f..0960950c50b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ netlayer = { strictly = '2b459dc' } openjfx-javafx-plugin = { strictly = '0.0.10' } protobuf = { strictly = '3.19.1' } qrgen = { strictly = '1.3' } +javacv = { strictly = '1.5.10' } slf4j = { strictly = '1.7.30' } [libraries] @@ -108,4 +109,5 @@ netlayer-tor-native = { module = "com.github.bisq-network.netlayer:tor.native", openjfx-javafx-plugin = { module = "org.openjfx:javafx-plugin", version.ref = "openjfx-javafx-plugin" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } qrgen = { module = "net.glxn:qrgen", version.ref = "qrgen" } +javacv = { module = "org.bytedeco:javacv-platform", version.ref = "javacv" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5582e3a7a3b..b17acc1d1c2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1135,6 +1135,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e6e9608473543bb1718909ab58ad49e156dee2fd Mon Sep 17 00:00:00 2001 From: Devin Bileck <603793+devinbileck@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:45:16 -0800 Subject: [PATCH 2/3] Fix image loading from resources on Windows This should hopefully resolve images failing to load from resources on Windows due to problems with imread encountering "can't open/read file: check file path/integrity". --- .../content/notifications/qr/FrameUtil.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java index 22c20e3aaee..39c6a7b5e49 100644 --- a/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java +++ b/desktop/src/test/java/bisq/desktop/main/account/content/notifications/qr/FrameUtil.java @@ -17,16 +17,22 @@ package bisq.desktop.main.account.content.notifications.qr; +import org.apache.commons.io.IOUtils; + import java.nio.ByteBuffer; -import java.util.Objects; +import java.io.IOException; +import java.io.InputStream; import org.jetbrains.annotations.NotNull; -import static org.bytedeco.opencv.global.opencv_imgcodecs.imread; +import static org.bytedeco.opencv.global.opencv_imgcodecs.imdecode; +import static org.opencv.imgcodecs.Imgcodecs.IMREAD_ANYCOLOR; +import static org.opencv.imgcodecs.Imgcodecs.IMREAD_ANYDEPTH; +import org.bytedeco.javacpp.BytePointer; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.OpenCVFrameConverter; import org.bytedeco.opencv.opencv_core.Mat; @@ -51,17 +57,20 @@ public static Frame createRandomFrame(final int width, final int height, final i } public static Frame createFrameFromImageResource(@NotNull final String imagePath) { - final String resImagePath = Objects.requireNonNull( - FrameUtil.class.getClassLoader().getResource(imagePath), - "Cannot find resource: " + imagePath - ).getPath(); - final Mat imageMat = imread(resImagePath); - if (imageMat.empty()) { - throw new IllegalArgumentException("Image could not be loaded: " + imagePath); - } - - try (OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat()) { + try ( + InputStream is = FrameUtil.class.getClassLoader().getResourceAsStream(imagePath); + OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat() + ) { + if (is == null) { + throw new IllegalArgumentException("Image could not be loaded: " + imagePath); + } + Mat imageMat = imdecode(new Mat(new BytePointer(IOUtils.toByteArray(is))), IMREAD_ANYDEPTH | IMREAD_ANYCOLOR); + if (imageMat.empty()) { + throw new IllegalArgumentException("Image could not be loaded: " + imagePath); + } return converter.convert(imageMat); + } catch (IOException e) { + throw new RuntimeException(e); } } } From 5f3f692b2f01ccf03ee319ed762111619ce6e5bf Mon Sep 17 00:00:00 2001 From: Devin Bileck <603793+devinbileck@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:59:57 -0700 Subject: [PATCH 3/3] Fix SIGSEGV encountered on macOS When attempting to close the frame grabber from a separate thread, the application would crash due to a SIGSEGV. --- .../content/notifications/qr/ImageCaptureReader.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java index f667822d198..b5f46d0ee04 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/qr/ImageCaptureReader.java @@ -120,6 +120,11 @@ public void run() { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { + try { + frameGrabber.close(); + } catch (FrameGrabber.Exception ignored) { + // Don't care if this throws an exception at this point + } close(); } } @@ -127,10 +132,5 @@ public void run() { @Override public void close() { isRunning = false; - try { - frameGrabber.close(); - } catch (FrameGrabber.Exception ignored) { - // Don't care if this throws an exception at this point - } } }