module fortplot_png use iso_c_binding use fortplot_context, only: setup_canvas use fortplot_raster, only: raster_context, create_raster_canvas, raster_draw_axes_and_labels, raster_render_ylabel use fortplot_zlib, only: zlib_compress, crc32_calculate use fortplot_logging, only: log_error, log_info use, intrinsic :: iso_fortran_env, only: wp => real64, int8, int32 implicit none private public :: png_context, create_png_canvas, raster_draw_axes_and_labels, write_png_file, get_png_data ! PNG plotting context - extends raster context and adds PNG file I/O type, extends(raster_context) :: png_context contains procedure :: save => png_finalize procedure :: get_png_data_backend => png_get_png_data end type png_context contains function create_png_canvas(width, height) result(ctx) integer, intent(in) :: width, height type(png_context) :: ctx type(raster_context) :: raster_base ! Create raster canvas and copy components to PNG context raster_base = create_raster_canvas(width, height) ! Initialize PNG context with same data as raster context call setup_canvas(ctx, width, height) ctx%raster = raster_base%raster ctx%margins = raster_base%margins ctx%plot_area = raster_base%plot_area end function create_png_canvas ! All drawing methods are inherited from raster_context subroutine png_finalize(this, filename) class(png_context), intent(inout) :: this character(len=*), intent(in) :: filename call write_png_file(filename, this%width, this%height, this%raster%image_data) end subroutine png_finalize subroutine png_get_png_data(this, width, height, png_data, status) !! Get PNG data from PNG context's raster data class(png_context), intent(in) :: this integer, intent(in) :: width, height integer(1), allocatable, intent(out) :: png_data(:) integer, intent(out) :: status call generate_png_data(width, height, this%raster%image_data, png_data) status = 0 end subroutine png_get_png_data ! Generate PNG data from image data subroutine generate_png_data(width, height, image_data, png_buffer) integer, intent(in) :: width, height integer(1), intent(in) :: image_data(:) integer(1), allocatable, intent(out) :: png_buffer(:) integer(1) :: png_signature(8) = & [int(-119,1), int(80,1), int(78,1), int(71,1), int(13,1), int(10,1), int(26,1), int(10,1)] integer, parameter :: bit_depth = 8, color_type = 2 integer(int8), allocatable :: compressed_data(:) integer :: compressed_size, data_size integer(1), allocatable :: png_row_data(:) ! Convert RGB image data to PNG row format with filter bytes call convert_rgb_to_png_rows(width, height, image_data, png_row_data) data_size = size(png_row_data) compressed_data = zlib_compress(png_row_data, data_size, compressed_size) if (.not. allocated(compressed_data) .or. compressed_size <= 0) then call log_error("PNG compression failed") return end if call build_png_buffer(width, height, compressed_data, compressed_size, png_buffer) if (allocated(compressed_data)) deallocate(compressed_data) if (allocated(png_row_data)) deallocate(png_row_data) end subroutine generate_png_data ! Build complete PNG buffer from compressed data subroutine build_png_buffer(width, height, compressed_data, compressed_size, png_buffer) integer, intent(in) :: width, height, compressed_size integer(int8), intent(in) :: compressed_data(:) integer(1), allocatable, intent(out) :: png_buffer(:) integer(1) :: png_signature(8) = & [int(-119,1), int(80,1), int(78,1), int(71,1), int(13,1), int(10,1), int(26,1), int(10,1)] integer, parameter :: bit_depth = 8, color_type = 2 integer :: total_size, pos integer(1) :: ihdr_data(13) integer :: w_be, h_be ! Calculate total PNG size: signature + IHDR + IDAT + IEND total_size = 8 + (4+4+13+4) + (4+4+compressed_size+4) + (4+4+0+4) allocate(png_buffer(total_size)) pos = 1 ! Write PNG signature png_buffer(pos:pos+7) = png_signature pos = pos + 8 ! Build IHDR data w_be = to_big_endian(width) h_be = to_big_endian(height) ihdr_data(1:4) = transfer(w_be, ihdr_data(1:4)) ihdr_data(5:8) = transfer(h_be, ihdr_data(5:8)) ihdr_data(9) = int(bit_depth, 1) ihdr_data(10) = int(color_type, 1) ihdr_data(11:13) = 0_1 ! Write IHDR chunk to buffer call write_chunk_to_buffer(png_buffer, pos, "IHDR", ihdr_data, 13) ! Write IDAT chunk to buffer call write_chunk_to_buffer(png_buffer, pos, "IDAT", compressed_data, compressed_size) ! Write IEND chunk to buffer call write_chunk_to_buffer(png_buffer, pos, "IEND", [integer(1)::], 0) end subroutine build_png_buffer ! Write PNG data to file with error handling subroutine write_png_file(filename, width, height, image_data) character(len=*), intent(in) :: filename integer, intent(in) :: width, height integer(1), intent(in) :: image_data(:) integer(1), allocatable :: png_buffer(:) integer :: png_unit, ios character(len=512) :: error_msg call generate_png_data(width, height, image_data, png_buffer) if (.not. allocated(png_buffer)) then call log_error("Failed to generate PNG data for '" // trim(filename) // "'") return end if open(newunit=png_unit, file=filename, access='stream', form='unformatted', & status='replace', iostat=ios, iomsg=error_msg) if (ios /= 0) then call log_error("Cannot save PNG file '" // trim(filename) // "': " // trim(error_msg)) if (allocated(png_buffer)) deallocate(png_buffer) return end if write(png_unit, iostat=ios) png_buffer if (ios /= 0) then call log_error("Failed to write PNG data to '" // trim(filename) // "'") close(png_unit, status='delete') ! Remove incomplete file if (allocated(png_buffer)) deallocate(png_buffer) return end if close(png_unit) if (allocated(png_buffer)) deallocate(png_buffer) call log_info("PNG file '" // trim(filename) // "' created successfully!") end subroutine write_png_file ! Public wrapper for getting PNG data subroutine get_png_data(width, height, image_data, png_buffer) integer, intent(in) :: width, height integer(1), intent(in) :: image_data(:) integer(1), allocatable, intent(out) :: png_buffer(:) call generate_png_data(width, height, image_data, png_buffer) end subroutine get_png_data ! Convert RGB image data to PNG row format with filter bytes subroutine convert_rgb_to_png_rows(width, height, rgb_data, png_row_data) integer, intent(in) :: width, height integer(1), intent(in) :: rgb_data(:) ! width * height * 3 RGB bytes integer(1), allocatable, intent(out) :: png_row_data(:) ! height * (1 + width * 3) bytes integer :: row, col, rgb_idx, png_idx, row_start ! Allocate PNG row data: height rows * (1 filter byte + width * 3 RGB bytes) allocate(png_row_data(height * (1 + width * 3))) ! Convert row by row do row = 1, height ! Calculate indices row_start = (row - 1) * (1 + width * 3) + 1 ! Set filter byte to 0 (no filter) png_row_data(row_start) = 0_1 ! Copy RGB data for this row do col = 1, width rgb_idx = (row - 1) * width * 3 + (col - 1) * 3 + 1 png_idx = row_start + 1 + (col - 1) * 3 png_row_data(png_idx) = rgb_data(rgb_idx) ! R png_row_data(png_idx + 1) = rgb_data(rgb_idx + 1) ! G png_row_data(png_idx + 2) = rgb_data(rgb_idx + 2) ! B end do end do end subroutine convert_rgb_to_png_rows ! Write PNG chunk to buffer subroutine write_chunk_to_buffer(buffer, pos, chunk_type, data, data_len) integer(1), intent(inout) :: buffer(:) integer, intent(inout) :: pos character(len=4), intent(in) :: chunk_type integer(1), intent(in) :: data(:) integer, intent(in) :: data_len integer :: length_be, crc_val, crc_be integer(1) :: type_bytes(4) ! Convert chunk type to bytes type_bytes = transfer(chunk_type, type_bytes) ! Write length (big endian) length_be = to_big_endian(data_len) buffer(pos:pos+3) = transfer(length_be, buffer(pos:pos+3)) pos = pos + 4 ! Write chunk type buffer(pos:pos+3) = type_bytes pos = pos + 4 ! Write data if (data_len > 0) then buffer(pos:pos+data_len-1) = data(1:data_len) pos = pos + data_len end if ! Calculate and write CRC crc_val = calculate_chunk_crc(type_bytes, data, data_len) crc_be = to_big_endian(crc_val) buffer(pos:pos+3) = transfer(crc_be, buffer(pos:pos+3)) pos = pos + 4 end subroutine write_chunk_to_buffer ! Calculate CRC for PNG chunk function calculate_chunk_crc(type_bytes, data, data_len) result(crc) integer(1), intent(in) :: type_bytes(4), data(:) integer, intent(in) :: data_len integer :: crc integer(1), allocatable :: combined(:) allocate(combined(4 + data_len)) combined(1:4) = type_bytes if (data_len > 0) then combined(5:4+data_len) = data(1:data_len) end if crc = int(crc32_calculate(combined, size(combined))) if (allocated(combined)) deallocate(combined) end function calculate_chunk_crc ! PNG chunk writing functions (simplified versions) subroutine write_ihdr_chunk(unit, w, h, bd, ct) integer, intent(in) :: unit, w, h, bd, ct integer(1) :: ihdr_data(13) integer :: w_be, h_be w_be = to_big_endian(w) h_be = to_big_endian(h) ihdr_data(1:4) = transfer(w_be, ihdr_data(1:4)) ihdr_data(5:8) = transfer(h_be, ihdr_data(5:8)) ihdr_data(9) = int(bd, 1) ihdr_data(10) = int(ct, 1) ihdr_data(11) = 0_1 ihdr_data(12) = 0_1 ihdr_data(13) = 0_1 call write_chunk(unit, "IHDR", ihdr_data, 13) end subroutine write_ihdr_chunk subroutine write_idat_chunk(unit, data, size) integer, intent(in) :: unit, size integer(1), intent(in) :: data(size) call write_chunk(unit, "IDAT", data, size) end subroutine write_idat_chunk subroutine write_iend_chunk(unit) integer, intent(in) :: unit integer(1) :: dummy(1) call write_chunk(unit, "IEND", dummy, 0) end subroutine write_iend_chunk subroutine write_chunk(unit, chunk_type, chunk_data, data_size) integer, intent(in) :: unit character(len=4), intent(in) :: chunk_type integer(1), intent(in) :: chunk_data(*) integer, intent(in) :: data_size integer :: length_be integer(1) :: type_bytes(4) integer(1), allocatable, target :: full_data(:) integer(c_int32_t) :: crc_value integer :: crc_be integer :: i length_be = to_big_endian(data_size) do i = 1, 4 type_bytes(i) = int(iachar(chunk_type(i:i)), 1) end do allocate(full_data(4 + data_size)) full_data(1:4) = type_bytes if (data_size > 0) then full_data(5:4+data_size) = chunk_data(1:data_size) end if crc_value = calculate_crc32(full_data, 4 + data_size) crc_be = to_big_endian(int(crc_value)) write(unit) length_be write(unit) type_bytes if (data_size > 0) then write(unit) chunk_data(1:data_size) end if write(unit) crc_be if (allocated(full_data)) deallocate(full_data) end subroutine write_chunk function to_big_endian(value) result(be_value) integer, intent(in) :: value integer :: be_value integer(1) :: bytes(4) bytes(1) = int(ibits(value, 24, 8), 1) bytes(2) = int(ibits(value, 16, 8), 1) bytes(3) = int(ibits(value, 8, 8), 1) bytes(4) = int(ibits(value, 0, 8), 1) be_value = transfer(bytes, be_value) end function to_big_endian function calculate_crc32(data, len) result(crc) integer(1), intent(in) :: data(*) integer, intent(in) :: len integer(int32) :: crc crc = crc32_calculate(data, len) end function calculate_crc32 end module fortplot_png