naps2 icon indicating copy to clipboard operation
naps2 copied to clipboard

Javascript API

Open WeiLu67 opened this issue 2 years ago • 25 comments

Is your feature request related to a problem? Please describe. Due to many scenarios, it is necessary to call the scanner on the H5 page and obtain the corresponding blob file stream for display on the H5 page.

Describe the solution you'd like It can be run as a background service (mainly supporting Windows) and supports HTTP requests, such as scanx (This project only supports WIA and is no longer maintained)

Additional context I tried Naps and felt that it was the first and most powerful source in open source. I hope it can support more docking methods.

WeiLu67 avatar Mar 23 '23 08:03 WeiLu67

NAPS2.Sdk is a work in progress that will provide a .NET library for scanning. This could be used to build a service like that.

cyanfish avatar Mar 23 '23 22:03 cyanfish

Thanks for you reply. So, may I take the liberty to ask if this is in the future plan

WeiLu67 avatar Mar 24 '23 01:03 WeiLu67

It's not in my plans - maybe someday but there are a lot of higher priority things for me.

If anyone else wanted to build such a thing on top of NAPS2.Sdk I'd be happy to help.

cyanfish avatar Mar 24 '23 02:03 cyanfish

Thanks,i got it.

WeiLu67 avatar Mar 24 '23 02:03 WeiLu67

@cyanfish I want to start working on a javascript package that would call the SDK using http(s) use .Net Core.

I have written at most 10 lines of codes of .Net/C# to contribute PRs for small bugs in some projects. But never a full project. Since the nugget for the SDK is not yet available, I am wasting time trying to link my local project to yours. If you could help me with all this I would be very greatful.

Also, now that I read that you are interested in this. Maybe I can make a PR with new subProject Naps.WebServer that calls the SDK. Maybe the project setup would be easier.

@WeiLu67 I agree with your sentiment, this feels like the most promising library to interact with scanners

yelhouti avatar Aug 25 '23 20:08 yelhouti

@cyanfish I managed to create a controller in project with code like this:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NAPS2.Images.Gdi;
using NAPS2.Scan;
using NAPS2.Images;

namespace NAPS2.WebApi.Controllers
{

    [ApiController]
    [Route("api/[controller]")]
    public class ScannerController : ControllerBase
    {

        private readonly ILogger<ScannerController> _logger;

        private readonly ScanController _scanController;

        public ScannerController(ILogger<ScannerController> logger)
        {
            _logger = logger;
            // TODO depending on OS use GDI/GTK... (this only works for windows)
            using ScanningContext scanningContext = new ScanningContext(new GdiImageContext());
            _logger.LogInformation("scanningContext: " + scanningContext);
            _scanController = new ScanController(scanningContext);
        }

        [HttpGet("list")]
        public async Task<ActionResult<List<ScanDevice>>> ListDevices()
        {
            // TODO based on env Var change driver
            // TODO set 32 or 64 based on OS
            var devices = await _scanController.GetDeviceList(new ScanOptions { Driver = Driver.Twain, TwainOptions = { Dsm = TwainDsm.NewX64 } });
            return Ok(devices);
        }

        [HttpGet("scan/{deviceId}")]
        public async Task<ActionResult<List<ProcessedImage>>> Scan([FromRoute] string deviceId)
        {
            var devices = await _scanController.GetDeviceList(new ScanOptions { Driver = Driver.Twain, TwainOptions = { Dsm = TwainDsm.NewX64 } });
            ScanDevice device = devices.Find(d => d.ID == deviceId);
            if (device == null)
            {
                return NotFound();
            }
            var scannedImages = new List<ProcessedImage>();
            await foreach (var processedImage in _scanController.Scan(new ScanOptions { Device = device, Driver = Driver.Twain, TwainOptions = { Dsm = TwainDsm.NewX64 } }))
            {
                scannedImages.Add(processedImage);
            }
            return Ok(scannedImages);

        }
    }
}

The issue I am facing is that ListDevices fails with:

Exception thrown: 'System.DllNotFoundException' in NTwain.dll

Any idea why ?

My project config looks like:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
	<Reference Include="NTwain">
		<HintPath>..\NAPS2.Setup\lib\NTwain.dll</HintPath>
	</Reference>

	<ProjectReference Include="..\NAPS2.Sdk\NAPS2.Sdk.csproj" />
	<ProjectReference Include="..\NAPS2.Images.Gdi\NAPS2.Images.Gdi.csproj" />
	  
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>

yelhouti avatar Aug 26 '23 01:08 yelhouti

I've published a first alpha version of the SDK so you should be able to use that. There might be an issue though where you might need to set ScanOptions.TwainOptions.Dsm = TwainDsm.Old - see if that makes a difference.

cyanfish avatar Aug 26 '23 02:08 cyanfish

@cyanfish I am getting:

Tried to run TWAIN from a 64-bit process. If this is intentional, set ScanOptions.TwainOptions.Dsm to TwainDsm.NewX64. Otherwise you can set up a worker process with ScanningContext.WorkerFactory.

EDIT: fix that like this for the next person:

            using ScanningContext scanningContext = new ScanningContext(new GdiImageContext());
            scanningContext.WorkerFactory = WorkerFactory.CreateDefault();
            scanningContext.WorkerFactory.Init(scanningContext);
            _logger.LogInformation("YEH scanningContext: " + scanningContext);
            _scanController = new ScanController(scanningContext);

Now I am working on another issue:

Exception thrown: 'System.ComponentModel.Win32Exception' in System.Diagnostics.Process.dll Probably a bug in the Win32 Worker.

EDIT2: code seems to fail when calling native worker:

FileName=".<...>\naps2\NAPS2.WebApi\bin\Debug\net6.0\NAPS2.exe", Arguments="worker 12320", WorkingDirectory=""

This fails because exe is not present there, I don't know why it's looking for it there, but I'll try to fix that

yelhouti avatar Aug 26 '23 10:08 yelhouti

Sorry yes that message needs updating. I would recommend using the nugets, not sure how easy it would be to get the exe working otherwise.

Instructions are here https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/TwainSample.cs

cyanfish avatar Aug 26 '23 13:08 cyanfish

I tried the sample code with nuggets it fails because of the bug mentioned before:

Tried to run TWAIN from a 64-bit process. If this is intentional, set ScanOptions.TwainOptions.Dsm to TwainDsm.NewX64. Otherwise you can set up a worker process with ScanningContext.WorkerFactory.

When I try to use my code using the factory and the nuggets, it doesn't work because NAPS2.Remoting.Worker is not published.

            scanningContext.WorkerFactory = WorkerFactory.CreateDefault();
            scanningContext.WorkerFactory.Init(scanningContext);

Could you provide a project that works with nuggets, or a sample with Main that i can easily run ? https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/ doesn't have a main so i don't know how to start them (again not to much experience with C#) ?

yelhouti avatar Aug 26 '23 16:08 yelhouti

If you look at the sample I linked it explains, see here: https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/TwainSample.cs

Specifically you just need to call scanningContext.SetUpWin32Worker().

cyanfish avatar Aug 26 '23 17:08 cyanfish

@cyanfish thanks a lot for adding that sample it really helped, I was able to scan documents. The only thing left now is to export them as jpeg.

How do I make sure to always return scan in JPEG ?

Now, I am using this:

private string processedImageToBase64(ProcessedImage processedImage)
{
    byte[] byteArray;
    using (MemoryStream stream = new MemoryStream())
    {
        processedImage.RenderToBitmap().Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);
        byteArray = stream.ToArray();
    }

    // Convert the byte array to a Base64 string
    return Convert.ToBase64String(byteArray);
}

But it doesn't feel right

yelhouti avatar Aug 26 '23 21:08 yelhouti

That should work but I would recommend this (which avoids using the GDI-specific Bitmap type):

private string processedImageToBase64(ProcessedImage processedImage)
{
    var stream = processedImage.Render().SaveToMemoryStream(ImageFileFormat.Jpeg);
    return Convert.ToBase64String(stream.ToArray());
}

I'll also mention that you may or may not want to use base64; certainly it's a reasonable option (and works nicely with JSON), but for large images it might be more efficient to return the binary data and have the Javascript read it as a blob.

cyanfish avatar Aug 26 '23 22:08 cyanfish

Thanks for all the tips, but how would I return multiple blobs ?

yelhouti avatar Aug 26 '23 22:08 yelhouti

Yeah that would be tougher, you might need to do something like have the server be stateful (e.g. store the images or scanning state in a static session variable and have a separate action call to get each image).

Anyway, don't let me stop you from doing the base64 way if that's easier to get something working, maybe could do the other way later down the road.

cyanfish avatar Aug 26 '23 22:08 cyanfish

@cyanfish I have a working server now, do you want me to make a PR for Naps2.WebApi so that other can contribute, make it cross paltforms, allow more params...

Or should I create a new project ?

yelhouti avatar Aug 26 '23 22:08 yelhouti

I would recommend creating your own project.

cyanfish avatar Aug 26 '23 23:08 cyanfish

For anyone interested here is a working server for windows/TWAI, PR as more than welcome for other drivers/OSs.

I will add a README and releases later. For now it can be build locally into an MSI and installed wherever you want.

https://github.com/tailosoft/scan-server

yelhouti avatar Aug 27 '23 17:08 yelhouti

Cool, that's great! And good to see the SDK is already getting some use.

I would suggest adding a license file to the project. If you like you can use the LGPL which NAPS2.Sdk uses.

I also just published NAPS2.Images.ImageSharp so you can use ImageSharpImageContext instead of GdiImageContext if you like - that will work across all platforms without needing any extra work.

cyanfish avatar Aug 27 '23 21:08 cyanfish

@cyanfish ImageSharp doesn't seem to work in my project, you can try it. It fails to save for same reason because of an unimplemented method if I read correctly.

Also, When a create a windows service and run it, for some reason, the endpoints specific to the scanner hangs indefinitely, I am suspecting it is because of SetUpWin32Worker Could you please have a look ? Thanks in advance

yelhouti avatar Aug 28 '23 04:08 yelhouti

Yeah I see a couple ImageSharp issues, I'll try and fix those at some point. For the service I don't know what the issue is but it's not the worker; scanning with WIA (and no worker) has the same problem.

cyanfish avatar Aug 29 '23 02:08 cyanfish

@cyanfish I found that the issue happens at the gRPC it looks like the server hangs waiting for a response. This looks like some locking issue that happens only when in Windows Service. Your help would be much appreciated

yelhouti avatar Aug 29 '23 17:08 yelhouti

That means the issue is somewhere in the actual scanning process (which happens in the worker), it doesn't really narrow it down past that. It would be easier for you to diagnose the problem without the worker.

Does your scanner work with WIA (instead of TWAIN)? If so that would avoid the worker.

Alternatively you can try building your server exe as 32-bit (i.e. win-x86 instead of win-x64). Then TWAIN would work without the worker.

cyanfish avatar Aug 29 '23 17:08 cyanfish

@cyanfish thanks for pointing me to WIA, I should have written here this morning. You rock

yelhouti avatar Aug 29 '23 19:08 yelhouti

No problem. Also ImageSharpImageContext should work now with 0.1.0-alpha3.

cyanfish avatar Aug 30 '23 04:08 cyanfish

If anyone cares to try there is an ESCL JS/TS library, which should connect natively to NAPS2's scanner-sharing server.

You'd need to set this flag on the server (added in NAPS2.Sdk 1.1.1):

using var scanServer = new ScanServer(scanningContext, new EsclServer
{
    SecurityPolicy = EsclSecurityPolicy.ServerAllowAnyOrigin
});

And you'll want to manually specify a port as discovery doesn't work from the browser:

scanServer.RegisterDevice(device, port: 9880);

Then the JS code would look something like this (assuming the scanner-sharing server is running on the same machine as the web browser, e.g. in a background service):

const ip = '127.0.0.1'
const port = '9880'
const scanner = new Scanner({ ip, port })

cyanfish avatar Aug 17 '24 05:08 cyanfish

This does seem to be working (with NAPS2.Sdk 1.1.2 and a lightly modified version of escl-sdk-ts). I've created the naps2-webscan project as the canonical sample/documentation for this.

I now consider this as the recommended approach for scanning from a web browser with NAPS2, and I'll close this issue. Feel free to open issues or PRs on the naps2-webscan project.

cyanfish avatar Aug 17 '24 22:08 cyanfish