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