Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 157 additions & 60 deletions src/device/scannerdevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
#include <QDir> // For separator
#include <QProcess> // For QProcess
#include <QPainter>
#include <QDateTime>

Check warning on line 15 in src/device/scannerdevice.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDateTime> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QStandardPaths>

Check warning on line 16 in src/device/scannerdevice.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QStandardPaths> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QMetaType>

Check warning on line 17 in src/device/scannerdevice.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QMetaType> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QElapsedTimer>

Check warning on line 18 in src/device/scannerdevice.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QElapsedTimer> not found. Please note: Cppcheck does not need standard library headers to get proper results.

using namespace DDLog;

Expand All @@ -41,6 +42,7 @@
connect(this, &ScannerDevice::triggerCloseDevice, m_worker, &ScannerWorker::doCloseDevice);
connect(this, &ScannerDevice::triggerStartScan, m_worker, &ScannerWorker::doStartScan);
connect(this, &ScannerDevice::triggerCancelScan, m_worker, &ScannerWorker::doCancelScan);
connect(this, &ScannerDevice::triggerGetAvailableDevices, m_worker, &ScannerWorker::doGetAvailableDevices);

// --- Connect signals from worker back to this (UI) thread's ddslots ---
connect(m_worker, &ScannerWorker::errorOccurred, this, &ScannerDevice::onWorkerError);
Expand All @@ -51,8 +53,9 @@
emit deviceUnavailable(deviceName);
});
connect(m_worker, &ScannerWorker::deviceUnavailable, this, &ScannerDevice::deviceUnavailable);
connect(m_worker, &ScannerWorker::availableDevicesReady, this, &ScannerDevice::onAvailableDevicesReady);
connect(m_worker, &ScannerWorker::captureCompleted, this, &ScannerDevice::onCaptureCompleted);
connect(m_worker, &ScannerWorker::scanProgress, this, &ScannerDevice::scanProgress); // Forward signal
connect(m_worker, &ScannerWorker::captureCompletedImage, this, &ScannerDevice::onCaptureCompletedImage);

m_workerThread.start();
qCInfo(app) << "Scanner worker thread started.";
Expand Down Expand Up @@ -86,42 +89,26 @@

QStringList ScannerDevice::getAvailableDevices()
{
// This is synchronous for now, but could be moved to the worker if it proves slow.
#ifndef _WIN32
const SANE_Device **device_list;
QStringList deviceNames;

SANE_Status status = sane_get_devices(&device_list, SANE_FALSE);
if (status != SANE_STATUS_GOOD) {
emit errorOccurred(tr("Failed to get device list: %1").arg(sane_strstatus(status)));
return deviceNames;
}
// The SANE enumeration runs in the worker thread, see requestAvailableDevices(): it can
// block for seconds on network backends, which must not happen on the UI thread.
// Callers get the last list reported by the worker.
return m_availableDevices;
}

if (device_list) {
for (int i = 0; device_list[i] != nullptr; ++i) {
if (device_list[i]->name) {
deviceNames.append(QString::fromUtf8(device_list[i]->name));
}
}
}

if (deviceNames.isEmpty() && ADD_TEST_DEVICE) {
deviceNames.append("test:0");
}
void ScannerDevice::requestAvailableDevices()
{
emit triggerGetAvailableDevices();
}

return deviceNames;
#else
if (ADD_TEST_DEVICE) {
return QStringList() << "test:0";
}
return QStringList();
#endif
void ScannerDevice::onAvailableDevicesReady(const QStringList &deviceNames)
{
m_availableDevices = deviceNames;
emit availableDevicesReady(deviceNames);
}

bool ScannerDevice::openDevice(const QString &deviceName)
{
m_currentDeviceName = deviceName;
m_deviceOpening = true;
emit triggerOpenDevice(deviceName);
return true; // Asynchronous operation, success is reported via signal
}
Expand Down Expand Up @@ -263,7 +250,6 @@
void ScannerDevice::onWorkerError(const QString &errorMessage)
{
m_isCapturing = false;
m_deviceOpening = false;
if (!m_deviceOpen) {
// The error came from an open attempt: no open is in flight any more, so drop
// the queued scan request together with the device name that marks it.
Expand All @@ -277,7 +263,6 @@
void ScannerDevice::onDeviceOpened(const QList<int> &resolutions, const QList<ScannerDevice::ScanMode> &modes)
{
m_deviceOpen = true;
m_deviceOpening = false;
m_supportedResolutions = resolutions;
m_supportedScanModes = modes;

Expand Down Expand Up @@ -344,6 +329,14 @@
}


void ScannerDevice::onCaptureCompletedImage(const QImage &image)
{
m_isCapturing = false;
setState(Connected);

emit imageCaptured(image);
}

// =================================================================
// ScannerWorker Implementation (Worker Thread)
// =================================================================
Expand All @@ -361,11 +354,6 @@

ScannerWorker::ScannerWorker() : QObject(nullptr)
{
// Set SANE_DEBUG env vars for more info
qputenv("SANE_DEBUG_DLL", "2");
qputenv("SANE_DEBUG_NET", "2");
qputenv("SANE_DEBUG_HPAIO", "2");
qputenv("SANE_DEBUG_USB", "2");
}

ScannerWorker::~ScannerWorker()
Expand All @@ -378,6 +366,37 @@
}


void ScannerWorker::doGetAvailableDevices()
{
#ifndef _WIN32
const SANE_Device **device_list = nullptr;
QStringList deviceNames;

SANE_Status status = sane_get_devices(&device_list, SANE_FALSE);
if (status != SANE_STATUS_GOOD) {
emit errorOccurred(tr("Failed to get device list: %1").arg(sane_strstatus(status)));
emit availableDevicesReady(deviceNames);
return;
}

if (device_list) {
for (int i = 0; device_list[i] != nullptr; ++i) {
if (device_list[i]->name) {
deviceNames.append(QString::fromUtf8(device_list[i]->name));
}
}
}

if (deviceNames.isEmpty() && ADD_TEST_DEVICE) {
deviceNames.append("test:0");
}

emit availableDevicesReady(deviceNames);
#else
emit availableDevicesReady(ADD_TEST_DEVICE ? QStringList() << "test:0" : QStringList());
#endif
}

void ScannerWorker::doOpenDevice(const QString &deviceName)
{
#ifndef _WIN32
Expand All @@ -395,8 +414,12 @@

m_deviceName = deviceName;

QElapsedTimer openTimer;
openTimer.start();

QByteArray deviceNameBytes = deviceName.toUtf8();
SANE_Status status = sane_open(deviceNameBytes.constData(), &m_device);
const qint64 openMs = openTimer.elapsed();

if (status != SANE_STATUS_GOOD) {
emit errorOccurred(tr("Failed to open SANE device '%1': %2").arg(deviceName).arg(sane_strstatus(status)));
Expand All @@ -411,7 +434,9 @@
}

m_deviceOpen = true;
openTimer.restart();
updateSupportedOptions();
qCInfo(app) << "Worker: Device" << deviceName << "opened in" << openMs << "ms, options read in" << openTimer.elapsed() << "ms";
qCDebug(app) << "Worker: Device" << deviceName << "opened successfully.";
#else
m_usingTestDevice = true;
Expand Down Expand Up @@ -451,16 +476,20 @@
m_device = nullptr;
m_deviceOpen = false;

QElapsedTimer openTimer;
openTimer.start();

QByteArray deviceNameBytes = m_deviceName.toUtf8();
SANE_Status status = sane_open(deviceNameBytes.constData(), &m_device);
if (status != SANE_STATUS_GOOD) {
qCWarning(app) << "Worker: Failed to re-open device" << m_deviceName << ":" << sane_strstatus(status);
qCWarning(app) << "Worker: Failed to re-open device" << m_deviceName << "after" << openTimer.elapsed() << "ms:" << sane_strstatus(status);
m_device = nullptr;
m_deviceOpen = false;
return false;
}

m_deviceOpen = true;
qCInfo(app) << "Worker: Device" << m_deviceName << "re-opened in" << openTimer.elapsed() << "ms";
return true;
}

Expand Down Expand Up @@ -526,6 +555,9 @@
return;
}

QElapsedTimer scanTimer;
scanTimer.start();

doSetScanMode(mode);
doSetColorMode(colorMode);
doSetResolution(dpi);
Expand All @@ -537,8 +569,13 @@

m_scanCancelled = false;

const qint64 optionsMs = scanTimer.elapsed();
scanTimer.restart();

// Start the scan
SANE_Status status = sane_start(m_device);
const qint64 startMs = scanTimer.elapsed();
scanTimer.restart();
if (status != SANE_STATUS_GOOD) {
reportScanFailure(tr("Failed to start scan: %1").arg(sane_strstatus(status)));
return;
Expand All @@ -552,9 +589,13 @@
return;
}

status = scan_it(ofp); // The blocking call
QImage scannedImage;
status = scan_it(ofp, scannedImage); // The blocking call
const qint64 readMs = scanTimer.elapsed();
fclose(ofp);

qCInfo(app) << "Worker: scan timing: options" << optionsMs << "ms, sane_start" << startMs << "ms, read" << readMs << "ms";

if (m_scanCancelled) {
QFile::remove(tempOutputFilePath);
// Don't emit error, as it's a user action
Expand All @@ -567,6 +608,10 @@
if (status != SANE_STATUS_GOOD && status != SANE_STATUS_EOF) {
QFile::remove(tempOutputFilePath);
reportScanFailure(tr("Scan failed during read: %1").arg(sane_strstatus(status)));
} else if (!scannedImage.isNull()) {
// The page is already an image, the temp file stayed empty.
QFile::remove(tempOutputFilePath);
emit captureCompletedImage(scannedImage);
} else {
emit captureCompleted(tempOutputFilePath);
}
Expand Down Expand Up @@ -761,7 +806,29 @@


#ifndef _WIN32
SANE_Status ScannerWorker::scan_it(FILE *ofp)
// The QImage format that matches the SANE frame, or Format_Invalid when the page has to go
// through the PNG temp file: lineart is bit packed and JPEG/G42D are not raw pixels.
static QImage::Format imageFormatFor(const SANE_Parameters &parm)
{
if (parm.format == SANE_FRAME_GRAY) {
if (parm.depth == 8) {
return QImage::Format_Grayscale8;
}
if (parm.depth == 16) {
return QImage::Format_Grayscale16;
}
return QImage::Format_Invalid;
}
if (parm.format == SANE_FRAME_RGB && parm.depth == 8) {
return QImage::Format_RGB888;
}
return QImage::Format_Invalid;
}

// Reads the page. When the frame format can be handed over as an image, `image` is filled
// and nothing is written to `ofp`; otherwise the page is written there as PNG, which is
// also what happens when the backend does not tell the line count in advance.
SANE_Status ScannerWorker::scan_it(FILE *ofp, QImage &image)
{
SANE_Parameters parm;
SANE_Status status;
Expand All @@ -776,40 +843,70 @@

unsigned char *buffer = new unsigned char[parm.bytes_per_line];

// Dummy PNG write header function.
// Replace with your actual implementation if you have one.
int bit_depth = (parm.depth == 1) ? 8 : parm.depth;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
info_ptr = png_create_info_struct(png_ptr);
png_init_io(png_ptr, ofp);
png_set_IHDR(png_ptr, info_ptr, parm.pixels_per_line, parm.lines, bit_depth,
parm.format == SANE_FRAME_RGB ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_GRAY,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
// End dummy png header

int total_lines = parm.lines > 0 ? parm.lines : 1000; // Guess if not provided
int lines_read = 0;
QImage::Format imageFormat = QImage::Format_Invalid;
if (parm.lines > 0) {
imageFormat = imageFormatFor(parm);
if (imageFormat != QImage::Format_Invalid) {
image = QImage(parm.pixels_per_line, parm.lines, imageFormat);
if (image.isNull()) {
imageFormat = QImage::Format_Invalid;
}
}
}

const bool writePng = imageFormat == QImage::Format_Invalid;
if (writePng) {
// Dummy PNG write header function.
// Replace with your actual implementation if you have one.
int bit_depth = (parm.depth == 1) ? 8 : parm.depth;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
info_ptr = png_create_info_struct(png_ptr);
png_init_io(png_ptr, ofp);
png_set_IHDR(png_ptr, info_ptr, parm.pixels_per_line, parm.lines, bit_depth,
parm.format == SANE_FRAME_RGB ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_GRAY,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
// End dummy png header
}

QElapsedTimer readTimer;
readTimer.start();
int linesRead = 0;

do {
if (m_scanCancelled) {
status = SANE_STATUS_CANCELLED;
break;
}

if (!writePng && linesRead >= parm.lines) {
break; // the image is complete
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
}

status = sane_read(m_device, buffer, parm.bytes_per_line, &len);

if (status == SANE_STATUS_GOOD) {
png_write_row(png_ptr, buffer);
lines_read++;
if (parm.lines > 0) {
emit scanProgress((lines_read * 100) / total_lines);
if (writePng) {
png_write_row(png_ptr, buffer);
} else {
memcpy(image.scanLine(linesRead), buffer, qMin(len, image.bytesPerLine()));
}
linesRead++;
}
} while (status == SANE_STATUS_GOOD);

png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
if (!writePng && linesRead > 0 && linesRead < parm.lines) {
// The backend stopped early, keep only the lines that arrived.
image = image.copy(0, 0, image.width(), linesRead);
}

qCInfo(app) << "Worker: read" << linesRead << "of" << parm.lines << "lines in" << readTimer.elapsed() << "ms"
<< (writePng ? "(via PNG temp file)" : "(direct image)");

if (writePng) {
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
}
delete[] buffer;

if (status != SANE_STATUS_EOF) {
Expand Down
Loading
Loading