Print Driver Resolution and Debugging

Hello,

I am .NET Developer but got assigned this C++ Code writing/enhancing/troubleshooting a legacy print driver. I have the legacy source code written like 10 to 15 years back. The driver still works but users are complaining that its fuzzy and it is!
How do I troubleshoot this?
I was able to port all the code to VS 2017 complied a new driver, signed it and installed it. The driver sends PCL to the print spooler. Then the applications converts this PCL to bitmap

So does the fuzziness happen because of bad PCL or then further client processing where PCL to bitmap happens?

THis is how the pcl.cpp looks. Can anyone show me some red flags.. to help with fuzzy print?
How do I write print statements here? What tools do I need. Where does OMNIDEbug write to?
In .net we say File.Write(....). How can I see the PCl being emitted by the driver?


BOOL APIENTRY
CreatePCLRasterGraphicPage(
SURFOBJ *pso,
BOOL firstPage,
char *pageText
)
/*++

Routine Description:

Creates standard PCL end-of-document lines.

Arguments:

SURFOBJ - Surface Object
BOOL - First Page ?
char * Page Text

Return Value:

BOOL - True if successful

--*/
{
PDEVOBJ pDevObj = (PDEVOBJ)pso->dhpdev;
POEMPDEV pOemPDEV = (POEMPDEV)pDevObj->pdevOEM;

DWORD dwOffset = 0;
DWORD dwWritten = 0;
DWORD dwPageBufferSize = 0;
int i = 0;
ULONG n = 0;

BYTE bitmapRow[1050];
BYTE compRow[2100];
DWORD dwRowSize = 0;
DWORD dwCompPCLBitmapSize = 0;
//wchar_t traceBuff[256];


pOemPDEV->dwCompBitmapBufSize = 0;

// TRACE OUT ----------------------------------------------------
//ZeroMemory(traceBuff, 256);
//StringCchPrintf(traceBuff, 256, L"Top of CreatePCLRasterGraphicPage");
//WriteTraceLine(traceBuff);
// -----------------------------------------------------------------

// Invert color
for (n = 0; n < pso->cjBits; n++)
*(((PBYTE &)pso->pvBits) + n) ^= 0xFF;

// compress each row and store in a buffer with PCL line headings
for (i = 0; i < pso->sizlBitmap.cy; i++) {
// Zero Memory hack for bottom of form black line
if (*(((PBYTE &)pso->pvScan0) + (i * pso->lDelta) + 319) == 0xFF)
ZeroMemory(((PBYTE &)pso->pvScan0) + (i * pso->lDelta), 320);

// Copy the bitmap scan line into bitmapRow and send them off to be compressed
ZeroMemory(bitmapRow, 1050);
ZeroMemory(compRow, 2100);
MoveMemory(bitmapRow, ((PBYTE &)pso->pvScan0) + (i * pso->lDelta), pso->lDelta);
dwRowSize = CompressBitmapRow(compRow, bitmapRow, pso->lDelta);

// Create PCL Row Heading
char bufPCLLineHead[9];
StringCchPrintfA(bufPCLLineHead, 9, "%c%s%d%s", 27, "*b", dwRowSize, "W");

if ((dwCompPCLBitmapSize + dwRowSize + strlen(bufPCLLineHead))
> pOemPDEV->dwCompBitmapBufSize) {
if (!GrowCompBitmapBuf(pOemPDEV)) {
//ZeroMemory(traceBuff, 256);
//StringCchPrintf(traceBuff, 256,
// L"Compressed bitmap buffer could not allocate more memory.");
//WriteTraceLine(traceBuff);
}
}

if (pOemPDEV->pCompBitmapBufStart) {
// write the PCL line heading to the buffer
MoveMemory(pOemPDEV->pCompBitmapBufStart + dwCompPCLBitmapSize,
bufPCLLineHead, strlen(bufPCLLineHead));
dwCompPCLBitmapSize += strlen(bufPCLLineHead);

// write the compressed row to the buffer
MoveMemory(pOemPDEV->pCompBitmapBufStart + dwCompPCLBitmapSize,
compRow, dwRowSize);
dwCompPCLBitmapSize += dwRowSize;
}
}

// Calculate size and create buffer
dwPageBufferSize = 21;

if (!firstPage)
dwPageBufferSize++;

bGrowBuffer(pOemPDEV, dwPageBufferSize);

// Add all Raster Header Lines
if (!firstPage)
{
// Add a Form Feed
char bufFormFeed[2];
StringCchPrintfA(bufFormFeed, 2, "%c", 12); // 1 char
MoveMemory(pOemPDEV->pBufStart + dwOffset, bufFormFeed, 2);
dwOffset += 1;
}

// Position cursor at X0, Y0
char bufXY[8];
StringCchPrintfA(bufXY, 8, "%c%s", 27, "*p0x0Y"); // 7 chars
MoveMemory(pOemPDEV->pBufStart + dwOffset, bufXY, 8);
dwOffset += 7;

// Start Raster Graphics
char bufStartRas[6];
StringCchPrintfA(bufStartRas, 6, "%c%s", 27, "*r1A"); // 5 chars
MoveMemory(pOemPDEV->pBufStart + dwOffset, bufStartRas, 6);
dwOffset += 5;

// Raster Encoding - Run-Length Encoding
char bufRasEncoding[6];
StringCchPrintfA(bufRasEncoding, 6, "%c%s", 27, "*b1M"); // 5 chars
MoveMemory(pOemPDEV->pBufStart + dwOffset, bufRasEncoding, 6);
dwOffset += 5;

// Write out bitmap header PCL
dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pOemPDEV->pBufStart, dwPageBufferSize);

// Write out PCL plus compressed bitmap bytes
dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pOemPDEV->pCompBitmapBufStart, dwCompPCLBitmapSize);

// End Raster Graphics
char bufEndRas[5];
StringCchPrintfA(bufEndRas, 5, "%c%s", 27, "*rB"); // 4 chars
MoveMemory(pOemPDEV->pBufStart + dwOffset, bufEndRas, 5);

// Write out PCL end bitmap
dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, bufEndRas, 4);

// Free Compressed Bitmap Memory
if (pOemPDEV->pCompBitmapBufStart) {
MemFree(pOemPDEV->pCompBitmapBufStart);
pOemPDEV->pCompBitmapBufStart = NULL;
pOemPDEV->dwCompBitmapBufSize = 0;
dwPageBufferSize = 0;
}

// Free Memory
vFreeBuffer(pOemPDEV);

// Write Page Text to the spooler
size_t charCount = 0;
StringCchLengthA(pageText, 32767, &charCount);

char bufWriteText[15];
ZeroMemory(bufWriteText, 15);
StringCchPrintfA(bufWriteText, 15, "%c%s%d%s", 27, "(r", charCount, "W");
dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, bufWriteText, strlen(bufWriteText));

dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pageText, charCount);

return TRUE;
}
Last edited on
Please edit so we can read the code - https://www.cplusplus.com/articles/jEywvCM9/
salem: I tried but its not working. That code format is code cumbersome. Surely the code is readable to me

Thanks
What's so hard about typing [code] at the start of the code, and [/code] at the end of the code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
BOOL APIENTRY CreatePCLRasterGraphicPage(SURFOBJ * pso, BOOL firstPage, char *pageText)
/*++

Routine Description:

Creates standard PCL end-of-document lines.

Arguments:

SURFOBJ - Surface Object
BOOL - First Page ?
char * Page Text

Return Value:

BOOL - True if successful

--*/
{
  PDEVOBJ pDevObj = (PDEVOBJ) pso->dhpdev;
  POEMPDEV pOemPDEV = (POEMPDEV) pDevObj->pdevOEM;

  DWORD dwOffset = 0;
  DWORD dwWritten = 0;
  DWORD dwPageBufferSize = 0;
  int i = 0;
  ULONG n = 0;

  BYTE bitmapRow[1050];
  BYTE compRow[2100];
  DWORD dwRowSize = 0;
  DWORD dwCompPCLBitmapSize = 0;
  //wchar_t traceBuff[256];


  pOemPDEV->dwCompBitmapBufSize = 0;

  // TRACE OUT ----------------------------------------------------
  //ZeroMemory(traceBuff, 256);
  //StringCchPrintf(traceBuff, 256, L"Top of CreatePCLRasterGraphicPage");
  //WriteTraceLine(traceBuff);
  // -----------------------------------------------------------------

  // Invert color
  for (n = 0; n < pso->cjBits; n++)
    *(((PBYTE &) pso->pvBits) + n) ^= 0xFF;

  // compress each row and store in a buffer with PCL line headings
  for (i = 0; i < pso->sizlBitmap.cy; i++) {
    // Zero Memory hack for bottom of form black line
    if (*(((PBYTE &) pso->pvScan0) + (i * pso->lDelta) + 319) == 0xFF)
      ZeroMemory(((PBYTE &) pso->pvScan0) + (i * pso->lDelta), 320);

    // Copy the bitmap scan line into bitmapRow and send them off to be compressed
    ZeroMemory(bitmapRow, 1050);
    ZeroMemory(compRow, 2100);
    MoveMemory(bitmapRow, ((PBYTE &) pso->pvScan0) + (i * pso->lDelta), pso->lDelta);
    dwRowSize = CompressBitmapRow(compRow, bitmapRow, pso->lDelta);

    // Create PCL Row Heading
    char bufPCLLineHead[9];
    StringCchPrintfA(bufPCLLineHead, 9, "%c%s%d%s", 27, "*b", dwRowSize, "W");

    if ((dwCompPCLBitmapSize + dwRowSize + strlen(bufPCLLineHead))
        > pOemPDEV->dwCompBitmapBufSize) {
      if (!GrowCompBitmapBuf(pOemPDEV)) {
        //ZeroMemory(traceBuff, 256);
        //StringCchPrintf(traceBuff, 256,
        // L"Compressed bitmap buffer could not allocate more memory.");
        //WriteTraceLine(traceBuff);
      }
    }

    if (pOemPDEV->pCompBitmapBufStart) {
      // write the PCL line heading to the buffer
      MoveMemory(pOemPDEV->pCompBitmapBufStart + dwCompPCLBitmapSize,
                 bufPCLLineHead, strlen(bufPCLLineHead));
      dwCompPCLBitmapSize += strlen(bufPCLLineHead);

      // write the compressed row to the buffer
      MoveMemory(pOemPDEV->pCompBitmapBufStart + dwCompPCLBitmapSize, compRow, dwRowSize);
      dwCompPCLBitmapSize += dwRowSize;
    }
  }

  // Calculate size and create buffer
  dwPageBufferSize = 21;

  if (!firstPage)
    dwPageBufferSize++;

  bGrowBuffer(pOemPDEV, dwPageBufferSize);

  // Add all Raster Header Lines
  if (!firstPage) {
    // Add a Form Feed
    char bufFormFeed[2];
    StringCchPrintfA(bufFormFeed, 2, "%c", 12); // 1 char
    MoveMemory(pOemPDEV->pBufStart + dwOffset, bufFormFeed, 2);
    dwOffset += 1;
  }

  // Position cursor at X0, Y0
  char bufXY[8];
  StringCchPrintfA(bufXY, 8, "%c%s", 27, "*p0x0Y"); // 7 chars
  MoveMemory(pOemPDEV->pBufStart + dwOffset, bufXY, 8);
  dwOffset += 7;

  // Start Raster Graphics
  char bufStartRas[6];
  StringCchPrintfA(bufStartRas, 6, "%c%s", 27, "*r1A"); // 5 chars
  MoveMemory(pOemPDEV->pBufStart + dwOffset, bufStartRas, 6);
  dwOffset += 5;

  // Raster Encoding - Run-Length Encoding
  char bufRasEncoding[6];
  StringCchPrintfA(bufRasEncoding, 6, "%c%s", 27, "*b1M");  // 5 chars
  MoveMemory(pOemPDEV->pBufStart + dwOffset, bufRasEncoding, 6);
  dwOffset += 5;

  // Write out bitmap header PCL
  dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pOemPDEV->pBufStart, dwPageBufferSize);

  // Write out PCL plus compressed bitmap bytes
  dwWritten =
      pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pOemPDEV->pCompBitmapBufStart,
                                           dwCompPCLBitmapSize);

  // End Raster Graphics
  char bufEndRas[5];
  StringCchPrintfA(bufEndRas, 5, "%c%s", 27, "*rB");  // 4 chars
  MoveMemory(pOemPDEV->pBufStart + dwOffset, bufEndRas, 5);

  // Write out PCL end bitmap
  dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, bufEndRas, 4);

  // Free Compressed Bitmap Memory
  if (pOemPDEV->pCompBitmapBufStart) {
    MemFree(pOemPDEV->pCompBitmapBufStart);
    pOemPDEV->pCompBitmapBufStart = NULL;
    pOemPDEV->dwCompBitmapBufSize = 0;
    dwPageBufferSize = 0;
  }

  // Free Memory
  vFreeBuffer(pOemPDEV);

  // Write Page Text to the spooler
  size_t charCount = 0;
  StringCchLengthA(pageText, 32767, &charCount);

  char bufWriteText[15];
  ZeroMemory(bufWriteText, 15);
  StringCchPrintfA(bufWriteText, 15, "%c%s%d%s", 27, "(r", charCount, "W");
  dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, bufWriteText, strlen(bufWriteText));

  dwWritten = pDevObj->pDrvProcs->DrvWriteSpoolBuf(pDevObj, pageText, charCount);

  return TRUE;
}


The first thing to check would be all those magic numbers like 1050, 2100, 319, 320.

The invert colour is a hack that only works if the input is a BMP.

How does CompressBitmapRow know not to overflow the given buffer?

> How do I write print statements here?
Look at the commented out "TRACE OUT" blocks.


Thanks. I was just clicking on the format code button.
Salem, This is a dumb question and apologies
a) This was written like 13 years back. We are a .NET shop and nothing with C++. A previous developer wrote it in c++. The input to the driver as of now is any file (word, pdf, tiff..anything). If there was a way to upload a screenshot I can show you the fuzziness. Since now after 13 years I am building this code, driver,please help me troubleshooot it. I tried to attach notepad.exe and print from notepad. But only DLLMain gets executed and the process quits. It doesn't get this PCL part. DLLMain has nothing in it except the following. How do I get it to invoke the PCL Logic. Its the same workflow.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
BOOL WINAPI DllMain(HINSTANCE hInst, WORD wReason, LPVOID lpReserved)
{
	
    OEMDBG(DBG_VERBOSE, L"DllMain entry.");

    UNREFERENCED_PARAMETER(hInst);
    UNREFERENCED_PARAMETER(lpReserved);

    switch(wReason)
    {
        case DLL_PROCESS_ATTACH:
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_PROCESS_DETACH:
            break;

        case DLL_THREAD_DETACH:
            break;
    }

    return TRUE;
}

Can I put an actual breakpoint and run this code? '
b) The driver emits PCL which then gets processed by the clients to TIFF. This TIFF is rendered on the screen to the user. The second part of the process happens in .NET and I can understand .NET. The client takes the PCL as Binary reader and has .NET code for converting the PCL to TIFF. TIFF is what gets displayed. I need to make sure there is nothing wrong with the driver. It has been built during XP age. The users have been complaining that the print is fuzzy.
c) Do you think it could be Run length encoding. Is there a way I can post a screenshot?
d) I couldn't tell you how CompressBitmapRow works. 1050 is rowsize and not sure why 1050. But I can show the source code.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
DWORD 
CompressBitmapRow(
				  PBYTE compRow, 
				  PBYTE bitmapRow, 
				  SIZE_T bitmapRowSize
				  ) 
/*++

Routine Description:

	Compresses a line of bitmap data using Run-length encoding

Arguments:

	PBYTE - Byte array for the compressed row bytes
	PBYTE - Byte array with the bitmap row bytes
	SIZE_T - Size of the bitmap row

Return Value:

	DWORD - Size of the compressed row

--*/
{
	//UNREFERENCED_PARAMETER(compRow);
	//UNREFERENCED_PARAMETER(bitmapRow);
	//UNREFERENCED_PARAMETER(bitmapRowSize);


	DWORD rowSize = 0;
	BYTE currentByte = NULL;
	BYTE firstByte = NULL;
	short repCount = -1;
	unsigned int i;
	// Loop through bitmap row bytes and create RL compressed bytes from them
	for (i = 0; i < bitmapRowSize; i++) {
		currentByte = bitmapRow[i];
		if (repCount == 255) {
			compRow[rowSize] = (BYTE)repCount;
			compRow[rowSize + 1] = firstByte;
			firstByte = currentByte;
			repCount = 0;
			rowSize += 2;
		}
		else if (repCount == -1) {
			firstByte = currentByte;
			repCount++;
		}
		else if (currentByte == firstByte)
			repCount++;
		else {	// Not equal and not first so write two bytes
			compRow[rowSize] = (BYTE)repCount;
			compRow[rowSize + 1] = firstByte;
			firstByte = currentByte;
			repCount = 0;
			rowSize += 2;
		}
	}
	compRow[rowSize] = (BYTE)repCount;
	compRow[rowSize + 1] = firstByte;
	rowSize += 2;

	return rowSize;
}

BOOL
GrowCompBitmapBuf(
				  POEMPDEV pOemPDEV
				  )
/*++

Routine Description:

	Grows memory by 1000 bytes (per call) to hold compressed
	bitmap and PCL data.

Arguments:

	POEMPDEV - Pointer to the private PDEV structure

Return Value:

	BOOL - True is successful

--*/
{
	DWORD dwOldBufferSize = 0;
	PBYTE pNewBuffer = NULL;

	dwOldBufferSize = pOemPDEV->pCompBitmapBufStart ? pOemPDEV->dwCompBitmapBufSize : 0;
	pOemPDEV->dwCompBitmapBufSize = dwOldBufferSize + 4096;

	pNewBuffer = (PBYTE)MemAlloc(pOemPDEV->dwCompBitmapBufSize);
	if (pNewBuffer == NULL) {
		MemFree(pOemPDEV->pCompBitmapBufStart);
		pOemPDEV->pCompBitmapBufStart = NULL;
		pOemPDEV->dwCompBitmapBufSize = 0;
		return FALSE;
	}

	if (pOemPDEV->pCompBitmapBufStart) {
		CopyMemory(pNewBuffer, pOemPDEV->pCompBitmapBufStart, dwOldBufferSize);
		MemFree(pOemPDEV->pCompBitmapBufStart);
		pOemPDEV->pCompBitmapBufStart = pNewBuffer;
	}
	else {
		pOemPDEV->pCompBitmapBufStart = pNewBuffer;
	}

	return TRUE;
}


Last edited on
heh, 13 years ago is about when I last did any serious windows debugging.
Yes, the debugger that comes with visual studio will have no problems.

There's nothing stopping you from opening a file to append your debug info to.
One idea might be to construct an actual BMP file from the input data, to see what it is the higher level print driver is generating in the first instance.

Yes you can attach urls to posts on one of the many free image hosting sites.


> BYTE bitmapRow[1050];
Some maths for you.
1050 / 3 bytes per pixel gets you 570 pixels.
Even at a meagre 150 dots per inch, you're at least doubling up every pixel to get across an A4 page.
I couldn't understand.
.
"One idea might be to construct an actual BMP file from the input data, to see what it is the higher level print driver is generating in the first instance."
I tried to attach a debugger and it takes me to DLL Main which does nothing. How do I get the PCL code to execute from debugger is what I am confused about. Why does DLL main do nothing just returns true

Are you saying this is OK?
BYTE bitmapRow[1050];
1050 / 3 bytes per pixel gets you 570 pixels.
Even at a meagre 150 dots per inch, you're at least doubling up every pixel to get across an A4 page.

Also sent you a PM.
Ill be the elephant in the room guy: have you tried just using the generic drivers that work with most antiques? They may not work, but it could be that Microsoft already solved this for you.

@jonnin: Thank you for the input. The driver writes PCL which gets picked up a .NET Client and gets converted to TIFF. Does that help why I may not be able to use a generic driver. There is another component (not the client, not the driver) which always listens on a port. It diverts traffic to users.
Again I don't have a lot of experience and none in C++, sorry if I didn't understand your input. Feel free to correct me..


Last edited on
> Can I put an actual breakpoint and run this code? '
If you're referring to the DllMain, sure - I guess....
The Windows debugger has the ability to break on loading a DLL.
AFAIK, you should be able to just attach to an existing process (say notepad), then set a breakpoint for the loading of your DLL, which should be triggered when you do print.

Like I said, a decade of fuzzy memory here.

> DLLMain has nothing in it except the following. How do I get it to invoke the PCL Logic. Its the same workflow.
Well the generic driver is going to be looking for other functions within the DLL with a defined name.


> b) The driver emits PCL which then gets processed by the clients to TIFF. This TIFF is rendered on the screen to the user. The second part of the process happens in .NET and I can understand .NET. The client takes the PCL as Binary reader and has .NET code for converting the PCL to TIFF. TIFF is what gets displayed.
So is this like just a print preview thing?

> I need to make sure there is nothing wrong with the driver. It has been built during XP age. The users have been complaining that the print is fuzzy.

c) Do you think it could be Run length encoding. Is there a way I can post a screenshot?

> d) I couldn't tell you how CompressBitmapRow works. 1050 is rowsize and not sure why 1050. But I can show the source code.
> BYTE bitmapRow[1050];
> BYTE compRow[2100];
The compression bit by itself is fine. If many adjacent bytes are the same, then it compresses well. If every single byte is different, then you end up with a buffer twice as big as when you started.
No, the issue is the fact that 1050 is a small number when if comes to dealing with a single row of supposedly high resolution pixels.

TBH, I would be also looking for where this printer driver reports back to the system it's capabilities. Those hard-coded small buffers would have overflowed long ago if the overall system was throwing any sized bitmap at it. It must be reporting things like the max resolution (which will be crap).

Also TBH, you need to talk to your management about these issues.
Perhaps find a competent Windows C++ programmer (maybe on a short term contract) to fix this up locally.

One more thing, does posting ever larger chunks of your bespoke printer driver violate any of the NDA policies of the company you're working for?

> Also sent you a PM.
Yeah - I don't have that kind of time.
People's ideas of a "15 minute" chat usually result in 3+ hours.
Ok, you can ignore the idea then. I just wanted to see if you were very sure you needed to fix the old stuff, and it sounds like yes, you do.
@salem: We are 15 people company. If you have a c++ developer please let me know. Management is one person who doesn't understand a thing :). I am out of luck here. I don't have enough experience as a developer to actually communicate this with that one person.

Thanks for your help


I'm a one person company with only so much free time.

I'm a freelancer, and I make money out of companies' inabilities to solve their own problems.

If you're able, you could put the whole C++ source code for the driver in question on say https://mega.nz/ (or dropbox at a pinch, but certainly not google/msoft), then PM me a URL.

I should be able to look at it within a few days, perhaps with a view to a f2f - depends if I find anything useful to suggest or say on it.


Topic archived. No new replies allowed.