Engineering Challenges Q&A - iseahound/ImagePut Wiki

Challenges encountered when developing ImagePut

Q: Why does the following regex ([a])+ fail for long strings?

A: I don't know. AutoHotkey hates the 1+ operation and will only permit it for short input strings. As a workaround, use the Kleene star (possessive) ([a]{2})*+. instead. The counter bracket is irrelevant, ([a][a])+ would fail also. This workaround is used in the detection of hexadecimal and base64 strings.

Q: What is the shortest possible image encoded into base64 and hex?

A: They are:

; 24 bytes. A black pixel. Unstable.
ImagePutWindow("R0lGODlhAQABAAAAACwAAAAAAQABAAAC")
ImagePutWindow("474946383961010001000000002c00000000010001000002")

; 33 bytes. Smallest possible 1x1 transparent pixel.
;ImagePutWindow({base64: "R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIA"})
;ImagePutWindow({hex: "4749463839610100010000000021f90401000000002c0000000001000100000200"})

For a nice 32 base64 and 48 hex string length.

Source: https://stackoverflow.com/questions/2570633/smallest-filesize-for-transparent-single-pixel-image

Q: What is the smallest image accepted by GDI+?

A: At least when using the CloneImage and CreateBitmapFromScan0 functions, the smallest valid bitmap is 1x1 or else it returns 0 as the pointer.

Q: Can the mysterious stock bitmap ever be leaked?

A: No. Although it cannot be retrieved via GetStockObject, it can be retrieved by the following code:

; Get a pointer to the mysterious stock bitmap.
; See: https://devblogs.microsoft.com/oldnewthing/20100416-00/?p=14313
MsgBox % obm := DllCall("CreateBitmap", "int", 0, "int", 0, "uint", 1, "uint", 1, "ptr", 0, "ptr")

Q: How to convert between an HBITMAP and a BITMAP with transparency?

A: The solution is to use LockBits and a custom pointer to the image memory.

Hey, I'm glad you all enjoy this code, but I don't recognize it at the moment. For people like me who are confused, the problem is that if a hBitmap has transparent pixels, they will be converted to black using the built-in function.

Problem: Transparent pixels are 0x00000000. Alpha = 0x00, Red = 0x00, Green = 0x00, Blue = 0x00. In conversion, I assume the alpha channel is dropped. So the transparent pixel becomes 0x000000 which is black. Solution: We know a Bitmap can hold 32 bits of color. So, we should be able to preserve transparency.

Words:

  1. We have a handle to our image, and we want to convert that handle to a pointer. The reason why we do this is because a pointer is global (can be transferred to more processes) while a handle is opaque, and probably has some limitations.
  2. Extract some information from our handle. Call GetObject and get width, height, and bpp. (bits per pixel)
  3. Now the most straightforward approach would be to find where the pixel values are in our hBitmap and memcpy the array of pixels to the pBitmap. This fails. Reasons #1 - The hBitmap could be bottom-up or top-down. #2 - hBitmap is pARGB (24-bit data) and ARGB is 32-bit data.
  4. We could just code everything in C, but we are going to avoid super low level stuff and be smart. We'll use some tricks. The first trick is to determine whether a bitmap is bottom-up or top-down. The standard approach is to change the value of the first pixel value in our data to a salmon-colored pink and call GetPixel. If we get a salmon-colored pink, then it's top down. If not it's bottom up. We won't do this. Instead, we'll create a new DIB (Device independent bitmap) which is the same thing as an hBitmap where the pixels are stored on local memory and copy the pixels using BitBlt. If our newly created DIB has is initialized with a negative height, then the pixels will be stored top-down. top-down is what a normal person should expect, like reading a book. Imagine reading a book from the last sentence on the page and making your way up. That's called bottom-up and it is how Windows normally stores pixel data. That's because some mathematicians were really concerned about purity and ideology. You know how a xy graph looks like? x-axis and y-axis? Yeah, the mathematicians wanted to use that model.
  5. For the second trick we'll use something special, a hidden flag in GdipBitmapLockBits that lets us determine where the return data will be buffered. You see, we get to choose where the output of the function will be, and I choose to put it where my DIB is. Importantly, I set another flag to display the output data as PARGB, the same format as a DIB.
  6. Do the following in order: Create a DIB. Create a Bitmap. Lock the Bitmap and point the output to the DIB. Use Trick #1 - copy the pixels to the DIB, erasing any top-down/bottom-up distinctions. Use Trick #2 - Unlock the bits, which copies the 24-bit pARGB data in a DIB into 32-bit ARGB pixels.
  7. Clean up and return the bitmap.

Q: What is the subtle distinction between a MemoryStream and a Stream?

A: MemoryStream are created by SHCreateMemStream and Streams from CreateStreamOnHGlobal. The subtle difference is that only regular streams can be locked to retrieve the hidden HGLOBAL supporting it. MemoryStreams have completely abstracted away the concept of physical memory backing the data. However, some smart people will attempt to call GetHGlobalFromStream on ImagePut stream outputs, so to maximize expectations, all stream outputs will be regular Streams. Likewise, all incoming streams are never locked to memory, permitting MemoryStream inputs.

Q: How does reference counting work with Streams and RandomAccessStreams?

A: A RandomAccessStream is always created over a Stream despite any attempt to imply otherwise. So, when creating a Stream from a RandomAccessStream, the internal stream is returned. As such, this internal stream always starts with a reference count of 2, 1 for the RandomAccessStream, and another for itself.

Note how the second stream has a reference count of +1 the first stream. They are the same stream!

p1 := ImagePutRandomAccessStream("https://picsum.photos/500")
MsgBox % "pRandomAccessStream References: " GetRefCount(p1) ; Displays 1.

p2 := get_RandomAccessStream(p1)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
MsgBox % "First pStream References: " GetRefCount(p2) ; Displays 7.

p3 := get_RandomAccessStream(p1)
MsgBox % "Second pStream References: " GetRefCount(p3) ; Displays 8.

GetRefCount(p) {
    ObjAddRef(p)
    return ObjRelease(p)
}

get_RandomAccessStream(image) {
    DllCall("ole32\CLSIDFromString", "wstr", "{0000000C-0000-0000-C000-000000000046}", "ptr", &CLSID := VarSetCapacity(CLSID, 16), "uint")
    DllCall("ShCore\CreateStreamOverRandomAccessStream", "ptr", image, "ptr", &CLSID, "ptr*", pStream:=0, "uint")
    return pStream
}

Yet creating a RandomAccessStream over a Stream returns a new RandomAccessStream each time! Each RandomAccessStream starts with a reference count of 1.

p1 := ImagePutStream("https://picsum.photos/500")
MsgBox % "pStream References: " GetRefCount(p1) ; Displays 1. 

p2 := set_RandomAccessStream(p1)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
ObjAddRef(p2)
MsgBox % "First pRandomAccessStream References: " GetRefCount(p2) ; Displays 6. 

p3 := set_RandomAccessStream(p1)
MsgBox % "Second pRandomAccessStream References: " GetRefCount(p3) ; Displays 1. 

GetRefCount(p) {
    ObjAddRef(p)
    return ObjRelease(p)
}

set_RandomAccessStream(pStream) {
    DllCall("ole32\CLSIDFromString", "wstr", "{905A0FE1-BC53-11DF-8C49-001E4FC686DA}", "ptr", &CLSID := VarSetCapacity(CLSID, 16), "uint")
    DllCall("ShCore\CreateRandomAccessStreamOverStream"
                ,    "ptr", pStream
                ,   "uint", BSOS_PREFERDESTINATIONSTREAM := 1
                ,    "ptr", &CLSID
                ,   "ptr*", pRandomAccessStream:=0
                ,   "uint")
    return pRandomAccessStream
}

Q: What is an image and what is a bitmap?

A: Within the context of GDI+, an image is either a metafile or a bitmap. Under the Liskov substitution principle, all image functions work on bitmaps.

For this project, an image is a base64, stream, clipboard, file, etc. And a bitmap refers to a GDI+ bitmap and a hbitmap to a GDI bitmap. Occasionally the BMP file format which is closely related to an hbitmap may pop up as well.

Q: How does creating a bitmap from a stream work?

A: Each created bitmap increments the reference count of the stream by 3. The resulting bitmap is a wrapper around the stream. Image data is only copied on LockBits with the read flag set, in addition to being drawn on through a graphics object. When copying image data from the stream to the bitmap's internal buffer, the stream is locked.

Q: How to clone a bitmap?

A: Call CloneImageArea on the bitmap to preserve the original image pixel format. Note that cloning creates a wrapper. See: https://stackoverflow.com/a/13935966

Q: Can the original bitmap be deleted before the clone?

A: Yes! This isn't C#, where there is a lock on the original bitmap.

ImagePut.gdiplusStartup()
DllCall("gdiplus\GdipCreateBitmapFromFile", "wstr", "cats.jpg", "ptr*", pBitmap:=0)
DllCall("gdiplus\GdipCloneImage", "ptr", pBitmap, "ptr*", pBitmapClone:=0)
DllCall("gdiplus\GdipDisposeImage", "ptr", pBitmap)
ImagePut.show(pBitmapClone)

Q: How to push image data into a bitmap?

A: Call GdipImageForceValidation after bitmap creation. This operation fails silently if not called immediately following bitmap creation.

The following code will fail silently. All DllCalls return 0 indicating success. This outcome will not change even if sleeps are added between lines, or asynchronous non-blocking timers are used.

; Incorrect code. 
pStream := ImagePutStream("https://picsum.photos/500")
DllCall("gdiplus\GdipCreateBitmapFromStream", "ptr", pStream, "ptr*", pBitmap1:=0)
DllCall("gdiplus\GdipCreateBitmapFromStream", "ptr", pStream, "ptr*", pBitmap2:=0)
DllCall("gdiplus\GdipImageForceValidation", "ptr", pBitmap1)
DllCall("gdiplus\GdipImageForceValidation", "ptr", pBitmap2)
ImagePut.show(pBitmap1, "1")
ImagePut.show(pBitmap2, "2") ; This image is corrupted or blurred.

Correct. Validation must be collated to each creation of the bitmap.

pStream := ImagePutStream("https://picsum.photos/500")
DllCall("gdiplus\GdipCreateBitmapFromStream", "ptr", pStream, "ptr*", pBitmap1:=0)
DllCall("gdiplus\GdipImageForceValidation", "ptr", pBitmap1)
DllCall("gdiplus\GdipCreateBitmapFromStream", "ptr", pStream, "ptr*", pBitmap2:=0)
DllCall("gdiplus\GdipImageForceValidation", "ptr", pBitmap2)
ImagePut.show(pBitmap1, "1")
ImagePut.show(pBitmap2, "2")

Note that the second bitmap is corrupted / blurred. This implies that the first bitmap has the superior lock (?). It is unknown why the first bitmap is not corrupted, as I expected. (The use of ImagePut.show() over ImagePutShow() bypasses the use of CloneImage on the input bitmap.)

Q: What happens when multiple bitmaps are created from a single stream?

A: A data race occurs if multiple bitmaps pointing to the same stream are accessed simultaneously. The cause of this is that the copying operation is interrupted by subsequent copying operations, and those original operations are never completed.