From 44d812f33e0260015e97907484d99c49781525d2 Mon Sep 17 00:00:00 2001 From: jalvesz <102541118+jalvesz@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:16:35 +0000 Subject: [PATCH 1/2] Add APNG output to save_animation Frames written to a .png or .apng file are stored as full-colour RGB instead of being quantised to a 256 colour GIF palette. The deflate and scanline filtering of png_encode is shared with the new apng_encode. --- README.md | 2 +- src/fplotlib.f90 | 25 +++++-- src/fplotlib_png.f90 | 141 ++++++++++++++++++++++++++++++------- tests/fingerprints.txt | 20 ++++++ tests/test_fingerprint.f90 | 17 ++++- tests/test_image_io.f90 | 91 +++++++++++++++++++++--- tests/test_plots.f90 | 14 +++- 7 files changed, 267 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 8bff704..3203484 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ call suptitle("figure title") scale along an edge - `table` for a block of text below (or above) the axes - `streamplot` for the streamlines of a vector field -- `add_frame` / `save_animation(file, fps=, loop=)` to write an animated GIF +- `add_frame` / `save_animation(file, fps=, loop=)` to write an animated GIF, or a full-colour animated PNG with a `.png` or `.apng` file name - `spy` (sparsity pattern of a dense matrix or COO indices), `matshow`, `eventplot` and `broken_barh` - `hist2d` and `hexbin` for counting points into square or hexagonal bins - `imshow(interpolation="bilinear")` to smooth an image instead of diff --git a/src/fplotlib.f90 b/src/fplotlib.f90 index 9b02b54..3f47bb3 100644 --- a/src/fplotlib.f90 +++ b/src/fplotlib.f90 @@ -13,6 +13,7 @@ module fplotlib use fplotlib_backend_pdf use fplotlib_backend_eps use fplotlib_gif, only: gif_encode + use fplotlib_png, only: apng_encode use fplotlib_proj3d use fplotlib_backend_png use fplotlib_mathtext @@ -6233,7 +6234,8 @@ subroutine anim_append(frame) anim_len = need end subroutine anim_append - ! Writes the frames collected so far and starts a new reel, so that a + ! Writes the frames collected so far as a GIF, or as an animated PNG for + ! .png and .apng (full colour), and starts a new reel, so that a ! program can make several animations without a reset call. subroutine save_animation(filename, fps, loop) character(len=*), intent(in) :: filename @@ -6247,10 +6249,13 @@ subroutine save_animation(filename, fps, loop) print *, "fplotlib: no frames; call add_frame before save_animation" return end if - if (file_ext(filename) /= "gif") then - print *, "fplotlib: save_animation writes .gif, not .", file_ext(filename) + select case (file_ext(filename)) + case ("gif", "png", "apng") + case default + print *, "fplotlib: save_animation writes .gif, .png or .apng, not .", & + file_ext(filename) return - end if + end select rate = 5.0_dp if (present(fps)) then if (fps <= 0.0_dp) error stop "fplotlib: save_animation fps must be positive" @@ -6262,8 +6267,16 @@ subroutine save_animation(filename, fps, loop) ! GIF counts delays in hundredths of a second and viewers treat 0 as ! "as fast as you like", so a frame never asks for less than one. delay = max(1, nint(100.0_dp/rate)) - call write_bytes(filename, gif_encode(anim_w, anim_h, anim_n, & - anim_pix(1:anim_len), delay, rep)) + if (file_ext(filename) == "gif") then + call write_bytes(filename, gif_encode(anim_w, anim_h, anim_n, & + anim_pix(1:anim_len), delay, rep)) + else + ! APNG delays are a fraction, so the frame rate is not rounded + ! to hundredths of a second. + call write_bytes(filename, apng_encode(anim_w, anim_h, anim_n, & + anim_pix(1:anim_len), & + min(65535, max(1, nint(1000.0_dp/rate))), 1000, rep)) + end if anim_n = 0 anim_w = 0 anim_h = 0 diff --git a/src/fplotlib_png.f90 b/src/fplotlib_png.f90 index 439a645..f81dd5d 100644 --- a/src/fplotlib_png.f90 +++ b/src/fplotlib_png.f90 @@ -21,7 +21,7 @@ module fplotlib_png ! zlib_compress is here rather than in the PDF backend because a PDF ! FlateDecode stream is the same zlib stream a PNG carries, and one ! compressor is enough. - public :: png_encode, zlib_compress + public :: png_encode, apng_encode, zlib_compress ! LZ77 parameters. WSIZE is fixed by the format; CHAIN bounds how hard the ! matcher looks before settling, which is the usual size/speed dial. @@ -314,12 +314,118 @@ function png_encode(w, h, rgba) result(bytes) integer, intent(in) :: w, h integer, intent(in) :: rgba(4, w, h) character(len=:), allocatable :: bytes + type(bitbuf_t) :: out + + call push_signature(out) + call ihdr(out, w, h, 6) + call chunk(out, "IDAT", filtered_zlib(w, h, 4, rgba)) + call chunk(out, "IEND", NO_DATA) + bytes = to_string(out) + end function png_encode + + ! ------------------------------------------------------------------ + ! Encode nframes RGB frames as an animated PNG. pix holds the frames as + ! packed RGB bytes, one after another, as the animation reel keeps them. + ! A delay is delay_num/delay_den seconds; loop true repeats forever. + ! Every frame is a full, opaque replacement of the canvas, so the first + ! frame is also the default image that viewers without APNG support show. + ! ------------------------------------------------------------------ + + function apng_encode(w, h, nframes, pix, delay_num, delay_den, loop) result(bytes) + integer, intent(in) :: w, h, nframes, delay_num, delay_den + character(len=*), intent(in) :: pix + logical, intent(in) :: loop + character(len=:), allocatable :: bytes + integer, allocatable :: rgb(:, :, :), z(:), d(:) + integer :: f, x, y, c, k, seq, base, nz + type(bitbuf_t) :: out + + allocate (rgb(3, w, h)) + call push_signature(out) + call ihdr(out, w, h, 2) + + allocate (d(8)) + call be32(nframes, d(1:4)) + d(5:8) = 0 ! zero plays forever + if (.not. loop) call be32(1, d(5:8)) + call chunk(out, "acTL", d) + deallocate (d) + + seq = 0 + do f = 1, nframes + base = (f - 1)*3*w*h + k = base + do y = 1, h + do x = 1, w + do c = 1, 3 + k = k + 1 + rgb(c, x, y) = iachar(pix(k:k)) + end do + end do + end do + + allocate (d(26)) + call be32(seq, d(1:4)) + call be32(w, d(5:8)) + call be32(h, d(9:12)) + d(13:20) = 0 ! x and y offset + d(21) = iand(ishft(delay_num, -8), 255) + d(22) = iand(delay_num, 255) + d(23) = iand(ishft(delay_den, -8), 255) + d(24) = iand(delay_den, 255) + d(25) = 0 ! dispose: leave the canvas as it is + d(26) = 0 ! blend: replace + call chunk(out, "fcTL", d) + deallocate (d) + seq = seq + 1 + + z = filtered_zlib(w, h, 3, rgb) + if (f == 1) then + call chunk(out, "IDAT", z) + else + nz = size(z) + allocate (d(4 + nz)) + call be32(seq, d(1:4)) + d(5:) = z + call chunk(out, "fdAT", d) + deallocate (d) + seq = seq + 1 + end if + end do + + call chunk(out, "IEND", NO_DATA) + bytes = to_string(out) + end function apng_encode + + subroutine push_signature(out) + type(bitbuf_t), intent(inout) :: out + + call push(out, 137); call push(out, 80); call push(out, 78) + call push(out, 71); call push(out, 13); call push(out, 10) + call push(out, 26); call push(out, 10) + end subroutine push_signature + + function to_string(out) result(bytes) + type(bitbuf_t), intent(in) :: out + character(len=:), allocatable :: bytes + integer :: i + + allocate (character(len=out%n) :: bytes) + do i = 1, out%n + bytes(i:i) = achar(out%b(i)) + end do + end function to_string + + ! The zlib stream of an image's scanlines, each filtered with whichever of + ! the five PNG filters looks cheapest. px is bpp interleaved channels. + function filtered_zlib(w, h, bpp, px) result(z) + integer, intent(in) :: w, h, bpp + integer, intent(in) :: px(bpp, w, h) + integer, allocatable :: z(:) integer, allocatable :: raw(:), comp(:), cur(:), up(:), try(:), best(:) - integer :: bpp, stride, nraw, x, y, c, i, p, f, score, bscore, bf + integer :: stride, nraw, x, y, c, i, p, f, score, bscore, bf integer :: a_, b_, c_, v - type(bitbuf_t) :: out - bpp = 4 stride = w*bpp nraw = h*(stride + 1) allocate (raw(nraw), cur(stride), up(stride), try(stride), best(stride)) @@ -329,9 +435,9 @@ function png_encode(w, h, rgba) result(bytes) do y = 1, h i = 0 do x = 1, w - do c = 1, 4 + do c = 1, bpp i = i + 1 - cur(i) = iand(rgba(c, x, y), 255) + cur(i) = iand(px(c, x, y), 255) end do end do @@ -372,21 +478,8 @@ function png_encode(w, h, rgba) result(bytes) end do comp = deflate(raw, nraw) - - ! Signature - call push(out, 137); call push(out, 80); call push(out, 78) - call push(out, 71); call push(out, 13); call push(out, 10) - call push(out, 26); call push(out, 10) - - call ihdr(out, w, h) - call chunk(out, "IDAT", zlib_wrap(comp, raw, nraw)) - call chunk(out, "IEND", NO_DATA) - - allocate (character(len=out%n) :: bytes) - do i = 1, out%n - bytes(i:i) = achar(out%b(i)) - end do - end function png_encode + z = zlib_wrap(comp, raw, nraw) + end function filtered_zlib ! Bytes in, a zlib stream out. Unlike a PNG the caller supplies its own ! layout, so nothing is filtered here. @@ -421,15 +514,15 @@ function zlib_wrap(comp, raw, nraw) result(z) z(n + 6) = iand(a, 255) end function zlib_wrap - subroutine ihdr(out, w, h) + subroutine ihdr(out, w, h, ctype) type(bitbuf_t), intent(inout) :: out - integer, intent(in) :: w, h + integer, intent(in) :: w, h, ctype integer :: d(13) call be32(w, d(1:4)) call be32(h, d(5:8)) d(9) = 8 ! bit depth - d(10) = 6 ! truecolor with alpha + d(10) = ctype ! 2 truecolor, 6 truecolor with alpha d(11) = 0 ! deflate d(12) = 0 ! adaptive filtering d(13) = 0 ! no interlace diff --git a/tests/fingerprints.txt b/tests/fingerprints.txt index 6ee6130..e15293b 100644 --- a/tests/fingerprints.txt +++ b/tests/fingerprints.txt @@ -148,3 +148,23 @@ anim_sine_f17 640 480 6833c312e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c0602 anim_sine_f18 640 480 6833c312e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef504cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b4ddf11a137d3a2d3cf7db67eed480729cf567f2db6b48b96d83e50c2d01e5af835af019a5629578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a242c96c7b55c0808e02165efce4364810b74afb14fb978a3e00000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7f891d6dc71c4e03f0000000000000000ad7065b4338b38b056cdecb8d73fa5290000000000000000a8dcd1a316ed0287fcde1e506cfeed5aa564e214757842a07a15dc5f64592fb9000000000000000000000000000000007cb1ef5292bebc880000000000000000bb6a6bd70a94057d3e2a2fb963d910d9df18ea01565e42880000000000000000000000000000000000000000000000009151553438a3705fd5b8de3fe3ed8290f1449c6d30d95b5eca1e3ad9090f47c3e8b1c871aa997be20000000000000000000000000000000000000000000000000000000000000000637cb5a7eef1c45a4cb261193db438d3f1164b880ce875ac5ab6cdc0b4efee4855a4e8f77c00eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e anim_sine_f19 640 480 6833c312e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef504cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b4ddf11a137d3a2d3cf7db67eed480729cf567f2db6b487db2f1cfeab919f4ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a26c0790d05b0b752ee83fff948b8e4e67fed414f830297d5800000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7e708263e874b45ca00000000000000004095c67f6168bfb300000000000000000000000000000000a8dcd1a316ed0287fcde1e506cfeed5a807d115ab20568864130bf45cec745ef0000000000000000e13d8f8d0c7219a0ae27ef17ab7a7f0e0000000000000000bb6a6bd70a94057d3e2a2fb963d910d9f642c72edb7dc9d10000000000000000000000000000000000000000000000003ec11fe1ae1ec75f3078231d49a59f4af1449c6d30d95b5eca1e3ad9090f47c3ecb1c871aa993ac2000000000000000000000000000000000000000000000000222440119950184aad73ae72ea4bd3244cb261193db438d3f1164b880ce875ac5ab6cdc0b4efee4855a4e8f77c00eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e anim_sine_f20 640 480 6833c312e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef504cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b4ddf11a137d3a2d3cf7db67eed48062be754bf6fb636828cfe0e21e6ccf14ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a209a50d4ea2706d43f5b209692901d7fb9e8afafe55a7a33900000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730caf7fa83ca2946b38b347c3b02afc420c10000000000000000e2660877e877115d00000000000000000000000000000000a8dcd1a316ed0287fcde1e506cfeed5abd34df143c55871f0000000000000000000000000000000050b4be6cba76bc02c5cc2fcc19f7fa0e0000000000000000bb6a6bd70a94057d3e2a2fb963d910d96846d7331f5707730000000000000000000000000000000000000001000000105315cc8b2c3b104f969de3a5e5624987f1449c6d30d95b5eca1e3ad9090f47c3ecb1c871aa993ac2000000000000000000000000000000000000000000000000a37fa2048b634ab981d3304774fa6ee34cb261193db438d3f1164b880ce875ac5ab6cdc0b4efee4855a4e8f77c00eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f01 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed4805fc5f314d49d5ae325db74127621f3f0ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725dfc8c7a3f55aeb534ab344199e85c7cb65c6c17b508088c67000000000000000000000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cae3675076562ba16d0000000000000000b11118980b479e9a4e45d23c21e715cb00000000000000000020010001001000a8dcd1a316ed0287fcde1e506cfeed5a3f9632cc69ffc3ec00000000000000000000000000000000524372f08cbb38160000000000000000657cd8c51ab1791abb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740000000000000000000000000000000008948a63d8d8d44cab7248661624c1ef8b6f81a96496c68bef1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000000000000000000000e21fd9094e29da6c00000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f02 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480dfa9c8e516fb621925db74127621f3f0ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725dcc50e0dfc7004e1287d4350752e0fabe39bef122ae091d76000000000000000000000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730ca2dd5a7450bccdfce0000000000000000fd38dd8dbf5ee43d00000000000000000000000000000000d92d3b7411f4c9aba8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c0000000000000000074cc625ead068812971cc7b6d9ebd130000000000000000f45fdaa4a6bd78a6bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e77400000000000000000000000000000000082cef30a9895369e8b03691d5c97d60d2e8858ef0c2ae6aff1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000003d47c249dcf877c417b5f7f6df442fee00000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f03 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3aa83de7cf7cdbe4941537af69414f940f25db74127621f3f0ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725da1ee5c12df477e7ba611b1771e6caec7232ebac41117b9a500000000000000000000000000000000e5300f5a620ecc17d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a70000000000000000e76f6c2ad2b3eef200000000000000000000000000000000e676c20b9c7dd242a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c0000000000000000435d2b87429981923006b4203a330012a34298ea573631100130615580d08dc8bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740000000000000000062442048264024259403bff5bb2e20d6926843f396666d7a0802000000040000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000001e263444e2a606c91a86904af8e7e02500000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f04 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a0f67e60389749f6970183d63f5ebee0325db74127621f3f0ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d27bacf264acc063fd8d3a7fcbfefb541000000000000000000000000000000000000000000000000230e558b64f0b1b3d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7a13cdd43323709909a6f14044204716d00000000000000000000000000000000b847414ed8a05875a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c0000000000000000d0b5a0c3c54cc72d00000000000000009754f78fb37345ad8ef8a7a9d98381e3bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e77400000000000000000809d97193cc26bed671e31541d3ea175b4fbfe5c6c4e59d00000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000001eb170ac2433234b0881f00410a8020200000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f05 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3ac8af3fd60b14a8d3729cf567f2db6b4825db74127621f3f0ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725daa1a632d2b91d85667d0b6ebc8a6de1a0000000000000000000000000000000000000000000000006875b1a2557c0e8ad1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7ad59cde69fe8e68d2801410a0740e135000000000000000054583e52735afb4483970063b89afcf4a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c0153800300a02045ff6b70479a1dbd40000000000000000012718531da462ba00000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e774000000000000000009ff69a239581fc9a1be6089d948fdd198d6e4762c7ab8a290000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000005935fc0de922051f000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f06 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3ad164cb0684920066729cf567f2db6b4825db74127621f3f0ab835ae019a572d578176ae5641fcee27d8495c6f1de5d5aa359466920cac7cd44de31cd4cec725dc93842046d872a0edcacaa5b303318ee000000000000000000000000000000000040000000801601f82bfcd00313a048d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7ddfef5757e18306900000000000000000000000000000000233c39a7cb8e70ac3d77a4808930b977a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c71c284e2d823faf94fd64c5479e6e0d60000000000000000c0832f12dcaa8e860000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e774000000000000000002ef8eca18c616b8160f80e674d2de92865fedfdbc5f0f69b0000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000026d9987ae50ed524236c820dfbc59ab2000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f07 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a6e7cf79b63eed4a2729cf567f2db6b4825db74127621f3f0ab835ae019a572d578176ae5641fcee2d0c2c6ace285e7a2a359466920cac7cd44de31cd4cec725d985c6a556ea4db4f1312044204519c8000000000000000000000000000000000ce9636f082b4ce6ebbf73dc6fed707a7d1b326e7f4fcbf8fd6bd6429ec7730caa6cd4b3f1ae347a679a561c0f1bcb49a00000000000000000000000000000000d862c86da8ba92090000000000000000a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c0130aa5e8dfba0f100000000000000004805974b58d5f265bc374426a394a7f70000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740063a58ce8db41aeedc0fbff45ce43b3c3174a1a697e2987e00000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac200000000000000002ec9e4bc8a0b63b7a46147387e835297000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f08 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0ab835ae019a572d578176ae5641fcee28131fc41ebe9c904a359466920cac7cd44de31cd4cec725d047eb4ab8cd3e8dd000000000000000000000000000000000000000000000000af816cd91937fadab567e9ba9c6e9b13d1b326e7f4fcbf8fd6bd6429ec7730cae658d1b5060930ee4c1531fd75f15183000000000000000000100020000040800c59e9cc340c010b0000000000000000a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c32ee26f99a2b0f3f0000000000000000234841b4344813880619610743982de60000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740060628f64d43ac321aa404974a2a43aebf81d10da03cdb4a00000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac20000000000000000690f31e33ec4dfe20020000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f09 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0ab835ae019a572d559457b3c610fda6a60dec288e767fb3da359466920cac7cd44de31cd4cec725dae3a27bc92dcc0ee00000000000000000000000000000000000000000000000011810c1d817999f0f483040cb701abeed1b326e7f4fcbf8fd6bd6429ec7730cafb62dc8b9525458d20000000000020000000000000000000e89e14908f5cb4c573a5b6f08cf48be00000000000000000a8dcd1a316ed0287fcde1e506cfeed5a72fbe7057c1bddbfb3f0db17186757150000000000000000cb7eb687d8c3d79d00000000000000000000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740d301bac18a251a3832402534260fc78491161bf0b0154e8600000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2008000000000110071f29049a011eeb90000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f10 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0ab835ae019a572d56634e2f2cf3dc8a5db94872088300501a359466920cac7cd44de31cd4cec725d760eae763d5bf1a20000000000000000000000000000000005151fb70d3f5ca0be9a9e8504953a4c94dc2593c0298724d1b326e7f4fcbf8fd6bd6429ec7730ca1e27c6d5fc87a5fa000000000000000000000000000000009b515a6683e2c7ea0001c4101680800859a94d5c49a120aca8dcd1a316ed0287fcde1e506cfeed5a9c55aebcbe22591fac70f1e7e61cce0f00000000000000008f3e08016c2d554900000000000000000000000000000000bb6a6bd70a94057d2e2a2fb963d910d9cb79c34be85e7740fdf35de16b408e93929387adf678302e5f24415d928b6ecc00000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2606979dd23860c9929387b19e29003c70000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f11 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0ab835ae019a572d514c80a79249db88b1b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a200000000000000000000000000000000f7f5f712a4939aa8766aac5d1213ab1ee5b4d87414fc09ebd1b326e7f4fcbf8fd6bd6429ec7730caa2598b17ca88f8b30000000000000000000000000000000042e47d568d1893530000000000000000b5a46f02bcf0a99da8dcd1a316ed0287fcde1e506cfeed5a7a1a824ed00a458000000000000000009eb7dbe8f97377abc0dd55931342341d00000000000000000000000000000000bb6a6bd70a94057d2e2a2fb963d910d95d0e778ac4bb093f21c409a8dd113d6e8f6c3aa0eaa5bb1c000000000000000000000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2e96fda7f80df3aecb2d4e25b7c8ad4d80000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f12 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0ab835ae019a572d5c142f32a5fd73ce41b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a200000000000000000000000000000000e01d31ccef1ce807b5d113781261daadd62b344a230732e7d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a70000000000000000a86971b0800aa7c4c79fef4d36600cb50000000000000000816f8219838f3e0fa8dcd1a316ed0287fcde1e506cfeed5ad6011b17dd0a58b100000000000000002b64e440c367cdd31f45408d00a29d000000000000000000f16f26254e512c3fbb6a6bd70a94057d2e2a2fb963d910d9ac5e819a6fe93cee25fd4eeaa41adbeec78e552a453f443d000000000000000000000000000000000000000000000000f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac25a7cdb39f95454ce00000000000000000000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f13 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f00982e6c2d5676a7dfaba3770801389021b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a200000000000000000000000000000000bab6c0d6c564b6247aef9fa4c8b612badcb3be64c58f2402d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a70000000000000000b604d3ec042c0a6bda27ad96fafa4ae902901044aa04080b9d69628a7e43c96aa8dcd1a316ed0287fcde1e506cfeed5a75b1883e8ce59bc000000000000000008188321839c35f2e000000000000000000000000000000003d0c7b411d7c3387bb6a6bd70a94057d2e2a2fb963d910d999d1619aad90862132383fbea5423bb8fd447836fbc5a818000000000000000000000000000000000040000002000000f1449c6d30d95b5eca1e3ad9090f47c764d6e81386417ec0a6359ebf0e60725500000000000000000000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f14 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f0631e92d0087d9acc3c8278e46a0f4ad21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a20000000000000000bc66d948affdb6405c08e3fe8dedb1a6b1df17ccfc14d1c20000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a70000000000000000a8d4976a497eb8480000000000000000fdcdd465fb48b7d997362530fbc60701a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833ce0c9705663a352e88aafb801de4e36dd00000000000000000000000000000000c86b4fcf6b5a1eefbb6a6bd70a94057d2e2a2fb963d910d905593c450ce8b31a3bb2eee663a49000f2401148c429840800000000000000000000000000000000b305f3826dfe7a0af1449c6d30d95b5eca1e3ad9090f47c78f4c3414cc28727b117dc5697e62a9d900000000000000000000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f15 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f04b53992a6cc4b72478176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a200000000000000009862c363793cf630f5457d78f7c9004441c4e742fef038cb0000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a70000000000000000afdc60baaa7a66f70000000000000000d6ce3d268e02021f0030080a20000090a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c22da465433e2d8fc1b5d779faecb669200000000000000000136f00b96e29e537257eaba211643c4bb6a6bd70a94057d2e2a2fb963d910d905be3aaaa009dc9d8d1bbd33083c5a2c000000000000000000000000000000000000000000000000cbc8a08636d67566f1449c6d30d95b5eca1e3ad9090f47c7b3ddea43a2cf153715a585815a52588c00000000000000000000000000000000000000000000000000000000000000004cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f16 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4825db74127621f3f069b17206f5feb9b578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a20000000000000000ce27cd3a6fd08c3e9ca0e56bad6afab9b8a25f8947635ad80000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a772059cb7b2add0e3805598c6155bceb30000000000000000f7d6e5e1b542c9970000000000000000a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c03070517ada4961000000000000000000000000000000000c5a2874b74ace65a6464d325dc996ba5bb6a6bd70a94057d2e2a2fb963d910d9afb05a97588ddd4a63204f9754536c12000000000000000000000000000000000000000000000000deb00f3144b5905df1449c6d30d95b5eca1e3ad9090f47c7aeec0c53d05177cd0000000000000000000000000000000000000000000000000000000000000000841b07d4c44080124cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f17 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b4895c9d3ddc92b6dd1a717784dff54106b78176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a20000000000000000c81b352cc6602587474a7d08b33c63d102420002121000100000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a703bd2096e5ad58b8a885e4162cbb2e45f76fb5a756810e735836453d27d45a650000000000000000a8dcd1a316ed0287fcde1e506cfeed5a31b886bc8666833c46d5b964006d87e600000000000000000000000000000000c675c5c264b0e0c80000000000000000bb6a6bd70a94057d2e2a2fb963d910d9f090065a1036cc11527aafa184968e9b00000000000000000000000000000000bcf9085df41dffe3bedd183939419063f1449c6d30d95b5eca1e3ad9090f47c7abfee65d9d2d6aa000000000000000000000000000000000000000000000000000000000000000008422858748a27fe54cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f18 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b48b96d83e50c2d01e5af835af019a5629578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a242c96c7b55c0808e02167efce4364890a74afb14fb978a3c00000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2712e117a7f891dedc71c0e03f0000000000000000ad7065b4338b38b056cdccb8d73fa5290000000000000000a8dcd1a316ed0287fcde1e506cfeed5aa564e214757842a07a75dc5f64592fb9000000000000000000000000000000007cb1ef5292bebc880000000000000000bb6a6bd70a94057d2e2a2fb963d910d9df18ea01565e42880000000000000000000000000000000000000000000000009151553438a3705fd5b8de3fe3ed8290f1449c6d30d95b5eca1e3ad9090f47c7e8b1c871aa997be20000000000000000000000000000000000000000000000000000000000000000637cb5a7ecf1c45a4cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f19 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed480729cf567f2db6b487db2f1cfeab919f4ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a22c0790d01b0b752ee83ffe948b8e4e67fed414f830297d5800000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730cab74d5b2512e117a7e708263e874b45ca0000000000000000c097c67f6168bfb300000000000000000000000000000000a8dcd1a316ed0287fcde1e506cfeed5a807d115eb20568864130bf45cec745ef0000000000000000e13d8f8d0c7219a0ae27ef17ab7a7f0e0000000000000000bb6a6bd70a94057d2e2a2fb963d910d9f642c72edb7dc9d10000000000000000000000000000000000000000000000003ec11fe1ae1ec75f3078231d49a59f6af1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000000000000000000000222440119950184aad73ae72ea5bd3244cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e +anim_sine_apng_f20 640 480 6833c313e3df098bf91952e619b2e9b2a0c37cfae9f0cb73dc0e7c06024ef704cf3b2799b520366731f68bf7b6b9c555b3f93ad9bb4292d516491874ad52649709b49df11a137d3a2d3cf7db67eed48062be754bf6fb636828cfe0e21e6ccf14ab835ae019a572d578176ae5641fcee21b80061009890390a359466920cac7cd44de31cd4cec725d760eae763d5bf1a209a50d4ea3702d43f5b229692901d7fb9e8afafe55a7a33900000000000000000000000000000000d1b326e7f4fcbf8fd6bd6429ec7730caf7fa83ca2946b38b347c3b02afd420c10000000000000000e2660877e877115d00000000000000000000000000000000a8dcd1a316ed0287fcde1e506cfeed5abd3cdf143c55871f0000000000000000000000000000000050b4be6cba74bc02c5cc2fcc19f7fa0e0000000000000000bb6a6bd70a94057d2e2a2fb963d910d96846d7331f570773000000000000000000000000000000000000000100000010d315cc8b2c3b104f969de3a5e5624b87f1449c6d30d95b5eca1e3ad9090f47c7ecb1c871aa993ac2000000000000000000000000000000000000000000000000a37fa2048b634ab981d3304774fa6ee34cb261193db438d3f1174b880ce875ac5ab6cdc0b4efee4855a4e8f73c02eba7901e2fd8c6bf735106456bb7b1055eb11b149c7ffa838e12dbb0a1a8a5df56cf6fc51c434a25072e diff --git a/tests/test_fingerprint.f90 b/tests/test_fingerprint.f90 index 2ac9ad0..764d666 100644 --- a/tests/test_fingerprint.f90 +++ b/tests/test_fingerprint.f90 @@ -32,11 +32,11 @@ module test_fingerprint use, intrinsic :: iso_fortran_env, only: int64 - use test_image_io, only: read_png_rgb, read_gif_frames + use test_image_io, only: read_png_rgb, read_apng_frames, read_gif_frames implicit none private - public :: fp_check_png, fp_check_gif, fp_finish + public :: fp_check_png, fp_check_apng, fp_check_gif, fp_finish ! FPHash-4096, version 1. Changing any of these invalidates every stored ! fingerprint, which then have to be regenerated with --update. @@ -232,6 +232,19 @@ subroutine fp_check_png(name, path) call record(name, rgb) end subroutine fp_check_png + subroutine fp_check_apng(name, path) + character(len=*), intent(in) :: name, path + integer, allocatable :: frames(:, :, :, :) + character(len=8) :: suffix + integer :: k + + call read_apng_frames(path, frames) + do k = 1, size(frames, 4) + write (suffix, "('_f',i2.2)") k + call record(name//trim(suffix), frames(:, :, :, k)) + end do + end subroutine fp_check_apng + ! Every frame is fingerprinted on its own, as name_f01, name_f02, ... subroutine fp_check_gif(name, path) character(len=*), intent(in) :: name, path diff --git a/tests/test_image_io.f90 b/tests/test_image_io.f90 index 5f1fdde..7075b9e 100644 --- a/tests/test_image_io.f90 +++ b/tests/test_image_io.f90 @@ -1,4 +1,4 @@ -! Reading back what the tests wrote: PNG and GIF, decoded to RGB. +! Reading back what the tests wrote: PNG, APNG and GIF, decoded to RGB. ! ! The fingerprints in test_fingerprint are taken from the files on disk ! rather than from the renderer's memory, so that the PNG and GIF encoders @@ -10,7 +10,7 @@ module test_image_io implicit none private - public :: read_png_rgb, read_gif_frames + public :: read_png_rgb, read_apng_frames, read_gif_frames type :: huff_t integer :: count(0:15) = 0 @@ -231,11 +231,9 @@ end function be32 subroutine read_png_rgb(path, rgb) character(len=*), intent(in) :: path integer, allocatable, intent(out) :: rgb(:, :, :) - integer, allocatable :: b(:), idat(:), raw(:) - integer :: i, p, ln, w, h, depth, ctype, bpp, nidat, fsize - integer :: x, y, c, stride, f, a, bb, cc, v, pa, pb, pc, base, alpha + integer, allocatable :: b(:), idat(:) + integer :: p, ln, w, h, depth, ctype, nidat, fsize character(len=4) :: typ - type(bits_t) :: s call read_bytes(path, b) fsize = size(b) @@ -263,6 +261,18 @@ subroutine read_png_rgb(path, rgb) end if p = p + 12 + ln end do + call decode_image(idat(1:nidat), w, h, depth, ctype, rgb) + end subroutine read_png_rgb + + ! The zlib stream of one image or frame, unfiltered and composited onto + ! white. + subroutine decode_image(z, w, h, depth, ctype, rgb) + integer, intent(in) :: z(:), w, h, depth, ctype + integer, allocatable, intent(out) :: rgb(:, :, :) + integer, allocatable :: raw(:) + integer :: i, bpp, x, y, c, stride, f, a, bb, cc, v, pa, pb, pc, base, alpha + type(bits_t) :: s + if (depth /= 8) error stop "image_io: only 8-bit PNG is read" select case (ctype) case (0); bpp = 1 @@ -273,8 +283,8 @@ subroutine read_png_rgb(path, rgb) end select stride = w*bpp - allocate (s%inp(nidat - 2)) - s%inp = idat(3:nidat) ! past the two byte zlib header + allocate (s%inp(size(z) - 2)) + s%inp = z(3:) ! past the two byte zlib header allocate (raw(h*(stride + 1))) call inflate(s, raw) @@ -326,7 +336,70 @@ subroutine read_png_rgb(path, rgb) end do end do end do - end subroutine read_png_rgb + end subroutine decode_image + + ! ------------------------------------------------------------------ + ! APNG: rgb(3, w, h, nframes). Frames are whole and opaque, which is all + ! apng_encode writes, so no dispose or blend is applied. + ! ------------------------------------------------------------------ + + subroutine read_apng_frames(path, rgb) + character(len=*), intent(in) :: path + integer, allocatable, intent(out) :: rgb(:, :, :, :) + integer, allocatable :: b(:), z(:), one(:, :, :) + integer :: p, ln, w, h, depth, ctype, nz, nf, nframes, expect + character(len=4) :: typ + + call read_bytes(path, b) + allocate (z(size(b))) + nz = 0 + nf = 0 + nframes = 0 + expect = 0 + w = 0 + h = 0 + depth = 0 + ctype = -1 + p = 9 + do while (p + 8 <= size(b)) + ln = be32(b, p) + typ = achar(b(p + 4))//achar(b(p + 5))//achar(b(p + 6))//achar(b(p + 7)) + select case (typ) + case ("IHDR") + w = be32(b, p + 8) + h = be32(b, p + 12) + depth = b(p + 16) + ctype = b(p + 17) + case ("acTL") + nframes = be32(b, p + 8) + allocate (rgb(3, w, h, nframes)) + case ("fcTL") + if (nz > 0) call flush_frame() + if (be32(b, p + 8) /= expect) error stop "image_io: APNG sequence" + expect = expect + 1 + case ("IDAT") + z(nz + 1:nz + ln) = b(p + 8:p + 7 + ln) + nz = nz + ln + case ("fdAT") + if (be32(b, p + 8) /= expect) error stop "image_io: APNG sequence" + expect = expect + 1 + z(nz + 1:nz + ln - 4) = b(p + 12:p + 7 + ln) + nz = nz + ln - 4 + case ("IEND") + exit + end select + p = p + 12 + ln + end do + if (nz > 0) call flush_frame() + if (nf /= nframes) error stop "image_io: APNG frame count" + contains + subroutine flush_frame() + call decode_image(z(1:nz), w, h, depth, ctype, one) + nf = nf + 1 + rgb(:, :, :, nf) = one + nz = 0 + end subroutine flush_frame + end subroutine read_apng_frames ! ------------------------------------------------------------------ ! GIF: rgb(3, w, h, nframes). Each frame starts from the previous one, diff --git a/tests/test_plots.f90 b/tests/test_plots.f90 index 2139553..9abba83 100644 --- a/tests/test_plots.f90 +++ b/tests/test_plots.f90 @@ -1,6 +1,6 @@ program test_plots use fplotlib - use test_fingerprint, only: fp_check_png, fp_check_gif, fp_finish + use test_fingerprint, only: fp_check_png, fp_check_apng, fp_check_gif, fp_finish implicit none integer :: fig1 type(axes), allocatable :: axs(:, :) @@ -1546,6 +1546,18 @@ program test_plots call fp_check_gif("anim_sine", "tests/out/anim_sine.gif") print *, "wrote tests/out/anim_sine.gif" +! 75) the same animation as an APNG, with full colour + do i = 1, 20 + call clf() + call plot(x, sin(x + real(i - 1, dp)*0.3_dp), "b-") + call ylim(-1.5_dp, 1.5_dp) + call title("anim_sine") + call add_frame() + end do + call save_animation("tests/out/anim_sine.png", fps=10.0_dp) + call fp_check_apng("anim_sine_apng", "tests/out/anim_sine.png") + print *, "wrote tests/out/anim_sine.png" + print *, "All test plots written." ! Every PNG and GIF frame against its stored fingerprint; see From 132a6eeb6cfde659c659d922fa69974bdec4509b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Alves?= <102541118+jalvesz@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:05:40 +0100 Subject: [PATCH 2/2] Update src/fplotlib_png.f90 test do concurrent --- src/fplotlib_png.f90 | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/fplotlib_png.f90 b/src/fplotlib_png.f90 index f81dd5d..95ca273 100644 --- a/src/fplotlib_png.f90 +++ b/src/fplotlib_png.f90 @@ -354,14 +354,9 @@ function apng_encode(w, h, nframes, pix, delay_num, delay_den, loop) result(byte seq = 0 do f = 1, nframes base = (f - 1)*3*w*h - k = base - do y = 1, h - do x = 1, w - do c = 1, 3 - k = k + 1 - rgb(c, x, y) = iachar(pix(k:k)) - end do - end do + do concurrent (y = 1:h, x = 1:w, c = 1:3) + k = base + (y - 1)*3*w + (x - 1)*3 + c + rgb(c, x, y) = iachar(pix(k:k)) end do allocate (d(26))