[Class] APNG - Animated PNGs in GUIs

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
RazorHalo
Posts: 45
Joined: 21 Dec 2015, 21:23

[Class] APNG - Animated PNGs in GUIs

Post by RazorHalo » 13 Dec 2023, 01:39

Class APNG
Class to display animated png (APNG) files in AutoHotkey. No external libraries or DLL's needed.
I created this as I was using tmplinshi's GIF class but wanted to have animations that supported better quality images and alpha transparency.

Credit goes out to @tmplinshi for the animated GIF class for the basic structure of this class and to @SKAN for the isPicture script which helped a lot in figuring out the png file format
some snippets of code were taken from both of those. There are few other functions that are referenced in the class which should have links to their respective posts.

There are several sample images to be used in the example script which can be downloaded.

Source

Code: Select all

;##################################################################################################
; Animated PNG Controls by RazorHalo
;##################################################################################################
class APNG {   

	__New(sFile, hWnd, cycle := true) {
	; References
	; https://wiki.mozilla.org/APNG_Specification
	; https://www.w3.org/TR/png/
	
	;  Verify its an APNG file
	File := FileOpen(sFile,"r")
	If (!IsObject(File))
		Return 0
		
	VarSetCapacity(bData,8,0)
	File.RawRead(bData,8)

	If (NumGet(bData,"Int64") != 0x0A1A0A0D474E5089)			; not a PNG file
		Return 0
		
	cTyp := 0
	this.frameArray := [], frmCount := -1
	this.frameDelay := []
	
	While (cTyp<>0x444E4549) {									; Until IEND chunk
        If (File.RawRead(bData,8)<8)
           Break
        cLen := this.SwapBytes(bData)							; Chunk data Length
        cTyp := NumGet(bData,4,"UInt")
        If (cTyp=0x52444849 && !this.IHDR) {					; IHDR chunk found
			File.Seek(-8,1)
			VarSetCapacity(bData,8+cLen+4,0)
			File.RawRead(bData, 8+cLen+4)
			this.width := this.SwapBytes(bData,8+0)
			this.height := this.SwapBytes(bData,8+4)
			this.SetCapacity("IHDR", 8+cLen+4)
			DllCall("RtlMoveMemory", "Ptr", this.GetAddress("IHDR"), "Ptr", &bData, "Ptr", 8+cLen+4)
			this.IHDR_Sz := 8+cLen+4
			Continue
        }
        If (cTyp=0x4C546361) {									; acTL chunk found
			File.Seek(-8,1)
			VarSetCapacity(bData,8+cLen+4,0)
            File.RawRead(bData, 8+cLen+4)
            this.frameCount := this.SwapBytes(bData,8+0)
            this.numPlays := this.SwapBytes(bData,8+4)
			this.SetCapacity("acTL", 8+cLen+4)
			DllCall("RtlMoveMemory", "Ptr", this.GetAddress("acTL"), "Ptr", &bData, "Ptr", 8+cLen+4)
			this.acTL_Sz := 8+cLen+4
			Continue
        }
		If (cTyp=0x45544C50) {									; PLTE chunk found
			File.Seek(-8,1)
			this.SetCapacity("PLTE", 8+cLen+4)
			File.RawRead(this.GetAddress("PLTE"), 8+cLen+4)
			this.PLTE_Sz := 8+cLen+4
			Continue
        }
		If (cTyp=0x73594870) {									; pHYs chunk found
			File.Seek(-8,1)
			this.SetCapacity("pHYs", 8+cLen+4)
			File.RawRead(this.GetAddress("pHYs"), 8+cLen+4)
			this.pHYs_Sz := 8+cLen+4
			Continue
        }
		If (cTyp=0x534E5274) {									; tRNS chunk found
			File.Seek(-8,1)
			this.SetCapacity("tRNS", 8+cLen+4)
			File.RawRead(this.GetAddress("tRNS"), 8+cLen+4)
			this.tRNS_Sz := 8+cLen+4
			Continue
        }
		If (cTyp=0x4C546366) {									; fcTL chunk found
			File.Seek(-8,1)
			VarSetCapacity(bData,8+cLen,0)
			File.RawRead(bData, 8+cLen)

			frmCount++
			this.frameArray[frmCount] := {}
			this.frameArray[frmCount].sequence_number := this.SwapBytes(bData,8)
			this.frameArray[frmCount].width := this.SwapBytes(bData,12)
			this.frameArray[frmCount].height := this.SwapBytes(bData,16)
			this.frameArray[frmCount].x_offset := this.SwapBytes(bData,20)
			this.frameArray[frmCount].y_offset := this.SwapBytes(bData,24)
			this.frameArray[frmCount].delay_num := this.SwapBytes(bData,28,2)
			this.frameArray[frmCount].delay_den := this.SwapBytes(bData,30,2)
			this.frameArray[frmCount].dispose_op := NumGet(bData,32, "UChar")	;0=APNG_DISPOSE_OP_NONE, 1=APNG_DISPOSE_OP_BACKGROUND, 2=APNG_DISPOSE_OP_PREVIOUS
			this.frameArray[frmCount].blend_op := NumGet(bData,33, "UChar")		;0=APNG_BLEND_OP_SOURCE, 1=APNG_BLEND_OP_OVER

			; Get frame delay in ms
			this.frameArray[frmCount].delay := Round((this.frameArray[frmCount].delay_num / this.frameArray[frmCount].delay_den) * 1000)
			; put delay into array
			this.frameDelay[frmCount] := this.frameArray[frmCount].delay

			this.frameArray[frmCount].SetCapacity("IHDR", 25)
			this.frameArray[frmCount].IHDR_Sz := 25
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IHDR")
								   , "Ptr", this.GetAddress("IHDR")
								   , "Ptr", 8)
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IHDR")+8
								   , "Ptr", &bData+12
								   , "Ptr", 8)
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IHDR")+16
								   , "Ptr", this.GetAddress("IHDR")+16
								   , "Ptr", 5)

			; Calculate new CRC32
			VarSetCapacity(bDataIn,17,0)
			DllCall("RtlMoveMemory", "Ptr", &bDataIn
								   , "Ptr", this.frameArray[frmCount].GetAddress("IHDR")+4
								   , "Ptr", 17)
			this.BinCRC32(CRC32Out, bDataIn, 17)
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IHDR")+21
								   , "Ptr", &CRC32Out
								   , "Ptr", 4)
			File.Seek(4,1)										; Skip past the CRC32 field	
			Continue
		}
		If (cTyp=0x54414449) {									; IDAT chunk found
			File.Seek(-8,1)
			If (!bSz := this.frameArray[frmCount].GetCapacity("IDAT"))
				bSz := 0
			this.frameArray[frmCount].SetCapacity("IDAT", bSz+8+cLen+4)
			File.RawRead(this.frameArray[frmCount].GetAddress("IDAT")+bSz, 8+cLen+4)
			this.frameArray[frmCount].IDAT_Sz := bSz+8+cLen+4
			Continue
		}
		If (cTyp=0x54416466) {									; fdAT chunk found
			File.Seek(-8,1)
			VarSetCapacity(bData,8+cLen,0)
			File.RawRead(bData, 8+cLen)
			
			; Convert fdAT chunk to IDAT chunk			
			new_cLen := cLen-4
			VarSetCapacity(bin,4,0)
			NumPut(new_cLen, bin, "UInt")
			
			If (!bSz := this.frameArray[frmCount].GetCapacity("IDAT"))
				bSz := 0
			this.frameArray[frmCount].SetCapacity("IDAT", bSz+8+new_cLen+4)
			
			; swap bytes of cLen (size of chunk) to write to file in Big Endian format
			NumPut(this.SwapBytes(bin), this.frameArray[frmCount].GetAddress("IDAT")+bSz, "UInt")
			; literal string "IDAT" in HEX format
			NumPut(0x54414449, this.frameArray[frmCount].GetAddress("IDAT")+bSz+4, "UInt")				
			; copy over the frame data - skipping over the sequence_number data (4 bytes at offset 8)
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IDAT")+bSz+8
								   , "Ptr", &bData+12
								   , "Ptr", new_cLen)
								   
			; recalculate CRC32
			; https://www.w3.org/TR/png/#table51
			; only calculated on the chunk type field and chunk data fields
			this.BinCRC32(CRC32Out, this.frameArray[frmCount].GetAddress("IDAT")+bSz+4, 4+new_cLen)
			DllCall("RtlMoveMemory", "Ptr", this.frameArray[frmCount].GetAddress("IDAT")+bSz+8+new_cLen
								   , "Ptr", &CRC32Out
								   , "Ptr", 4)
			this.frameArray[frmCount].IDAT_Sz := bSz+8+new_cLen+4
				
			File.Seek(4,1)										; Skip past the CRC32 field	
			Continue
		}
        File.Seek(cLen+4,1)
	} 

	file.Close()	
	this.file := sFile
	this.hWnd := hWnd
	this.cycle := cycle
	this.isPlaying := false
	this.frameCount := frmCount+1	; frmCount is zero based
	this.frameCurrent := -1
	
	; create PNGs
	Loop % this.framecount {
		this.CreateBitmap(this.frameArray[A_Index-1])
	}
	
	this._Play("")
	}
   
	CreateBitmap(frame) {
 		VarsetCapacity(PNGSignature, 8, 0)
		this.Hex2Bin(PNGSignature, "89504E470D0A1A0A")	
		VarsetCapacity(IEND, 12, 0)
		this.Hex2Bin(IEND, "0000000049454E44AE426082")

		; calculate new frame size in bytes
		bSize := 8										; PNG Signature size
				+ frame.IHDR_Sz							; critical chunk
				+ (this.PLTE_Sz ? this.PLTE_Sz : 0)		; ancillary chunk
				+ (this.pHYs_Sz ? this.pHYs_Sz : 0)		; ancillary chunk
				+ (this.tRNS_Sz ? this.tRNS_Sz : 0)		; ancillary chunk
				+ frame.IDAT_Sz							; frame data - critical chunk
				+ 12									; IEND
		
		frame.SetCapacity("PNG", bSize)
		frame.PNG_Sz := bSize

		; compile new PNG data
		Offset := 0
		; always start with PNG signature and IHDR chunks
		DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG"), "Ptr", &PNGSignature, "Ptr", 8), Offset += 8
		DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", frame.GetAddress("IHDR"), "Ptr", frame.IHDR_Sz), Offset += frame.IHDR_Sz

		; add any ancillary chunks here
		If (this.PLTE_Sz) {
			DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", this.GetAddress("PLTE"), "Ptr", this.PLTE_Sz), Offset += this.PLTE_Sz
		}
		If (this.pHYs_Sz) {
			DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", this.GetAddress("pHYs"), "Ptr", this.pHYs_Sz), Offset += this.pHYs_Sz
		}
		If (this.tRNS_Sz) {
			DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", this.GetAddress("tRNS"), "Ptr", this.tRNS_Sz), Offset += this.tRNS_Sz
		}
		
		; Always end with IDAT chuunks then IEND
		DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", frame.GetAddress("IDAT"), "Ptr", frame.IDAT_Sz), Offset += frame.IDAT_Sz
		DllCall("RtlMoveMemory", "Ptr", frame.GetAddress("PNG")+Offset, "Ptr", &IEND, "Ptr", 12)	
		
		frame.IDAT := ""	; free IDAT binary data to save memory as it is no longer needed
	}
   
	; Compile the PNGs into complete frames for display
	CreateFrame(frame) {
		static pOutputBuffer:=[], G_OutputBuffer:=[]

		If (!pOutputBuffer[this.hWnd]) {
			pOutputBuffer[this.hWnd] := Gdip_CreateBitmap(this.width, this.height)
			G_OutputBuffer[this.hWnd] := Gdip_GraphicsFromImage(pOutputBuffer[this.hWnd])
		}
		
		; Save current frame region of outputbuffer
		; APNG_DISPOSE_OP_PREVIOUS
		If (frame.dispose_op = 2) {
			TMP_pBitmap := Gdip_CreateBitmap(frame.Width, frame.Height)
			G_TMP := Gdip_GraphicsFromImage(TMP_pBitmap)
			Gdip_DrawImage(G_TMP, pOutputBuffer[this.hWnd], 0, 0, frame.Width, frame.Height, frame.x_offset, frame.y_offset, frame.Width, frame.Height)
		}

		DllCall("ole32\CreateStreamOnHGlobal", Ptr, frame.GetAddress("PNG"), "int", 1, A_PtrSize ? "UPtr*" : "UInt*", pStream)
		DllCall("gdiplus\GdipCreateBitmapFromStream", "UPtr", pStream, "PtrP", pFrameBitmap)
		DllCall(NumGet(NumGet(1*pStream)+8), "uint", pStream)
		
		;APNG_BLEND_OP_SOURCE
		If (!frame.blend_op) {	
			Gdip_SetClipRect(G_OutputBuffer[this.hWnd], frame.x_offset, frame.y_offset, frame.Width, frame.Height)
			Gdip_GraphicsClear(G_OutputBuffer[this.hWnd], 0x00000000)
			Gdip_DrawImage(G_OutputBuffer[this.hWnd], pFrameBitmap, frame.x_offset, frame.y_offset, frame.Width, frame.Height)
			
		;APNG_BLEND_OP_OVER
		} Else {
			E1 := Gdip_LockBits(pOutputBuffer[this.hWnd], frame.x_offset, frame.y_offset, frame.Width, frame.Height, Stride01, Scan01, BitmapData01)
			E2 := Gdip_LockBits(pFrameBitmap, 0, 0, frame.Width, frame.Height, Stride02, Scan02, BitmapData02)

			Loop % frame.width
			{
			  xindex := A_Index - 1
			  Loop % frame.height
			  {
				yindex := A_Index-1
				color1 := Gdip_GetLockBitPixel(Scan01, xindex, yindex, Stride01)
				color2 := Gdip_GetLockBitPixel(Scan02, xindex, yindex, Stride02)
				Gdip_FromARGB(color1, a1, r1, g1, b1)	;backgroung (dest)
				Gdip_FromARGB(color2, a2, r2, g2, b2)	;foreground (source)
				If (a2 == 0) {					
					; Foreground image is transparent here, there is nothing to do.
				} Else If (a2 == 255) {
					; Foregrond is Opaque - copy pixel to background
					Gdip_SetLockBitPixel(color2, Scan01, xindex, yindex, Stride01)
				} Else {	
					; Compositing is necessary
					u := a2*255
					v := (255-a2)*a1
					alpha := u+v
					r3 := (r2*u+r1*v)/alpha
					g3 := (g2*u+g1*v)/alpha
					b3 := (b2*u+b1*v)/alpha
					a3 := alpha/255

					color3 := Gdip_ToARGB(a3, r3, g3, b3)
					Gdip_SetLockBitPixel(color3, Scan01, xindex, yindex, Stride01)				
				}

			  }
			}
			
			Gdip_UnlockBits(pOutputBuffer[this.hWnd], BitmapData01), Gdip_UnlockBits(pFrameBitmap, BitmapData02)
		}
			
		Gdip_ResetClip(G_OutputBuffer[this.hWnd])
		frame.pBitmap := Gdip_CloneBitmapArea(pOutputBuffer[this.hWnd])
		
		; DISPOSE_OP
		
		; APNG_DISPOSE_OP_BACKGROUND
		; the frame's region of the output buffer is to be cleared to fully transparent black
		; before rendering the next frame.
		If (frame.dispose_op = 1) {
			Gdip_SetClipRect(G_OutputBuffer[this.hWnd], frame.x_offset
							  , frame.y_offset
							  , frame.Width
							  , frame.Height)
			Gdip_GraphicsClear(G_OutputBuffer[this.hWnd], 0x00000000)	
			
		; APNG_DISPOSE_OP_PREVIOUS
		; the frame's region of the output buffer is to be reverted to the previous contents
		; before rendering the next frame.
		} Else If (frame.dispose_op = 2) {	;APNG_DISPOSE_OP_PREVIOUS
			Gdip_SetClipRect(G_OutputBuffer[this.hWnd], frame.x_offset
							  , frame.y_offset
							  , frame.Width
							  , frame.Height)
			Gdip_GraphicsClear(G_OutputBuffer[this.hWnd], 0x00000000)
			Gdip_DrawImage(G_OutputBuffer[this.hWnd], TMP_pBitmap
							  , frame.x_offset
							  , frame.y_offset
							  , frame.Width
							  , frame.Height)

			Gdip_DeleteGraphics(G_TMP)
			Gdip_DisposeImage(TMP_pBitmap)
		}
		
		Gdip_DisposeImage(pFrameBitmap)
		
		; Dispose of output buffer if its the last frame
		If (this.framecurrent = this.frameCount-1) {
			Gdip_ResetClip(G_OutputBuffer[this.hWnd])
			Gdip_GraphicsClear(G_OutputBuffer[this.hWnd], 0x00000000)
		}
		
		Return frame.pBitmap
   }
   
   
	Play() {
		this.isPlaying := 1
		fn := this._Play.Bind(this)
		this._fn := fn
		SetTimer, % fn, -1
	}
   
	Pause() {
		this.isPlaying := false
		fn := this._fn
		SetTimer, % fn, Delete
	}
   
	_Play(mode := "set") {	
		this.frameCurrent := mod(++this.frameCurrent, this.frameCount)
		hBitmap := Gdip_CreateHBITMAPFromBitmap(this.CreateFrame(this.frameArray[this.frameCurrent]))
		SetImage(this.hwnd, hBitmap)
		DeleteObject(hBitmap)
		Gdip_DisposeImage(this.frameArray[this.frameCurrent].pBitmap)
		if (mode = "set" && this.frameCurrent < (this.cycle ? 0xFFFFFFFF : this.frameCount - 1)) {
			fn := this._fn
			SetTimer, % fn, % -1 * this.frameDelay[this.frameCurrent]
		} Else {
			this.isPlaying := false
		}
	}
   
	__Delete() {
		Gdip_DeleteGraphics(this.G_OutputBuffer[this[hWnd]])
		Gdip_DisposeImage(this.pOutputBuffer[this[hWnd]])
		; Probably need more clean-up here??
	}
   
	SwapBytes(byref bData, offset:=0, size:=4) {
		If (size=4) {
			VarSetCapacity(Num,4,0)
			Num := NumGet(bData, offset, "uInt")
			Return (0xFF000000&Num)>>24 | (0xFF0000&Num)>>8 | (0xFF00&Num)<<8 | (0xFF&Num)<<24
		} Else if (size=2) {
			VarSetCapacity(Num,2,0)
			Num := NumGet(bData, offset, "uShort")
			Return (0xFF00&Num)<<8 | (0xFF&Num)<<24
		}
	}
	
	BinCRC32(byref out, byref buffer, size) {
		hMod := DllCall("Kernel32.dll\LoadLibrary", "Str", "Ntdll.dll")
		CRC := DllCall("Ntdll.dll\RtlComputeCrc32", "UInt", CRC, "UInt", &buffer, "UInt", size, "UInt")
		CRC := Format("{:x}", crc)
		DllCall("Crypt32.dll\CryptStringToBinary", "Ptr", &CRC, "UInt", StrLen(CRC)
				, "UInt", 0x8, "Ptr", 0, "UInt*", OutLen, "Ptr", 0, "Ptr", 0)
		VarSetCapacity(Out, OutLen)
		DllCall("Crypt32.dll\CryptStringToBinary", "Ptr", &CRC, "UInt", StrLen(CRC)
				, "UInt", 0x8, "Str", Out, "UInt*", OutLen, "Ptr", 0, "Ptr", 0)
		return OutLen, DllCall("Kernel32.dll\FreeLibrary", "Ptr", hMod)
	}
	
	;;https://github.com/ahkscript/libcrypt.ahk
	Hex2Bin(ByRef Out, ByRef In, Flags:=0x8) {
		DllCall("Crypt32.dll\CryptStringToBinary", "Ptr", &In, "UInt", StrLen(In)
		, "UInt", Flags, "Ptr", 0, "UInt*", OutLen, "Ptr", 0, "Ptr", 0)
		VarSetCapacity(Out, OutLen)
		DllCall("Crypt32.dll\CryptStringToBinary", "Ptr", &In, "UInt", StrLen(In)
		, "UInt", Flags, "Str", Out, "UInt*", OutLen, "Ptr", 0, "Ptr", 0)
		return OutLen
	}
}

I'm unsure if I clean up at the end properly but I do not see it leaking memory from my tests, let me know if I have done that correctly or not.


Example

Code: Select all

#NoEnv
#SingleInstance, Force
SetBatchLines -1

#Include gdip.ahk			;<---your GDIP library

pToken := Gdip_Startup()

exStyles := (WS_EX_COMPOSITED := 0x02000000) | (WS_EX_LAYERED := 0x80000)
Gui, New, +E%exStyles%

Gui Add, Picture, hWndHImage gPlay x10 y50 w32 h32 0xE
Gui Add, Picture, hWndHImage2 gPlay2 x200 y20 w32 h32 0xE
Gui Add, Picture, hWndHImage3 gPlay3 x400 y20 w32 h32 0xE
Gui Add, Picture, hWndHImage4 gPlay4 x10 y100 w32 h32 0xE
Gui Add, Text, x50 y500 w300, Click pics to play/pause animations
Gui Show, w500 h530, APNG Animations v1.0

Button_REV := New APNG("PNGButton3_REV.png", hImage, 0)
Button := New APNG("PNGButton3.png", hImage, 0)
Button2 := New APNG("731_128.png", hImage2, 1)
Button3 := New APNG("Animated_PNG_example_bouncing_beach_ball.png", hImage3, 1)
Button4 := New APNG("elephant.png", hImage4, 1)

Dir := ""

Return


Play:
	If (Button.Isplaying || Button_REV.Isplaying)
		Return
	Button%Dir%.Play()
	Dir := Dir = "_REV" ? "" : "_REV"
Return

Play2:
	If (!Button2.isPlaying) {
	   Button2.Play()
	} Else {
	   Button2.Pause()
	}
Return

Play3:
	If (!Button3.isPlaying) {
	   Button3.Play()
	} Else {
	   Button3.Pause()
	}
Return

Play4:
	If (!Button4.isPlaying) {
	   Button4.Play()
	} Else {
	   Button4.Pause()
	}
Return



GuiClose:
Gdip_ShutDown(pToken)
 ExitApp
Attachments
apng_samples.zip
(555.59 KiB) Downloaded 60 times
robodesign
Posts: 934
Joined: 30 Sep 2017, 03:59
Location: Romania
Contact:

Re: [Class] APNG - Animated PNGs in GUIs

Post by robodesign » 13 Dec 2023, 10:10

Pure gold. Thank you!!!
-------------------------
KeyPress OSD v4: GitHub or forum. (presentation video)
Quick Picto Viewer: GitHub or forum.
AHK GDI+ expanded / compilation library (on GitHub)
My home page.
Post Reply

Return to “Scripts and Functions (v1)”