dynarithmic / twain_library

Dynarithmic TWAIN Library, Version 5.x
Apache License 2.0
60 stars 25 forks source link

DTWAIN_GetSourceAcquisitions() returning NULL #13

Closed Dcreeron closed 3 years ago

Dcreeron commented 4 years ago

I've successfully set up my code to use the DTWAIN_AcquireBuffered() method so I can capture the strips and display the image as it's scanned. However, all attempts to get the final DIB are failing.

I'm using MODELESS, with a Callback.

At first I was trying to use the result of the DTWAIN_AcquireBuffered() method thinking that had the DTWAIN_ARRAY, and then I saw that I needed to wait for the DTWAIN_TN_ACQUIREDONE message.

Now when I get the DTWAIN_TN_ACQUIREDONE message I try and call DTWAIN_GetSourceAcquisitions() but it always returns NULL. If I step into the call p->GetAcquisitionArray() returns NULL. [m_aAcqAttempts == 0x00000000]

DTWAIN_ARRAY DLLENTRY_DEF DTWAIN_GetSourceAcquisitions(DTWAIN_SOURCE Source) { LOG_FUNC_ENTRY_PARAMS((Source)) CTL_TwainDLLHandle pHandle = static_cast<CTL_TwainDLLHandle >(GetDTWAINHandle_Internal());

// See if DLL Handle exists
DTWAIN_Check_Bad_Handle_Ex(pHandle, NULL, FUNC_MACRO);
CTL_ITwainSource *p = VerifySourceHandle(pHandle, Source);
if (!p)
    LOG_FUNC_EXIT_PARAMS(NULL)
DTWAIN_ARRAY Array = p->GetAcquisitionArray();
**if (!Array)
    LOG_FUNC_EXIT_PARAMS(NULL)**
LOG_FUNC_EXIT_PARAMS(Array)
CATCH_BLOCK(DTWAIN_ARRAY(0))

}

Stepping into AddDibsToAcquisition() shows that m_aAcqAttempts is NULL initially; aDibs has a valid address.

Am I missing something regarding initializing this array or retrieving the DIB(s) after an Acquire? I've tried using just DTWAIN_AcquireNative() as well.

POTENTIAL ISSUE/AREA OF THE PROBLEM (?): If I put a break-point at line 281 in ctlsourceacquire.cpp -> SourceAcquireWorkerThread():

 if (DTWAIN_LLAcquireNative(opts) == -1L)

Then SET NEXT to line 323:

 pSource->ResetAcquisitionAttempts(aAcquisitionArray);

And execute that line so that m_aAcqAttempts is set, and then SET NEXT back up to line 281. At this point I can continue and the call to DTWAIN_GetSourceAcquisitions() works, returns a valid address, and the subsequent calls to DTWAIN_ArrayGetCount(), DTWAIN_GetNumAcquiredImages(), and DTWAIN_GetAcquiredImage() all appear to work as expected.

NOTE: In this 'hack' the m_PersistentArray in AddDibsToAcquisition() is still NULL.

Thanks.

Daniel

Dcreeron commented 4 years ago

If I add another call to ResetAcquisitionAttempts() after the DTWAIN_ArrayCreate(), like below, it has the same effect as my 'hack' with the breakpoints above.

DTWAIN_ARRAY dynarithmic::SourceAcquireWorkerThread(SourceAcquireOptions& opts) { LOG_FUNC_ENTRY_PARAMS((opts)) DTWAIN_ARRAY Array = NULL; DTWAIN_ARRAY aAcquisitionArray = NULL;

DTWAINArrayLL_RAII a1(Array);
DTWAINArrayLL_RAII aAcq(aAcquisitionArray);

CTL_ITwainSource *pSource = VerifySourceHandle(opts.getHandle(), opts.getSource());
pSource->ResetAcquisitionAttempts(NULL);
aAcquisitionArray = (DTWAIN_ARRAY)DTWAIN_ArrayCreate(DTWAIN_ArrayTypePTR, 0);
pSource->ResetAcquisitionAttempts(aAcquisitionArray);

pSource->m_pUserPtr = NULL;
dynarithmic commented 4 years ago

Thanks, I will take a closer look and get back to you. In the meantime, can you put together a simple application that demonstrates the issue? I want to make sure that I can duplicate the issue, since getting buffered strips, if not simply using the default methods, can be highly customized.

dynarithmic commented 4 years ago

I believe I've found the root cause. I ran a test program, and the issue seems to only appear when acquiring to files. Acquiring to images in memory does not have these issues.

The issue seems to be in ctlacquirefile.cpp, in function bool dynarithmic::AcquireFileHelper(SourceAcquireOptions& opts, LONG AcquireType)'

That function basically resets all the dibs to NULL, since for file retrieval, there are no DIBs to handle. It still is broken however, for modeless processing and attempting to grab the images after the acquisition is done.

You can try the following changes:

bool dynarithmic::AcquireFileHelper(SourceAcquireOptions& opts, LONG AcquireType)
{
    LOG_FUNC_ENTRY_PARAMS((opts))
    DTWAIN_ARRAY aDibs = 0;
    CTL_ITwainSource *pSource = VerifySourceHandle(GetDTWAINHandle_Internal(), opts.getSource());

    // Check if file type requires a loaded DLL
    DumpArrayContents(opts.getFileList(), 0);
    opts.setAcquireType(AcquireType);
    opts.setDiscardDibs(true); // make sure we remove acquired dibs for file handling
    aDibs = SourceAcquire(opts);
    if (opts.getStatus() < 0 && !aDibs)
    {
        LOG_FUNC_EXIT_PARAMS(false)
    }

    bool bRetval = false;
    if (aDibs)
    {
        bRetval = TRUE;
        if (DTWAIN_GetTwainMode() == DTWAIN_MODAL)
        {
            auto vDibs = EnumeratorVectorPtr<LPVOID>(aDibs);
            if (vDibs)
                for_each(begin(*vDibs), end(*vDibs), EnumeratorFunctionImpl::EnumeratorDestroy);
            pSource->ResetAcquisitionAttempts(nullptr);
            if (EnumeratorFunctionImpl::EnumeratorIsValid(aDibs))
                EnumeratorFunctionImpl::EnumeratorDestroy(aDibs);
        }
    }
    if (DTWAIN_GetTwainMode() == DTWAIN_MODAL)
    {
        if (!aDibs)
            bRetval = false;
        else
        if (opts.getStatus() == DTWAIN_TN_ACQUIREDONE)
            bRetval = true;
    }
    else
    if (DTWAIN_GetTwainMode() == DTWAIN_MODELESS)
        pSource->m_pUserPtr = nullptr;

    LOG_FUNC_EXIT_PARAMS(bRetval)
    CATCH_BLOCK(false)
}

Now the issue is whether this will cause a leak of DIBs once the file retrieval is done. I will need to debug further, but the above seems to be a workaround for now.

Dcreeron commented 4 years ago

I'll try this a little later tonight, but if you think this only solves Acquire to file and Modal then it probably won't work. I'm using Modeless and AcquireNative() / AcquireBuffered().

The AcquireNative() was just for testing trying to resolve this issue.

Ideally I want to use AcquireBuffered() so that I can display the images as they are scanned and then save them out. Reading the docs I think in the end I'll use the AcquireBufferedEx() so I can save each image after it's scanned, but I haven't gotten that far yet.

After either AcquireNative/AcquireBuffered I can't get at the DIBs to save them without the workaround I mentioned earlier. With the workaround it looks like the DIBs are available, but I haven't gotten to writing the code to save them out yet (FreeImage or my existing code). Maybe I'll try that tonight as well.

Dcreeron commented 4 years ago

The method you mentioned, AcquireFileHelper, isn't called in the AcquireNative() workflow.

Looks like you modified the declaration of the method, the code I have is:

static bool AcquireFileHelper(SourceAcquireOptions& opts)

So I'd need to know what else you changed (i.e., where is AcquireType set/etc,)

dynarithmic commented 4 years ago

I ran a quick test for simple AcquireNative() using modeless mode, and I was able to call DTWAIN_GetSourceAcquisitions() on the DTWAIN_TN_ACQUIREDONE notification without an issue, where the handle being returned is not null. The only issue was with using the file acquisition and calling DTWAIN_GetSourceAcquisitions(), from the test I ran. That is what the code in the previous comment was related to.

Maybe you should put together a simple application, since I cannot duplicate the issue using DTWAIN_AcquireNative or similarly, DTWAIN_AcquireBuffered.

dynarithmic commented 4 years ago

Around line 1056 in ctlwsrc.cpp, in function CTL_ITwainSource::ResetAcquisitionAttempts:

if ( aNewAttempts != m_aAcqAttempts)

See what calls that line, and look at the arguments being passed. The aNewAttempts should not be null on the final time this is called (it will be set to null to initialize, but should be called a second time to set the value). If for some reason this function is called and somehow resets the m_aAcqAttempts back to null before the acquisition is completed, then take a look at the call stack and see which function is (re)setting the value to null.

Dcreeron commented 4 years ago

It's pretty much what I said in my first post:

***NOTE: This is all using AcquireNative()

The first time ResetAcquisitionAttempts() is called aNewAttempts == NULL. It comes from SourcedAcquired() -> SourceAcquireWorkerThread() [ctlsourceacquire.cpp - line 236]

The code then hits my breakpoint (after the scanner grabs the page) in AddDibsToAcquisition(). [ctlwsrc.cpp - line 1049] aDibs is a valid address; m_aAcqAttempts is null. THIS IS THE PROBLEM, there's nowhere to place the DIBs This comes from ctlwproc.cpp -> DTWAIN_WindowProc() line 517.

Then the DTWAIN_TN_ACQUIREDONE notification is sent. I handle this in my callback and call a method to try and get at the DIBs (which is NULL).

I have another breakpoint on ctlsourceacquire.cpp - line 319 (may be off by one because of the other line I added -- will talk about that in a minute). ==> if (Array != NULL) // This is an immediate return

The second time ResetAcquisitionAttempts() is call is from line 324 just after this breakpoint ==> pSource->ResetAcquisitionAttempts(aAcquisitionArray). This correctly sets the m_aAcqAttempts to the new attempt, but it's too late, nothing else tries to add the DIBs to this, so it remains DIB-less.

Process is done now.

If I add a line in ctlsourceacquire.cpp to call ResetAcquisitionAttempts() just after line 235 so it looks like this:

aAcquisitionArray = (DTWAIN_ARRAY)DTWAIN_ArrayCreate(DTWAIN_ArrayTypePTR, 0);
**pSource->ResetAcquisitionAttempts(aAcquisitionArray);**

Then when AddDibsToAcquisition(). [ctlwsrc.cpp - line 1049] is called the DIBs are correctly added to the current acquisition and I can access them after the DTWAIN_TN_ACQUIREDONE notification is sent.

So either something like this needs to happen, or the block below needs to be repositioned (?):

if (Array != NULL)  // This is an immediate return
{
    // turn off RAII here
    a1.Disconnect();
    aAcq.Disconnect();
    pSource->ResetAcquisitionAttempts(aAcquisitionArray);
    p->m_lLastAcqError = DTWAIN_TN_ACQUIRESTARTED;
    opts.setStatus(DTWAIN_TN_ACQUIRESTARTED);
    pSource->m_pUserPtr = Array;
    LOG_FUNC_EXIT_PARAMS(Array)
}

pSource->ResetAcquisitionAttempts(aAcquisitionArray);

When we solve this I'd like to pick your brains on the best way to save images to TIFF/etc. using the DTWAIN library (using the in-memory DIBs from the acquisition).

dynarithmic commented 4 years ago

ok. I will take a further look. When you mention "callback", are you referring to using DTWAIN_SetCallback, DTWAIN_SetCallback64, or are you using the straight window procedure that is hooked? If you are not using DTWAIN_SetCallback/DTWAIN_SetCallback64, use that and see if this gives different results. The reason is at some point, the window procedure method may be deprecated in a future version.

Dcreeron commented 4 years ago

I'm using the DTWAIN_SetCallback:

    DTWAIN_SetCallback(DTWAINCallbackProc, (LONG)(this));

The callback seems to be working as expected.

dynarithmic commented 4 years ago

The following small program does not produce the results you're seeing. This is running with the latest version, 5.1.0.9.

The DTWAIN_TN_ACQUIREDONE does not assert. Maybe there is something different in your program than in the sample below?

#include <dtwain.h>
#include <stdio.h>
#include <assert.h>

LRESULT CALLBACK MyCallback(WPARAM w, LPARAM lp, LONG lng)
{
    switch (w)
    {
        case DTWAIN_TN_ACQUIREDONE:
        {
            DTWAIN_ARRAY acq = DTWAIN_GetSourceAcquisitions((DTWAIN_SOURCE)lp);
            assert(acq != NULL);
            LONG num = DTWAIN_ArrayGetCount(acq);
            assert(num > 0);
            HANDLE h = DTWAIN_GetAcquiredImage(acq, 0, 0);
            assert(h != NULL);
        }
       break;
    }
    return 1;
}

int main()
{
    DTWAIN_HANDLE isInitialized = DTWAIN_SysInitialize();
    DTWAIN_SOURCE Source;
    if (!isInitialized)
    {
        printf("Could not initialize DTWAIN library");
        return -1;
    }
    DTWAIN_SetTwainMode(DTWAIN_MODELESS);
    DTWAIN_EnableMsgNotify(TRUE);
    DTWAIN_SetCallback(MyCallback, 0);
    Source = DTWAIN_SelectSource();
    if (Source)
        DTWAIN_AcquireNative(Source, DTWAIN_PT_BW, DTWAIN_MAXACQUIRE, TRUE, TRUE, NULL);

    /* Main message loop: */
    {
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0) && DTWAIN_IsSourceAcquiring(Source))
        {
            if (!DTWAIN_IsTwainMsg(&msg))  // send message to TWAIN if DTWAIN message
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
    }
    DTWAIN_SysDestroy();
}
Dcreeron commented 4 years ago

I had two things, slightly different:

1) When initializing TWAIN I had two calls to DTWAIN_SysInitialize() like this: _dtwainHandle = DTWAIN_SysInitialize(); DTWAIN_SysInitialize();

2) In my "Scan" button code I had a call to DTWAIN_StartTwainSession() like this:

DTWAIN_BOOL twainSessionStarted = DTWAIN_StartTwainSession((HWND)this->winId(), NULL);
if (!twainSessionStarted)
{
    LogError("Failed to start TWAIN session: " + GetDTWAINErrorString());

    QMessageBox::warning(this, "Scan Failed", "Failed to start TWAIN scan session. Canceling scan.", QMessageBox::Ok);
    return;
}

I was calling DTWAIN_StartTwainSession() because it sounded like I needed to in order to have progress windows/etc. be parented to the correct window (that doesn't seem to work... the page scan progress indication dialog isn't parented to the window).

When I comment out these two things I no longer need my workaround of the additional call to ResetAcquisitionAttempts().

Not that it should matter, but I should have stated earlier, I'm using Qt for this application.

I'm testing now to see which of the two differences above is responsible for the issue I was seeing.

dynarithmic commented 4 years ago

Qt is fine. I was able to use it for a demo I had done on my local machine.

The DTWAIN_SysInitialize should just kick back the original handle if called multiple times. Check the return value and see if the same handle is returned.

Dcreeron commented 4 years ago

The issue is caused by the call to DTWAIN_StartTwainSession().

I tried three tests:

So the extra DTWAIN_SysInitialize() has nothing to do with it.

FWIW, I'm not doing anything before the DTWAIN_StartTwainSession(). It would be equivalent to:

DTWAIN_SetTwainMode(DTWAIN_MODELESS);
DTWAIN_EnableMsgNotify(TRUE);
DTWAIN_SetCallback(MyCallback, 0);
DTWAIN_StartTwainSession(hWndToMainWindow, 0);
DTWAIN_BOOL twainSessionStarted = DTWAIN_StartTwainSession((HWND)this->winId(), NULL);
if (Source)
    DTWAIN_AcquireNative(Source, DTWAIN_PT_BW, DTWAIN_MAXACQUIRE, TRUE, TRUE, NULL);
dynarithmic commented 4 years ago

ok. It is probably an issue with subclassing the user-window, which I will have to investigate further. In the meantime I suggest to use the DTWAIN_StartTwainSession() with a null window handle (or just remove the call), since you are using DTWAIN_SetCallBack anyway to get the notifications.

Dcreeron commented 4 years ago

I'm going to experiment a little more with it tonight (sadly, day job wants me to work during the day. lol). If I figure out anything I'll update this thread.

dynarithmic commented 4 years ago

ok. I probably will change the subclassing method in the next version if there is a problem (just to let you know it may be a more extensive change than just a couple of lines).

Dcreeron commented 4 years ago

The problem with calling DTWAIN_StartTWAINSession() has something to do with this section of code:

// do any prep work before we loop
pImpl->PrepareLoop();
switch (opts.getAcquireType())
{
    case ACQUIRENATIVE:
    case ACQUIRENATIVEEX:
        if (DTWAIN_LLAcquireNative(opts) == -1L)
        {
            opts.setStatus(DTWAIN_TN_ACQUIREFAILED);
            LOG_FUNC_EXIT_PARAMS((DTWAIN_ARRAY)NULL)
        }
        if (opts.getAcquireType() == ACQUIRENATIVEEX)
            pSource->SetUserAcquisitionArray(opts.getUserArray());
        break;

The line "if (opts.getAcquireType() == ACQUIRENATIVEEX)" returns false, using DTWAIN_AcquireNative() so it doesn't set the acq array. If I jump over the check and make the Set call the rest of the code works.

The same code appears in the AcquireBuffered section further down in the case, which is probably why that code didn't work for me originally.

Think it's safe to just remove the getAcquireType() check in those two cases? Or maybe add a check to see if the mode is MODELESS and then skip the check?

If I read the documentation right, in MODELESS mode you need to wait for the TN_ACQUIREDONE for both methods, so the UserAcqArray would have to be set here.

Let me know your thoughts and I can try it later tonight after my meeting.

dynarithmic commented 4 years ago

The assumption of the library is that the array of DIBS is generated by DTWAIN for ACQUIREBUFFER and ACQUIRENATIVE modes, while for the EX modes, the user had to provide the array.

Any changes to this part of the code would have to be compatible with the simplest of situations, where a program simply calls DTWAIN_AcquireNative or DTWAIN_AcquireBuffered, with no notification callbacks, no call to DTWIAN_StartTwainSession, etc. In addition, no memory leaks are introduced, no double-freeing of memory allocated, etc.

Dcreeron commented 3 years ago

After a long hiatus/laziness I spent some time last night trying to reproduce my issue in a much smaller sample. I ended up creating a Qt Creator Console app and moved over my DTWAIN scanning code.

TLDR; I think I may be confused about the usage of DTWAIN_AcquireBuffered() and how/when to get the final image(s). Please see the questions at the end.

Previously I was having problems trying to get the images after acquisition using DTWAIN_AcquireBuffered(). The calls to the following weren't returning expected results:

 DTWAIN_GetSourceAcquisitions)
 DTWAIN_ArrayGetCount()
 DTWAIN_GetNumAcquiredImages()

When I ran my new sample, these calls all worked. Hmmm... what's different?

After reviewing my main app and comparing with the sample code I realized I hadn't copied over the calls to set the strip size:

DTWAIN_GetAcquireStripSizes(_selectedSource, &minSize, &maxSize, &prefSize);

hTheDibStrip = GlobalAlloc(GHND, minSize * 5); //DJC prefSize);

DTWAIN_SetAcquireStripBuffer(_selectedSource, hTheDibStrip);

When I added this code to the new sample and ran again it failed like it did previously.

I then went back to the DTWAIN documentation to see if I missed something regarding buffered transfers, strips, and getting the image(s).

To refresh, in my application I:

Question/Clarification Needed:

I was assuming that DTWAIN was just giving me access to the strip data, but still maintaining the images internally. Re-reading the documentation it seems like if I turn on strips that it's completely up to me to handle the image data. So I shouldn't be using the DTWAIN_GetSourceAcquisitions() and other methods in this scenario. Is that correct?

Assuming I'm on the right path now, when I get the notification events I do the following:

 DTWAIN_TN_TRANSFERDONE => do any processing/save image/etc. for a single acquired image
 DTWAIN_TN_ACQUIREDONE => do any processing/etc. for all acquired images (post process to multi-page TIFF, convert format/etc.)

Does it sound like I'm understanding it correctly now?

Thanks.

dynarithmic commented 3 years ago

Yes, as soon as you turn on the processing of the strip data yourself, everything is up to your application to process the strips correctly. In this mode, DTWAIN basically acts as a pass through (you get the notifications however).

The notifications you may want to look at are DTWAIN_TN_TRANSFERSTRIPREADY and DTWAIN_TN_TRANSFERSTRIPDONE. The former is sent before the strip is transferred and the latter after the strip is transferred.

In addition, you can use DTWAIN_GetAcquireStripData() on the DTWAIN_TN_TRANSFERSTRIPDONE notification to get information on the acquired strip.

Dcreeron commented 3 years ago

Thanks for the confirmation Paul.

I'm already using the other transfer strip calls; I can successfully display the image as it's transferred.

I guess I just didn't completely understand from my first reading of the documentation that I was responsible for everything once I turned on strip processing. Once that clicked last night as I was reading through the docs again it started to make sense.

I'm going to spend more time on it tonight and see if I can get my code to save out the images tonight (I did a quick test during a meeting and it looks like it will work now).

Thanks for your assistance and sorry for going down the wrong path previously. I'll close this issue now.