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.
This commit is contained in:
Devin Bileck 2024-02-29 22:25:25 -08:00
parent 92283de42a
commit 0f373cee43
No known key found for this signature in database
GPG Key ID: 649D9C87FB168B88
20 changed files with 1575 additions and 8 deletions

View File

@ -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.

View File

@ -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

View File

@ -21,7 +21,14 @@ import bisq.desktop.common.view.ActivatableView;
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<GridPane, Void> {
private final MarketAlerts marketAlerts;
private final MobileNotificationService mobileNotificationService;
private WebCamWindow webCamWindow;
private ImageCaptureReader<String> qrCodeReader;
private TextField tokenInputTextField;
private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField;
private ToggleButton useSoundToggleButton, tradeToggleButton, marketToggleButton, priceToggleButton;
private ComboBox<TradeCurrency> currencyComboBox;
private ComboBox<PaymentAccount> paymentAccountsComboBox;
private Button downloadButton, eraseButton, setPriceAlertButton,
private Button downloadButton, webCamButton, noWebCamButton, eraseButton, setPriceAlertButton,
removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/;
private ChangeListener<Boolean> useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener,
@ -145,6 +155,8 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
// 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 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
// setup
tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener);
downloadButton.setOnAction(null);
webCamButton.setOnAction(null);
noWebCamButton.setOnAction(null);
//testMsgButton.setOnAction(null);
eraseButton.setOnAction(null);
@ -235,6 +249,63 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
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 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
Res.get("account.notifications.download.label"),
Layout.TWICE_FIRST_ROW_DISTANCE);
Tuple3<Label, Button, Button> 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 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <T> The object type to convert to.
*/
public interface FrameConverter<T> {
T convert(Frame frame);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<T>} containing the
* result if successful, or {@link Optional#empty()} if there is no result.
* @param <T> The object type of the result.
*/
public interface FrameProcessor<T> {
Optional<T> process(Frame frame);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<BinaryBitmap> {
private final FrameConverter<Mat> 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.
*
* <p>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();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Image> {
private final FrameConverter<Mat> frameToMatConverter = new FrameToMatConverter();
private final WritablePixelFormat<ByteBuffer> 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<ByteBuffer> pb = new PixelBuffer<>(
frame.imageWidth,
frame.imageHeight,
buffer,
pixelFormatByteBgra
);
return new WritableImage(pb);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Mat> {
@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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<FrameGrabber> 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<FrameGrabber> 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<FrameGrabber>} containing the first available
* capture device, or {@link Optional#empty()} if none are available.
*/
private Optional<FrameGrabber> getFirstAvailableCaptureDevice() {
try (FrameGrabber frameGrabber = new OpenCVFrameGrabber(0)) {
frameGrabber.start();
return Optional.of(frameGrabber);
} catch (FrameGrabber.Exception ignored) {
return Optional.empty();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications.qr;
public class ImageCaptureDeviceNotFoundException extends Exception {
public ImageCaptureDeviceNotFoundException() {
super();
}
public ImageCaptureDeviceNotFoundException(Throwable cause) {
super(cause);
}
}

View File

@ -15,10 +15,10 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <T> The object type that will be returned from the processor once it finds
* a result.
*/
public class ImageCaptureReader<T> extends Thread implements AutoCloseable {
private final FrameGrabber frameGrabber;
private final FrameConverter<Image> frameToImageConverter;
private final FrameProcessor<T> frameProcessor;
private final ImageView imageView;
private final Consumer<T> resultHandler;
private final ExceptionHandler exceptionHandler;
private boolean isRunning;
/**
* @param frameGrabber The JavaCV {@link FrameGrabber} to capture frames from.
* @param frameToImageConverter A {@link FrameConverter<Image>} 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<T>} 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<Image> frameToImageConverter,
@NotNull final FrameProcessor<T> frameProcessor,
@NotNull final ImageView imageView,
@NotNull final Consumer<T> 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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> {
private static final Map<DecodeHintType, Object> HINTS = Map.of(
DecodeHintType.TRY_HARDER, Boolean.TRUE
);
private final FrameConverter<BinaryBitmap> frameToBitmapConverter;
public QrCodeProcessor(
@NotNull final FrameConverter<BinaryBitmap> 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<String>} 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<String> 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();
}
}
}

View File

@ -42,10 +42,9 @@ import lombok.extern.slf4j.Slf4j;
public class WebCamWindow extends Overlay<WebCamWindow> {
@Getter
private ImageView imageView = new ImageView();
private final ImageView imageView = new ImageView();
private ChangeListener<Image> listener;
public WebCamWindow(double width, double height) {
type = Type.Feedback;
@ -53,6 +52,7 @@ public class WebCamWindow extends Overlay<WebCamWindow> {
imageView.setFitHeight(height);
}
@Override
public void show() {
headLine = Res.get("account.notifications.webCamWindow.headline");

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Image> frameToImageConverter;
@Mock
private FrameProcessor<String> frameProcessor;
@Mock
private ImageView imageView;
@Mock
private Consumer<String> 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<String> 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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<BinaryBitmap> 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<String> 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<String> result = processor.process(randomFrame);
assertFalse(result.isPresent());
}
@Test
void process_Null_ReturnsEmptyOptional() {
final Optional<String> result = processor.process(null);
assertFalse(result.isPresent());
}
@Test
void process_InvalidFrame_ReturnsEmptyOptional() {
try (Frame invalidFrame = createRandomFrame(0, 0, 0)) {
final Optional<String> result = processor.process(invalidFrame);
assertFalse(result.isPresent());
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

View File

@ -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" }

View File

@ -1135,6 +1135,580 @@
<sha256 value="40bc5efb0aa8ecb08d180edb4758255648877df6fd44ef0815db960a6c4d828f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="artoolkitplus" version="2.3.1-1.5.9">
<artifact name="artoolkitplus-2.3.1-1.5.9-android-arm.jar">
<sha256 value="2d5aa7b4eda74bea0d23d1d995ef546dd64b6bd6c0bccdf770c193603a96b917" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-android-arm64.jar">
<sha256 value="c87b0a0c784a04857d43b129f28bd533619dffc9ce362a348c147e960a037921" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-android-x86.jar">
<sha256 value="ab5d5ce28862378868ce700efab22525e539a11133a92f013a1b1da00e15a3be" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-android-x86_64.jar">
<sha256 value="317b6b091076b8c2dd02480d8018c0551d00499e8c57a0abb1606af3d4e3b61d" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-linux-arm64.jar">
<sha256 value="293a276aaabbfff0c1b6fcb1ce1b8c58c81f5abbccc5da90921cdda6c50c60f3" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-linux-armhf.jar">
<sha256 value="bee9ecaeeb68dc18c0d536bbb58d89b941e7fc2c699c6f92d17a9b73dfbc2ca5" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-linux-ppc64le.jar">
<sha256 value="d26fdbe4f7ac6461794ad7d2e3af492c44ea6eb873993bcbcff2ace49a8944d1" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-linux-x86.jar">
<sha256 value="72b2329974d41761dbed5c53596f5aa342af7754605a3986c1d20e150004848e" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-linux-x86_64.jar">
<sha256 value="b6cef20eae6acbab1b808cf91ee54f42eaf33a12a80bed6c0900d86a5c98712e" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-macosx-x86_64.jar">
<sha256 value="268453196a68de1945990f5bf7424fc4cd77d0ea55b15292b0f1dad4b0c88667" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-windows-x86.jar">
<sha256 value="a7003e2c49421a961d08475efd9dc912d2c2d7f838ce02fe9de7459393bbfff6" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9-windows-x86_64.jar">
<sha256 value="8e7ac8fa62a70f24ccee1e10e81e20229601d28a8459e6befbf90e6243412895" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9.jar">
<sha256 value="a981d290d14ab130bb21541483a307d87c04a4acec5d9561a4cbc2c4f94281ba" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-2.3.1-1.5.9.pom">
<sha256 value="ea86613042d337494dd087920e6927767327caa9184e9e08d18dd6a4c73d3ae7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="artoolkitplus-platform" version="2.3.1-1.5.9">
<artifact name="artoolkitplus-platform-2.3.1-1.5.9.jar">
<sha256 value="05514c14d4bfbc364262eab60a308e861739e75112a8e4eaa3497f73460c7300" origin="Generated by Gradle"/>
</artifact>
<artifact name="artoolkitplus-platform-2.3.1-1.5.9.pom">
<sha256 value="7b347f9ca62dc594f04431e973814cd3a2a806034cb67b4d8720bb7d58a8a4dc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="ffmpeg" version="6.1.1-1.5.10">
<artifact name="ffmpeg-6.1.1-1.5.10-android-arm64.jar">
<sha256 value="800bfa09387201dc1ababd9418e75c8614a9bd11aea8b654be886f139c538ea3" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-android-x86_64.jar">
<sha256 value="94f49b648e5cf173e4e25152a08b62ad4bb6683599b126888aac1aec4fc5bcec" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-linux-arm64.jar">
<sha256 value="543c70ca204772dbe804ba73d5baaa70c6456bbdd1861fbc27fb5d74832f45dc" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-linux-ppc64le.jar">
<sha256 value="b4190eeb16d1872521b5d2992404a514da89a8155dd0b1627378c5e00247a5d8" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-linux-x86_64.jar">
<sha256 value="fba80d4007014f8879766e969172b85a5adad9eba217a2b360f416fce275d379" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-macosx-arm64.jar">
<sha256 value="386dac8a99af483c1044bca2d46d4159f4289cb28e045138d6007e432e32c97f" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-macosx-x86_64.jar">
<sha256 value="b42dc1199956d8754ae961842b568850fdb2b6764303a5a0d166cbee80d5787b" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10-windows-x86_64.jar">
<sha256 value="ec061d4759ca7c19fe53db868c5071b3e71136d18abd5f9ebe305308d15caa2d" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10.jar">
<sha256 value="d73f0c2c0ac2f54c6963b125e06ad83c3ecc6c1083f98641296cd2c648bad27e" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-6.1.1-1.5.10.pom">
<sha256 value="3a1d5072f1a6f49e09672de57778a60c3fec86f9f213b400f9f1fd6ab9e5217b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="ffmpeg-platform" version="6.1.1-1.5.10">
<artifact name="ffmpeg-platform-6.1.1-1.5.10.jar">
<sha256 value="3aa4c953e281af016217990c4326ebe0401733048a3463a74610a76465f5d30f" origin="Generated by Gradle"/>
</artifact>
<artifact name="ffmpeg-platform-6.1.1-1.5.10.pom">
<sha256 value="64e063e095c9ac2ddaf94e89419d398b7c0d2e59fcd868e2e867189cfb6f63a5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="flycapture" version="2.13.3.31-1.5.9">
<artifact name="flycapture-2.13.3.31-1.5.9-linux-arm64.jar">
<sha256 value="17f41dae598a0818899a069f5369a457d295a124ac77e9cec615de27dac510bf" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9-linux-armhf.jar">
<sha256 value="2effed3c28413f4396c297d06202d2164c4dce254dd5f3e8339cacbbe9cce8a3" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9-linux-x86.jar">
<sha256 value="a788529c4e78b80373c94364e2f995014ecdd075a7c22c4270d38d6c06523c35" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9-linux-x86_64.jar">
<sha256 value="5431481d6faa2f940c895e0e9ff49e227cfa53f37e014dc83190b59bf8dc55b3" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9-windows-x86.jar">
<sha256 value="c585528a9d2ecfeb40629d59a1c6b668cd7721219cb95293caf4ecd26740da93" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9-windows-x86_64.jar">
<sha256 value="1a68bf5ca3b4f4c78b438b3800cfeea160e0b4078c1a6a431d37a74d3ed741ae" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9.jar">
<sha256 value="5ec13444de21369733ffdba57416921b4fa2fbc77f2302ea59342a120610e201" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-2.13.3.31-1.5.9.pom">
<sha256 value="dc9b09459108ceec971e3696e4a63586787bf6459f51c541882cf748f46eeed7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="flycapture-platform" version="2.13.3.31-1.5.9">
<artifact name="flycapture-platform-2.13.3.31-1.5.9.jar">
<sha256 value="dbd904f5e9cd0811fd3cf10cba1f819a928024f4b635964195f4c82ed704e941" origin="Generated by Gradle"/>
</artifact>
<artifact name="flycapture-platform-2.13.3.31-1.5.9.pom">
<sha256 value="3ded23320c6f35eecfd7a90084825211a232490cf26783abffa4afe4ed8ac5a4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacpp" version="1.5.10">
<artifact name="javacpp-1.5.10-android-arm64.jar">
<sha256 value="06393202f8ec8ec42e4f80b511cfb946318d641ced112cea9fff84d3dd2f7c63" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-android-x86_64.jar">
<sha256 value="d3f663d9eb9f35136459ff90b79992053c102afc39895f8f4511176a6343594a" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-ios-arm64.jar">
<sha256 value="07638d8a230f1473decb68ec9ed08e54a93c2b8d39b5ed8b956515d4aeacd773" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-ios-x86_64.jar">
<sha256 value="d621f2b168256c414e3ea1780708c21b67cabff1d031cdab0d49989b076f80ed" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-linux-arm64.jar">
<sha256 value="a7a177131972e07e22ba0c2da976dcc6ef3f31fabca1253cd5141cfe32ccd567" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-linux-ppc64le.jar">
<sha256 value="1555022947716e9410f2bccfd9349f56aef53a24749529d479ffaf026bc8d6c2" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-linux-x86_64.jar">
<sha256 value="fad4bd5eef759752c0659032b427af549c51d5809f005ede03ff46c5e7264fd8" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-macosx-arm64.jar">
<sha256 value="6f7f5022b70d51bc8f76951261b94f333989b839860deb3b8dafa9c2bbc006f6" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-macosx-x86_64.jar">
<sha256 value="4af2d8ff28908f806cea44e717162f450c679ae6704bb0dff75479687aa740a0" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10-windows-x86_64.jar">
<sha256 value="3ef632afb696fb4e447cfc4055f45de15114374c8b84e689ef5c24faab9b51ce" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10.jar">
<sha256 value="7783dd969b51d9bbec02d1711ad5830785da88aad61f18eda93b7937d949dff1" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-1.5.10.pom">
<sha256 value="0bbe96329f5bfab8a1336a5051c687bbb59c60ea4ede3db18c2a9bbe1bab9237" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacpp-platform" version="1.5.10">
<artifact name="javacpp-platform-1.5.10.jar">
<sha256 value="a809e3f70ae99f801048ecafce7996d721799fb7a93c9d71cf75bf7420f07058" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacpp-platform-1.5.10.pom">
<sha256 value="8686ee01c9b59eb2c1b2a38705a4c215096f4e5f3375e9947e5b6b6a9fd48343" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacpp-presets" version="1.5.10">
<artifact name="javacpp-presets-1.5.10.pom">
<sha256 value="4966a7236fbabb99d1d70ec67a6cfb0079ab51d7700a23c9c29a2f2e4d98a7a0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacpp-presets" version="1.5.9">
<artifact name="javacpp-presets-1.5.9.pom">
<sha256 value="24b12566401bc7723f46490bd106106ab6be9b965db5204586123344e4d12eb0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacv" version="1.5.10">
<artifact name="javacv-1.5.10.jar">
<sha256 value="8036f0d6307e251ed7147f603b34ace4280a66b931a02c02bdeaf87501f9493c" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacv-1.5.10.pom">
<sha256 value="79ae1cc4077815f2ff70d5f09cb721f590c5487a7a30f857a4cd37c495d6c17e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="javacv-platform" version="1.5.10">
<artifact name="javacv-platform-1.5.10.jar">
<sha256 value="e3f363321c59907602676939624ad8aaeb55dc5269808ae0bd162eb899fd1b36" origin="Generated by Gradle"/>
</artifact>
<artifact name="javacv-platform-1.5.10.pom">
<sha256 value="9cfdb75688a99202425b572667903a18b8ed6b52d2b1561b65798f8d2a6520e7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="leptonica" version="1.84.1-1.5.10">
<artifact name="leptonica-1.84.1-1.5.10-android-arm64.jar">
<sha256 value="6f789ab39eab5276665e41781d6f2fcaaf61ab65f00aeef4ed5a7e6b2314f627" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-android-x86_64.jar">
<sha256 value="53bd9ca74addddeaa2df45e5ed86c90830a0e40c382286fdee6b6e0f34b4b671" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-linux-arm64.jar">
<sha256 value="1bb8e9094f6e9170ce34c824ac784b94d9f50ef601489cffa7490266df56ca77" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-linux-ppc64le.jar">
<sha256 value="ddf57f9ca2d83d96ccaf1fe66e7469463ae2dbcf5b06ca72f06d8a7f9274d991" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-linux-x86_64.jar">
<sha256 value="28ea1355ddb6f1492a6b061ebf808b83882663d11d60625f0040adc582cc3422" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-macosx-arm64.jar">
<sha256 value="a99fbf3bebf55943f3e4b0c65e379da846e0ec493114d7e41b45f894537b5dab" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-macosx-x86_64.jar">
<sha256 value="3fff790fb80b24ba24e5376657c1f4060f00498e029e8ecef94c18bfc6f0067e" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10-windows-x86_64.jar">
<sha256 value="604e217edbbcd932f31ab4a8ab8465d6812b8252424d270d55a9f0eb7f4bf71e" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10.jar">
<sha256 value="9669f6e6e88ac9677f842968a20c0ae2dbc1f361e807cee05041c3bd0c25d79d" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-1.84.1-1.5.10.pom">
<sha256 value="13e4d3e01496e46357bd12b373ec0926f77af284ac0664b9c7302b5c02c0a237" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="leptonica-platform" version="1.84.1-1.5.10">
<artifact name="leptonica-platform-1.84.1-1.5.10.jar">
<sha256 value="abe64eab402b9b65b5abaf46f2cbd7f2434cba8da5883bb3d2749210d91908ba" origin="Generated by Gradle"/>
</artifact>
<artifact name="leptonica-platform-1.84.1-1.5.10.pom">
<sha256 value="6aa09a4c6bd4af072307e915c60e1986accab9a2ee3ea7a48c43bf0b57ffbfc7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libdc1394" version="2.2.6-1.5.9">
<artifact name="libdc1394-2.2.6-1.5.9-linux-arm64.jar">
<sha256 value="c2bd99e2d6df71648875c21c4853c40647ab73d10aced36e214a091048205c56" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-linux-armhf.jar">
<sha256 value="86ccde8975a9b53227b756cce45e97b514bcb2765c6713d8502b68c4dbf43345" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-linux-ppc64le.jar">
<sha256 value="bcbf872111318b0dc7bff37ceeab8521a963539ba383b28cacd357efb21a747a" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-linux-x86.jar">
<sha256 value="b66a37cbdf933e8b0dd25fe4d0f4f2e9de7e48243f871a540ab5f4c2094a9217" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-linux-x86_64.jar">
<sha256 value="e1521ddffae02de372661bb80db44dd886811ec5220373dc8b3a8711afcad117" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-macosx-x86_64.jar">
<sha256 value="19331d0b6e29e8e6db69556f8162e75e65c932596f569f3b46bab1de1ee924c8" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-windows-x86.jar">
<sha256 value="d31c17ece488a4c31151077c49fbf4074ad7f72119b48e911d63188fb95083d5" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9-windows-x86_64.jar">
<sha256 value="b334933c78c02d2f2bb270059e05ecfeecf12ef95597267cb73d86c0d596d11c" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9.jar">
<sha256 value="d712b07c9713c5b79329dbb03d9bd212df0e535ade52f1f7ae5045793accf1b9" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-2.2.6-1.5.9.pom">
<sha256 value="3e742b1dced710b14d3ace510f2aec40575e559cf71530c05277a521d88b6f30" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libdc1394-platform" version="2.2.6-1.5.9">
<artifact name="libdc1394-platform-2.2.6-1.5.9.jar">
<sha256 value="440fdbf44ed26bbda70949b59080a32f8f420ae7f9e959f9c2618c40bc96e865" origin="Generated by Gradle"/>
</artifact>
<artifact name="libdc1394-platform-2.2.6-1.5.9.pom">
<sha256 value="115da0a22bc15d07f2dd3958baf51d5b3e76a9b41eb0e7b45c7082b916eef2cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libfreenect" version="0.5.7-1.5.9">
<artifact name="libfreenect-0.5.7-1.5.9-linux-arm64.jar">
<sha256 value="bb6c9fbd8e4b64b82fe0932e88111bf8a675f55354ee15360d8716d9ef583e12" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-linux-armhf.jar">
<sha256 value="9150dd2be3816d8f32b70617c2434bc6cf559bcb9a28bc87d6ddc1ff4343bbb8" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-linux-ppc64le.jar">
<sha256 value="1e51ac42cb6ad2da704a044dc9b64f50996eae67b9478b97df6b63145cb96287" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-linux-x86.jar">
<sha256 value="eec63673641069938ebd0a25aed3795df4c72e5c087ddff6eaa1af2c9c3403d3" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-linux-x86_64.jar">
<sha256 value="86035ab00e3d645c00a8437a01a9236ec062f4c78d777e9d89314ee72a22afe6" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-macosx-x86_64.jar">
<sha256 value="74c82a4c9b5b15b98bb832cf11822701de87626f9b25ce46b9f0013c1a3e4962" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-windows-x86.jar">
<sha256 value="d96c9939cb89a248be92f69c377d8877774afda75aa46c46168eacdcf30e3402" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9-windows-x86_64.jar">
<sha256 value="6c57a0fd181c04046dd8006a380080540d4bf9c7a6d265fcfcd93d23faa73b0b" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9.jar">
<sha256 value="1a339b47f9f0091774cdcf852dd75616768b3adfb96917c6f89c24f7184d23cb" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-0.5.7-1.5.9.pom">
<sha256 value="037010561fb6f27335847cd30da4c3b0b72a678bba5912d61be6b093f9b2087a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libfreenect-platform" version="0.5.7-1.5.9">
<artifact name="libfreenect-platform-0.5.7-1.5.9.jar">
<sha256 value="7fcff98c166aefded9bb5188eacd93a21ac720d61af03bd5941f69446054578e" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect-platform-0.5.7-1.5.9.pom">
<sha256 value="053a8dfa536897c8a43bdd8971c1947ec5b2ddba30d4834f780911805cbc6b48" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libfreenect2" version="0.2.0-1.5.9">
<artifact name="libfreenect2-0.2.0-1.5.9-linux-x86.jar">
<sha256 value="3eafc819b938f1941a3bcb65ea814748e445e6ebbeccb310ff7ecf126a7572eb" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-0.2.0-1.5.9-linux-x86_64.jar">
<sha256 value="ca8b6386767bd2d40a1a72a9e2d5897970ee22ae390b6232dfb2c6f41a995328" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-0.2.0-1.5.9-macosx-x86_64.jar">
<sha256 value="12da52769a9173175cf90f53ebcebfe2e748bb0f61c2d355e1b7e027c9981646" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-0.2.0-1.5.9-windows-x86_64.jar">
<sha256 value="63721ab519ac7047b609afceb8abd1bf498bb1ad3a4e0bcef4288206cd106162" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-0.2.0-1.5.9.jar">
<sha256 value="e4239659dbd92f91262ad2131cdb8502a5095b0442756884b55ae93d34db7d2d" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-0.2.0-1.5.9.pom">
<sha256 value="6078c9bd2c28b879f066f432106d75c0d56d0024e172611283bfa98feb426519" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="libfreenect2-platform" version="0.2.0-1.5.9">
<artifact name="libfreenect2-platform-0.2.0-1.5.9.jar">
<sha256 value="b7601c863c26955e2b313b0c1fc2b75abc6012c6d04af94f724b885007ca4d19" origin="Generated by Gradle"/>
</artifact>
<artifact name="libfreenect2-platform-0.2.0-1.5.9.pom">
<sha256 value="4039338f4f48d87411f15570ea4e51b22100361149c668284c86c442310a48ba" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="librealsense" version="1.12.4-1.5.9">
<artifact name="librealsense-1.12.4-1.5.9-linux-arm64.jar">
<sha256 value="815c45348737df36061db834b3c75b71db1b476a08e46569d8a6012c8f131c56" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-linux-armhf.jar">
<sha256 value="ed7ecf4ad8ac86500785953cee68c8a5e358298b57137f8754c563cbfc3d0411" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-linux-x86.jar">
<sha256 value="1005a2ba24206a449908a1ba215f0054eacf83300646bae64b21651763334d7e" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-linux-x86_64.jar">
<sha256 value="613f4ac57a50cd04dcfc66afacb27f15d7c0fc37b391efb7ca03115533acc5c4" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-macosx-x86_64.jar">
<sha256 value="beb99414a978844b19c3a55c2a4afaa4cbdb7db1debf427a666f80241f2bcd5d" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-windows-x86.jar">
<sha256 value="a47346ed1cee23df87bf2e048bc4e5c0f805895485e23058f67d7c81696e8c74" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9-windows-x86_64.jar">
<sha256 value="234fcd45686a41a5e537b9ce54ff4d3cc56190f49ac6b1143007fddd0b1c69f6" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9.jar">
<sha256 value="1ab5ed78cb40903eb3734c1867e53403c86f63f67abee5c18bd417566fa99ee9" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-1.12.4-1.5.9.pom">
<sha256 value="eb6459543e6f8898c688ee71752065f0b8673a5dea1e84d1192b1eb745ed08e8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="librealsense-platform" version="1.12.4-1.5.9">
<artifact name="librealsense-platform-1.12.4-1.5.9.jar">
<sha256 value="aef33972337ccfe3294c517387327a0f320ecbf306a03f753e8af52542fbca1b" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense-platform-1.12.4-1.5.9.pom">
<sha256 value="b054892d209a057828035cb07281a2429859b5a18826cd364f1f2a28bd5eff73" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="librealsense2" version="2.53.1-1.5.9">
<artifact name="librealsense2-2.53.1-1.5.9-linux-arm64.jar">
<sha256 value="bc6b14e89dc45acfc99c90fc6e9a45c980b461d130b7f6152805eb6c73366565" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-linux-armhf.jar">
<sha256 value="b3f8683e9ea311bba4b2dd41e5f69240a892c99749d70afbc82702a8168c644d" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-linux-x86.jar">
<sha256 value="fd5d5fb38093831d2ea20e31597728f7e90d55cc52172d266e48ab6943ac686f" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-linux-x86_64.jar">
<sha256 value="330b8b5c1c9c1a4071d8bd832714489992afa1483a8e34d0c8e4299ae887925f" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-macosx-x86_64.jar">
<sha256 value="fa3fac1a4a183a109e069819fbe5331bfd6b3e1faa31d5e0ceadae81236a1768" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-windows-x86.jar">
<sha256 value="e1584c493d4c9156fe8ad602ac98b496d60788277095167a5db789f106a93613" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9-windows-x86_64.jar">
<sha256 value="8abab8c8c25a02e0e7a15dae1599370594e545fc6e7c4ab4c004641e407fc4a0" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9.jar">
<sha256 value="d38bc851c948ea1370483e42b3a67209d4da66b4b5ad5e1616583308fb64afbd" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-2.53.1-1.5.9.pom">
<sha256 value="9bd223287cb18f910c19bdba6b6bbefbaab01b2ce6ce1afa4a3ddd87e03f8877" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="librealsense2-platform" version="2.53.1-1.5.9">
<artifact name="librealsense2-platform-2.53.1-1.5.9.jar">
<sha256 value="cb7cb47538ca4867dcbc298bf90aedc019a8f0dceab33f3a449aaa260c625a0b" origin="Generated by Gradle"/>
</artifact>
<artifact name="librealsense2-platform-2.53.1-1.5.9.pom">
<sha256 value="d3b35d859a0c72676c7e4864559fe66203af1ba30bbd02b366d56f88f7816598" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="openblas" version="0.3.26-1.5.10">
<artifact name="openblas-0.3.26-1.5.10-android-arm64.jar">
<sha256 value="e717fe75ff2df9d539a7599fcd7864ec8d029461533fb0958a3516417c5cd451" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-android-x86_64.jar">
<sha256 value="72b047bea695bdac0828679bdc5d7276dbe5b4e8f272da391bf84b4fe8674c6b" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-ios-arm64.jar">
<sha256 value="5638b63dbe29801fc814f5d297f1c6daa52b188427823ccc8161863307c0ec25" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-ios-x86_64.jar">
<sha256 value="9d368cfd3474997e399a43db722dba2c01640d53f0e996c93299fe056d96f001" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-linux-arm64.jar">
<sha256 value="85411748d62d26cba51c14d74cb09221e9d513692fb18f462013f098456cefa3" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-linux-ppc64le.jar">
<sha256 value="11b228a1bc12c43f08ae92ea7295c31d4f851ea5b407a92c4ea1de1adc1db467" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-linux-x86_64.jar">
<sha256 value="7a8eb64c8a0ae43de9c0742b861e1ec31575bd4b0e4108014d4261b45353a34c" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-macosx-arm64.jar">
<sha256 value="e7f4e89ba4dbb63bfe0de99056f1da730028842abd31671abae29c00f2c010d5" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-macosx-x86_64.jar">
<sha256 value="6b95828cbb7b767fd158b199d4bcc21975c5fd3a1f19c648b1b66c1fa57f9e5c" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10-windows-x86_64.jar">
<sha256 value="abfdfb1b7cdf4d6453b40d63cefaa0e0fff0b4b382941a824cbeecd954d3d1bb" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10.jar">
<sha256 value="190ac4de8e3a1769f26ae9a1bfa7b1e63e119bec36563f59d269ebb7d55f7458" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-0.3.26-1.5.10.pom">
<sha256 value="bf3b63fc67c215cf8e92fd9abe496f00157cf9fac56e48cab5951b63403aebc0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="openblas-platform" version="0.3.26-1.5.10">
<artifact name="openblas-platform-0.3.26-1.5.10.jar">
<sha256 value="e178ba4c0de94bd80128d6f8b499c52be1c28a8153d246f7b871e23886e187ba" origin="Generated by Gradle"/>
</artifact>
<artifact name="openblas-platform-0.3.26-1.5.10.pom">
<sha256 value="5569060cf7766ec7e98683a2ffb896c63e06743e34c633c845e7267dfa6d7cb3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="opencv" version="4.9.0-1.5.10">
<artifact name="opencv-4.9.0-1.5.10-android-arm64.jar">
<sha256 value="13bb0bb8bdc3a5954f7e3a3279e339ad432f49afd7108a55525d7d62900a117a" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-android-x86_64.jar">
<sha256 value="0cc061c29a67ef926e4798110c1c73109289ed5d22d2f1c26bd22d7ceff5695f" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-ios-arm64.jar">
<sha256 value="ab4b394608b3385df377324a960d9bb5e3b49567d35a6de78172040511727a92" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-ios-x86_64.jar">
<sha256 value="db2f5e85306a2dbd66a61e5e854a6e762cd5f8e7df440f5e2a64b0428c1a1b65" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-linux-arm64.jar">
<sha256 value="7c984f622650a69ced1e970fdc2bc5375e52ee18ec19d39f3b927409d85e7ea4" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-linux-ppc64le.jar">
<sha256 value="5add1ba2253a02f1ea73140f6f8a8bd397288d39ef9161506be6a229908376f9" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-linux-x86_64.jar">
<sha256 value="4059a3f74fab828f9c513f14d28ac5a0ad542353d972f9c1e03c33b34bf51970" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-macosx-arm64.jar">
<sha256 value="879b1b3e88e2e0836602a5aab5cf4973cf6c739af3db4dbda2022ad2ec40bf74" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-macosx-x86_64.jar">
<sha256 value="6377855669a4727bbf218a2cc5ac0e9c1aab7631058912721484972907550b57" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10-windows-x86_64.jar">
<sha256 value="02f38d5894fe69d35ef37b78ac51ad49867bac6ab2f42318218fcf2dd4192222" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10.jar">
<sha256 value="1c02418aaf324aca1b3e882243ff35252bb5c1ab1eb5a1e58ee7f31f359536e1" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-4.9.0-1.5.10.pom">
<sha256 value="ae5187103d3c19a3a114c05f3dbc6e94b0ecffc9ce46163609d5d17efc545aed" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="opencv-platform" version="4.9.0-1.5.10">
<artifact name="opencv-platform-4.9.0-1.5.10.jar">
<sha256 value="6b6067d142fa5cccacdeb9cccfdc278a8ee820ab39e995b43e1f0505bf207777" origin="Generated by Gradle"/>
</artifact>
<artifact name="opencv-platform-4.9.0-1.5.10.pom">
<sha256 value="4a94db5d560ffa37dad5aa0b03b90c3b21c59a22a0a79be47a2e497b5a02cb7e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="tesseract" version="5.3.4-1.5.10">
<artifact name="tesseract-5.3.4-1.5.10-android-arm64.jar">
<sha256 value="d4f06883c861f4eb504ea626b99b58a5771beeb91e85a9eb3ad3ca16f8317530" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-android-x86_64.jar">
<sha256 value="5923f4d73244d1017f7227dd81b32235054358232c872ebcb3a82144e86ddef3" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-linux-arm64.jar">
<sha256 value="85eeafb396fb2df89200ecdd1509b541d60e0ccdb7bc19ec0adeba6ee460a496" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-linux-ppc64le.jar">
<sha256 value="139c20f866df32e8b842d73c58634e85a4162ba8ba37e52600f83a43a00ab500" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-linux-x86_64.jar">
<sha256 value="31cf6f87ba7d1654d17280ce214931824f4d0c173143768e59ae9d9805ee1e7d" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-macosx-arm64.jar">
<sha256 value="e80d32afd077bc64f56371b62230ec45120699aa13cf9d07c2064535a8b17754" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-macosx-x86_64.jar">
<sha256 value="3449863bcd6ee4a93b2f655b745e44b589acc4798d4b74f03055b58c08bffb70" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10-windows-x86_64.jar">
<sha256 value="b6f9d459e1bf92e084dde7e4630b7265971622012a56feac7f246ac04c152f34" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10.jar">
<sha256 value="0bf16780c4a81b6cf3a7c24c8dbfe265112fca9c9e6c7a4c65f417561ab7c04c" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-5.3.4-1.5.10.pom">
<sha256 value="d46186b5b9defa0ec9eda2975bedc9f09da908922f3d3f14adadc1384fcf4ae8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="tesseract-platform" version="5.3.4-1.5.10">
<artifact name="tesseract-platform-5.3.4-1.5.10.jar">
<sha256 value="f95cc2437aa424385038be6b147601d6f088c3041b081582312381e524b797d7" origin="Generated by Gradle"/>
</artifact>
<artifact name="tesseract-platform-5.3.4-1.5.10.pom">
<sha256 value="ea34b699b8f46bc593547937dace27fa86db3f2b36c32d8f646e3686515d3ebe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="videoinput" version="0.200-1.5.9">
<artifact name="videoinput-0.200-1.5.9-windows-x86.jar">
<sha256 value="be17d0a0ac0c5fc824274f6e2877f88b4392ff74cb88654c73baeb4ed1b66055" origin="Generated by Gradle"/>
</artifact>
<artifact name="videoinput-0.200-1.5.9-windows-x86_64.jar">
<sha256 value="aafb5be8755ab8eb2477d9b8c4a53872668040b25d1cfe2584453c8a3ba7e19a" origin="Generated by Gradle"/>
</artifact>
<artifact name="videoinput-0.200-1.5.9.jar">
<sha256 value="09cc270f22a1d49ac3166ab7a12f153362c5fdbe7febdc04704b2522af36c5e5" origin="Generated by Gradle"/>
</artifact>
<artifact name="videoinput-0.200-1.5.9.pom">
<sha256 value="2b699141ecde1d450b09a72479eae50a085a92f65f04c92c7bb8ff9bbb5fb2f9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bytedeco" name="videoinput-platform" version="0.200-1.5.9">
<artifact name="videoinput-platform-0.200-1.5.9.jar">
<sha256 value="7eb51cd56e7dd6aae24fd0137dc760994ebd3287754673d35fadc35c1a66f271" origin="Generated by Gradle"/>
</artifact>
<artifact name="videoinput-platform-0.200-1.5.9.pom">
<sha256 value="46269a5bfcd2d4d19db8c1edd06b7f4eda157ae265a0d23ddb4d07065b48ba24" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.checkerframework" name="checker-qual" version="2.11.1">
<artifact name="checker-qual-2.11.1.jar">
<sha256 value="015224a4b1dc6de6da053273d4da7d39cfea20e63038169fc45ac0d1dc9c5938" origin="Generated by Gradle"/>