From b6711389cf8398b5ad076ef31575faea44a6d9d9 Mon Sep 17 00:00:00 2001
From: Eugene Wang <8755753+soukoku@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:56:31 -0500
Subject: [PATCH] Initial AI-generated memoryimage xfer logic.
---
src/NTwain/TransferLoopThread.Logic.cs | 267 ++++++++++++++++++++++---
1 file changed, 238 insertions(+), 29 deletions(-)
diff --git a/src/NTwain/TransferLoopThread.Logic.cs b/src/NTwain/TransferLoopThread.Logic.cs
index 57d0a88..8b3dcbf 100644
--- a/src/NTwain/TransferLoopThread.Logic.cs
+++ b/src/NTwain/TransferLoopThread.Logic.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
+using Windows.Win32.Graphics.Gdi;
namespace NTwain;
@@ -391,16 +392,32 @@ partial class TransferLoopThread
private STS TransferMemoryImage(ref TW_PENDINGXFERS pending)
{
- // TODO: build the image from strips before firing off transferred event
-
-
var rc = DGControl.SetupMemXfer.Get(_twain.AppIdentity, _twain.CurrentSource!, out TW_SETUPMEMXFER memSetup);
if (rc != TWRC.SUCCESS) return _twain.WrapInSTS(rc);
rc = DGImage.ImageInfo.Get(_twain.AppIdentity, _twain.CurrentSource!, out TW_IMAGEINFO info);
if (rc != TWRC.SUCCESS) return _twain.WrapInSTS(rc);
- rc = DGImage.ImageLayout.Get(_twain.AppIdentity, _twain.CurrentSource!, out TW_IMAGELAYOUT layout);
- if (rc != TWRC.SUCCESS) return _twain.WrapInSTS(rc);
+ // Check if the image is compressed (TIFF, JPEG, etc.) or uncompressed (raw pixels)
+ // When compressed, the data forms a complete file format (like TIFF) and strips are just concatenated.
+ // When uncompressed, we need to build a BMP from raw pixel strips.
+ bool isCompressed = info.Compression != TWCP.NONE;
+
+ if (isCompressed)
+ {
+ return TransferMemoryImageCompressed(ref pending, ref info, ref memSetup);
+ }
+ else
+ {
+ return TransferMemoryImageUncompressed(ref pending, ref info, ref memSetup);
+ }
+ }
+
+ ///
+ /// Handles memory transfer for compressed images (TIFF, JPEG, etc.).
+ /// The strips form a complete file format and are simply concatenated.
+ ///
+ private STS TransferMemoryImageCompressed(ref TW_PENDINGXFERS pending, ref TW_IMAGEINFO info, ref TW_SETUPMEMXFER memSetup)
+ {
uint buffSize = memSetup.DetermineBufferSize();
var memPtr = _twain.MemoryManager.Alloc(buffSize);
@@ -414,7 +431,12 @@ partial class TransferLoopThread
};
memXferOSX.Memory = memXfer.Memory;
- byte[] dotnetBuff = BufferedData.MemPool.Rent((int)buffSize);
+ // For compressed transfers, we don't know the final size upfront,
+ // so use a MemoryStream to accumulate the data
+ byte[] stripBuff = BufferedData.MemPool.Rent((int)buffSize);
+ using var outStream = new MemoryStream();
+ TWRC rc;
+
try
{
do
@@ -427,15 +449,11 @@ partial class TransferLoopThread
{
try
{
- var written = TWPlatform.IsMacOSX ?
- memXferOSX.BytesWritten : memXfer.BytesWritten;
+ var written = TWPlatform.IsMacOSX ? memXferOSX.BytesWritten : memXfer.BytesWritten;
IntPtr lockedPtr = _twain.MemoryManager.Lock(memPtr);
-
- // assemble!
-
- //Marshal.Copy(lockedPtr, dotnetBuff, 0, (int)written);
- //outStream.Write(dotnetBuff, 0, (int)written);
+ Marshal.Copy(lockedPtr, stripBuff, 0, (int)written);
+ outStream.Write(stripBuff, 0, (int)written);
}
finally
{
@@ -443,32 +461,223 @@ partial class TransferLoopThread
}
}
} while (rc == TWRC.SUCCESS);
+
+ if (rc == TWRC.XFERDONE)
+ {
+ _twain.State = STATE.S7;
+
+ try
+ {
+ // The accumulated data is the complete compressed image (TIFF, JPEG, etc.)
+ var finalData = outStream.ToArray();
+ var data = new BufferedData(finalData, (int)outStream.Length, false);
+
+ var args = new TransferredEventArgs(_twain, info, null, data);
+ _twain.RaiseTransferred(args);
+ }
+ catch { }
+
+ pending = TW_PENDINGXFERS.DONTCARE();
+ var sts = _twain.WrapInSTS(DGControl.PendingXfers.EndXfer(_twain.AppIdentity, _twain.CurrentSource!, ref pending));
+ if (sts.RC == TWRC.SUCCESS)
+ {
+ _twain.State = pending.Count == 0 ? STATE.S5 : STATE.S6;
+ }
+ return sts;
+ }
+
+ return _twain.WrapInSTS(rc);
}
finally
{
if (memPtr != IntPtr.Zero) _twain.MemoryManager.Free(memPtr);
- BufferedData.MemPool.Return(dotnetBuff);
+ BufferedData.MemPool.Return(stripBuff);
}
+ }
+ ///
+ /// Handles memory transfer for uncompressed images (raw pixels).
+ /// Builds a BMP file from the raw pixel strips.
+ ///
+ private STS TransferMemoryImageUncompressed(ref TW_PENDINGXFERS pending, ref TW_IMAGEINFO info, ref TW_SETUPMEMXFER memSetup)
+ {
+ uint buffSize = memSetup.DetermineBufferSize();
+ var memPtr = _twain.MemoryManager.Alloc(buffSize);
- if (rc == TWRC.XFERDONE)
+ TW_IMAGEMEMXFER memXfer = TW_IMAGEMEMXFER.DONTCARE();
+ TW_IMAGEMEMXFER_MACOSX memXferOSX = TW_IMAGEMEMXFER_MACOSX.DONTCARE();
+ memXfer.Memory = new TW_MEMORY
{
- try
- {
- DGImage.ImageInfo.Get(_twain.AppIdentity, _twain.CurrentSource!, out info);
- //var args = new DataTransferredEventArgs(info, null, outStream.ToArray());
- //DataTransferred?.Invoke(this, args);
- }
- catch { }
+ Flags = (uint)(TWMF.APPOWNS | TWMF.POINTER),
+ Length = buffSize,
+ TheMem = memPtr
+ };
+ memXferOSX.Memory = memXfer.Memory;
- pending = TW_PENDINGXFERS.DONTCARE();
- var sts = _twain.WrapInSTS(DGControl.PendingXfers.EndXfer(_twain.AppIdentity, _twain.CurrentSource!, ref pending));
- if (sts.RC == TWRC.SUCCESS)
- {
- _twain.State = pending.Count == 0 ? STATE.S5 : STATE.S6;
- }
- return sts;
+ // Calculate image dimensions for BMP construction
+ // BMP rows must be aligned to 4-byte boundaries
+ int stride = ((info.ImageWidth * info.BitsPerPixel + 31) / 32) * 4;
+ int imageDataSize = stride * Math.Abs(info.ImageLength);
+
+ // Calculate color table size (for indexed images)
+ int colorTableSize = 0;
+ if (info.BitsPerPixel <= 8)
+ {
+ int colorCount = 1 << info.BitsPerPixel;
+ colorTableSize = colorCount * 4; // RGBQUAD is 4 bytes
}
+
+ int bitmapInfoHeaderSize = Marshal.SizeOf();
+ int bitmapFileHeaderSize = Marshal.SizeOf();
+ int totalSize = bitmapFileHeaderSize + bitmapInfoHeaderSize + colorTableSize + imageDataSize;
+
+ // Allocate output buffer from pool
+ byte[] outputBuff = BufferedData.MemPool.Rent(totalSize);
+ byte[] stripBuff = BufferedData.MemPool.Rent((int)buffSize);
+ int pixelDataOffset = bitmapFileHeaderSize + bitmapInfoHeaderSize + colorTableSize;
+ TWRC rc;
+
+ try
+ {
+ // Build BMP headers
+ var fileHeader = new BITMAPFILEHEADER
+ {
+ bfType = 0x4D42, // "BM"
+ bfSize = (uint)totalSize,
+ bfOffBits = (uint)pixelDataOffset
+ };
+
+ var infoHeader = new BITMAPINFOHEADER
+ {
+ biSize = (uint)bitmapInfoHeaderSize,
+ biWidth = info.ImageWidth,
+ biHeight = info.ImageLength, // positive = bottom-up DIB
+ biPlanes = 1,
+ biBitCount = (ushort)info.BitsPerPixel,
+ biCompression = 0, // BI_RGB (uncompressed)
+ biSizeImage = (uint)imageDataSize,
+ biXPelsPerMeter = (int)(info.XResolution * 39.3701), // DPI to pixels per meter
+ biYPelsPerMeter = (int)(info.YResolution * 39.3701),
+ biClrUsed = (uint)(info.BitsPerPixel <= 8 ? (1 << info.BitsPerPixel) : 0),
+ biClrImportant = 0
+ };
+
+ // Write file header
+ unsafe
+ {
+ fixed (byte* p = outputBuff)
+ {
+ Marshal.StructureToPtr(fileHeader, (IntPtr)p, false);
+ Marshal.StructureToPtr(infoHeader, (IntPtr)(p + bitmapFileHeaderSize), false);
+ }
+ }
+
+ do
+ {
+ rc = TWPlatform.IsMacOSX ?
+ DGImage.ImageMemXfer.Get(_twain.AppIdentity, _twain.CurrentSource!, ref memXferOSX) :
+ DGImage.ImageMemXfer.Get(_twain.AppIdentity, _twain.CurrentSource!, ref memXfer);
+
+ if (rc == TWRC.SUCCESS || rc == TWRC.XFERDONE)
+ {
+ try
+ {
+ var written = TWPlatform.IsMacOSX ? memXferOSX.BytesWritten : memXfer.BytesWritten;
+ var rows = TWPlatform.IsMacOSX ? memXferOSX.Rows : memXfer.Rows;
+ var bytesPerRow = TWPlatform.IsMacOSX ? memXferOSX.BytesPerRow : memXfer.BytesPerRow;
+ var yOffset = TWPlatform.IsMacOSX ? memXferOSX.YOffset : memXfer.YOffset;
+
+ IntPtr lockedPtr = _twain.MemoryManager.Lock(memPtr);
+
+ // Copy strip data to temp buffer
+ Marshal.Copy(lockedPtr, stripBuff, 0, (int)written);
+
+ // Copy each row to the correct position in output buffer
+ // BMP is stored bottom-up, so we need to flip the row order
+ for (int row = 0; row < rows; row++)
+ {
+ int srcOffset = (int)(row * bytesPerRow);
+ // Calculate destination row (flip for bottom-up BMP)
+ int destRow = info.ImageLength - 1 - ((int)yOffset + row);
+ int destOffset = pixelDataOffset + (destRow * stride);
+
+ // Copy the row data (up to the actual bytes per row from source, but pad to stride)
+ int bytesToCopy = Math.Min((int)bytesPerRow, stride);
+ Buffer.BlockCopy(stripBuff, srcOffset, outputBuff, destOffset, bytesToCopy);
+ }
+ }
+ finally
+ {
+ _twain.MemoryManager.Unlock(memPtr);
+ }
+ }
+ } while (rc == TWRC.SUCCESS);
+
+ if (rc == TWRC.XFERDONE)
+ {
+ _twain.State = STATE.S7;
+
+ // Handle color table for indexed images
+ if (colorTableSize > 0)
+ {
+ // For grayscale images, create a grayscale palette
+ if (info.PixelType == TWPT.GRAY || info.PixelType == TWPT.BW)
+ {
+ int colorCount = 1 << info.BitsPerPixel;
+ int paletteOffset = bitmapFileHeaderSize + bitmapInfoHeaderSize;
+ for (int i = 0; i < colorCount; i++)
+ {
+ byte grayValue = (byte)(i * 255 / (colorCount - 1));
+ outputBuff[paletteOffset + i * 4 + 0] = grayValue; // Blue
+ outputBuff[paletteOffset + i * 4 + 1] = grayValue; // Green
+ outputBuff[paletteOffset + i * 4 + 2] = grayValue; // Red
+ outputBuff[paletteOffset + i * 4 + 3] = 0; // Reserved
+ }
+ }
+ // For B&W images specifically, ensure proper black/white palette
+ if (info.BitsPerPixel == 1)
+ {
+ int paletteOffset = bitmapFileHeaderSize + bitmapInfoHeaderSize;
+ // Index 0 = Black (for TWAIN B&W where 0 is typically black)
+ outputBuff[paletteOffset + 0] = 0; // Blue
+ outputBuff[paletteOffset + 1] = 0; // Green
+ outputBuff[paletteOffset + 2] = 0; // Red
+ outputBuff[paletteOffset + 3] = 0; // Reserved
+ // Index 1 = White
+ outputBuff[paletteOffset + 4] = 255; // Blue
+ outputBuff[paletteOffset + 5] = 255; // Green
+ outputBuff[paletteOffset + 6] = 255; // Red
+ outputBuff[paletteOffset + 7] = 0; // Reserved
+ }
+ }
+
+ try
+ {
+ var data = new BufferedData(outputBuff, totalSize, true);
+ // Transfer ownership to BufferedData, so don't return to pool in finally
+ outputBuff = null!;
+
+ var args = new TransferredEventArgs(_twain, info, null, data);
+ _twain.RaiseTransferred(args);
+ }
+ catch { }
+
+ pending = TW_PENDINGXFERS.DONTCARE();
+ var sts = _twain.WrapInSTS(DGControl.PendingXfers.EndXfer(_twain.AppIdentity, _twain.CurrentSource!, ref pending));
+ if (sts.RC == TWRC.SUCCESS)
+ {
+ _twain.State = pending.Count == 0 ? STATE.S5 : STATE.S6;
+ }
+ return sts;
+ }
+ }
+ finally
+ {
+ if (memPtr != IntPtr.Zero) _twain.MemoryManager.Free(memPtr);
+ if (outputBuff != null) BufferedData.MemPool.Return(outputBuff);
+ BufferedData.MemPool.Return(stripBuff);
+ }
+
return _twain.WrapInSTS(rc);
}