diff --git a/src/animation/fortplot_animation_constants.f90 b/src/animation/fortplot_animation_constants.f90 deleted file mode 100644 index 0c298d9c..00000000 --- a/src/animation/fortplot_animation_constants.f90 +++ /dev/null @@ -1,16 +0,0 @@ -module fortplot_animation_constants - implicit none - private - - ! Animation configuration constants - integer, parameter, public :: DEFAULT_FRAME_INTERVAL_MS = 50 - integer, parameter, public :: DEFAULT_ANIMATION_FPS = 10 - integer, parameter, public :: MIN_VALID_VIDEO_SIZE = 100 - integer, parameter, public :: MIN_EXPECTED_VIDEO_SIZE = 1000 - integer, parameter, public :: MAX_FILENAME_LENGTH = 255 - - ! Enhanced recovery constants for exponential backoff - integer, parameter, public :: MAX_RETRY_ATTEMPTS = 3 - integer, parameter, public :: BASE_RETRY_DELAY_MS = 100 - -end module fortplot_animation_constants \ No newline at end of file diff --git a/src/animation/fortplot_animation_core.f90 b/src/animation/fortplot_animation_core.f90 index 62f8ec5c..a0ce5088 100644 --- a/src/animation/fortplot_animation_core.f90 +++ b/src/animation/fortplot_animation_core.f90 @@ -1,7 +1,6 @@ module fortplot_animation_core use iso_fortran_env, only: real64, wp => real64 use iso_c_binding, only: c_char, c_int, c_null_char - use fortplot_animation_constants use fortplot_constants, only: MILLISECONDS_PER_SECOND use fortplot_figure_core, only: figure_t, plot_data_t ! savefig is part of figure_t, not rendering module @@ -11,6 +10,17 @@ module fortplot_animation_core implicit none private + ! Animation configuration constants (consolidated from fortplot_animation_constants) + integer, parameter, public :: DEFAULT_FRAME_INTERVAL_MS = 50 + integer, parameter, public :: DEFAULT_ANIMATION_FPS = 10 + integer, parameter, public :: MIN_VALID_VIDEO_SIZE = 100 + integer, parameter, public :: MIN_EXPECTED_VIDEO_SIZE = 1000 + integer, parameter, public :: MAX_FILENAME_LENGTH = 255 + + ! Enhanced recovery constants for exponential backoff + integer, parameter, public :: MAX_RETRY_ATTEMPTS = 3 + integer, parameter, public :: BASE_RETRY_DELAY_MS = 100 + ! Animation callback interface abstract interface subroutine animate_interface(frame) diff --git a/src/animation/fortplot_animation_pipeline.f90 b/src/animation/fortplot_animation_pipeline.f90 index 8ed293ae..bb9c1d86 100644 --- a/src/animation/fortplot_animation_pipeline.f90 +++ b/src/animation/fortplot_animation_pipeline.f90 @@ -1,8 +1,9 @@ module fortplot_animation_pipeline use iso_fortran_env, only: real64, wp => real64 - use fortplot_animation_constants + use fortplot_animation_core, only: animation_t, DEFAULT_FRAME_INTERVAL_MS, DEFAULT_ANIMATION_FPS, & + MIN_VALID_VIDEO_SIZE, MIN_EXPECTED_VIDEO_SIZE, MAX_FILENAME_LENGTH, & + MAX_RETRY_ATTEMPTS, BASE_RETRY_DELAY_MS use fortplot_constants, only: MILLISECONDS_PER_SECOND - use fortplot_animation_core, only: animation_t use fortplot_animation_rendering, only: render_frame_to_png use fortplot_animation_validation, only: validate_generated_video_enhanced use fortplot_pipe, only: open_ffmpeg_pipe, write_png_to_pipe, close_ffmpeg_pipe diff --git a/src/animation/fortplot_animation_validation.f90 b/src/animation/fortplot_animation_validation.f90 index f32876c9..8b239e12 100644 --- a/src/animation/fortplot_animation_validation.f90 +++ b/src/animation/fortplot_animation_validation.f90 @@ -1,6 +1,7 @@ module fortplot_animation_validation use iso_fortran_env, only: real64, wp => real64 - use fortplot_animation_constants + use fortplot_animation_core, only: MIN_VALID_VIDEO_SIZE, MIN_EXPECTED_VIDEO_SIZE, & + MAX_FILENAME_LENGTH, MAX_RETRY_ATTEMPTS, BASE_RETRY_DELAY_MS use fortplot_logging, only: log_error, log_info, log_warning implicit none private diff --git a/src/backends/raster/fortplot_png.f90 b/src/backends/raster/fortplot_png.f90 index d2f5864e..7c4a6298 100644 --- a/src/backends/raster/fortplot_png.f90 +++ b/src/backends/raster/fortplot_png.f90 @@ -2,7 +2,7 @@ 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_zlib_core, 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 @@ -107,7 +107,7 @@ subroutine build_png_buffer(width, height, compressed_data, compressed_size, png png_buffer(pos:pos+7) = png_signature pos = pos + 8 - ! Build IHDR data + ! Build IHDR data using the exact working approach w_be = to_big_endian(width) h_be = to_big_endian(height) ihdr_data(1:4) = transfer(w_be, ihdr_data(1:4)) @@ -214,15 +214,19 @@ subroutine write_chunk_to_buffer(buffer, pos, chunk_type, data, data_len) integer(1), intent(in) :: data(:) integer, intent(in) :: data_len - integer :: length_be, crc_val, crc_be + integer :: length_be, crc_val, crc_be, i integer(1) :: type_bytes(4) - ! Convert chunk type to bytes - type_bytes = transfer(chunk_type, type_bytes) + ! Convert chunk type to bytes (using correct ASCII conversion) + do i = 1, 4 + type_bytes(i) = int(iachar(chunk_type(i:i)), 1) + end do - ! Write length (big endian) - length_be = to_big_endian(data_len) - buffer(pos:pos+3) = transfer(length_be, buffer(pos:pos+3)) + ! Write length (big endian) - write bytes directly + buffer(pos) = int(ibits(data_len, 24, 8), 1) + buffer(pos+1) = int(ibits(data_len, 16, 8), 1) + buffer(pos+2) = int(ibits(data_len, 8, 8), 1) + buffer(pos+3) = int(ibits(data_len, 0, 8), 1) pos = pos + 4 ! Write chunk type @@ -235,10 +239,12 @@ subroutine write_chunk_to_buffer(buffer, pos, chunk_type, data, data_len) pos = pos + data_len end if - ! Calculate and write CRC + ! Calculate and write CRC (write bytes directly in big-endian order) 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)) + buffer(pos) = int(ibits(crc_val, 24, 8), 1) + buffer(pos+1) = int(ibits(crc_val, 16, 8), 1) + buffer(pos+2) = int(ibits(crc_val, 8, 8), 1) + buffer(pos+3) = int(ibits(crc_val, 0, 8), 1) pos = pos + 4 end subroutine write_chunk_to_buffer @@ -330,6 +336,8 @@ subroutine write_chunk(unit, chunk_type, chunk_data, data_size) 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 @@ -343,7 +351,6 @@ function to_big_endian(value) result(be_value) 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 diff --git a/src/backends/raster/fortplot_raster_rendering.f90 b/src/backends/raster/fortplot_raster_rendering.f90 index 9814304b..ac2fc81f 100644 --- a/src/backends/raster/fortplot_raster_rendering.f90 +++ b/src/backends/raster/fortplot_raster_rendering.f90 @@ -6,7 +6,7 @@ module fortplot_raster_rendering use fortplot_margins, only: plot_area_t use fortplot_colormap, only: colormap_value_to_color use fortplot_interpolation, only: interpolate_z_bilinear - use fortplot_raster_drawing, only: color_to_byte, draw_filled_quad_raster + use fortplot_raster_primitives, only: color_to_byte, draw_filled_quad_raster use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none diff --git a/src/external/fortplot_zlib.f90 b/src/external/fortplot_zlib.f90 index 8dcf968c..492b683f 100644 --- a/src/external/fortplot_zlib.f90 +++ b/src/external/fortplot_zlib.f90 @@ -1,10 +1,10 @@ -module fortplot_zlib - !! Pure Fortran implementation of zlib compression, decompression, and CRC32 - !! Refactored for file size compliance (Issue #884) - delegates to specialized modules +module fortplot_zlib_bridge + !! Bridge module to re-export from the main fortplot_zlib_core + !! This maintains compatibility while using the working full implementation use fortplot_zlib_core, only: zlib_compress, crc32_calculate implicit none - private + private public :: zlib_compress, crc32_calculate -end module fortplot_zlib \ No newline at end of file +end module fortplot_zlib_bridge \ No newline at end of file diff --git a/src/external/fortplot_zlib_core.f90 b/src/external/fortplot_zlib_core.f90 index 09467c41..354b453f 100644 --- a/src/external/fortplot_zlib_core.f90 +++ b/src/external/fortplot_zlib_core.f90 @@ -1,12 +1,22 @@ module fortplot_zlib_core - !! Core zlib functionality: CRC32 calculation and main compression entry points - !! Split from fortplot_zlib.f90 for file size compliance (Issue #884) + !! Pure Fortran implementation of zlib compression, decompression, and CRC32 + !! Ported from STB image libraries for self-contained PNG support use, intrinsic :: iso_fortran_env, only: int8, int32 - use fortplot_zlib_huffman, only: analyze_compressibility, compress_with_fixed_huffman + use iso_c_binding, only: c_ptr, c_loc, c_f_pointer, c_associated implicit none private - public :: zlib_compress, crc32_calculate, calculate_adler32 + public :: zlib_compress, crc32_calculate + + private :: bit_reverse + + ! Deflate compression constants + integer, parameter :: MAX_MATCH = 258 + integer, parameter :: MIN_MATCH = 3 + integer, parameter :: MAX_DISTANCE = 32768 + integer, parameter :: HASH_BITS = 15 + integer, parameter :: HASH_SIZE = 2**HASH_BITS + integer, parameter :: WINDOW_SIZE = 32768 ! CRC32 lookup table (standard polynomial 0xEDB88320) integer(int32), parameter :: crc_table(0:255) = [ & @@ -89,335 +99,483 @@ function crc32_calculate(data, data_len) result(crc) do i = 1, data_len byte_val = data(i) - crc = ieor(ishft(crc, 8), crc_table(iand(ieor(crc, int(byte_val, int32)), z'FF'))) + crc = ieor(ishft(crc, -8), crc_table(iand(ieor(crc, int(byte_val, int32)), 255))) end do - crc = not(crc) + crc = not(crc) ! Final XOR with 0xFFFFFFFF end function crc32_calculate function zlib_compress(input_data, input_len, output_len) result(output_data) - !! Main zlib compression entry point with proper headers and checksums + !! Full deflate compression with LZ77 and Huffman coding integer(int8), intent(in) :: input_data(*) integer, intent(in) :: input_len integer, intent(out) :: output_len integer(int8), allocatable :: output_data(:) - integer(int8), allocatable :: compressed_data(:) - integer :: compressed_len, pos + + integer(int8), allocatable :: compressed_block(:) + integer :: compressed_block_len integer(int32) :: adler32_checksum - integer :: complexity_score, sample_region_size, padding_size, i, j - integer :: data_fingerprint, byte_frequencies(0:255), unique_bytes, middle_variance - integer :: line_count_estimate, non_zero_regions, byte_value + integer :: pos + + ! Compress using deflate algorithm + call deflate_compress(input_data, input_len, compressed_block, compressed_block_len) - ! Simplified reliable implementation: allocate conservative buffer sizes - allocate(output_data(input_len * 2 + 1000)) - allocate(compressed_data(input_len * 2 + 1000)) + ! Calculate total output size: zlib header (2) + compressed data + adler32 (4) + output_len = 2 + compressed_block_len + 4 + allocate(output_data(output_len)) pos = 1 - ! Write standard ZLIB header (RFC 1950) - ! CMF = 0x78: CM=8 (deflate), CINFO=7 (32K window size) - ! FLG = 0x9C: FCHECK=28, FDICT=0, FLEVEL=2 - output_data(pos) = int(z'78', int8) - output_data(pos+1) = int(z'9C', int8) - pos = pos + 2 - - ! CRITICAL FIX: Use reliable uncompressed blocks for FFmpeg compatibility - ! Fixed Huffman has complex bit ordering issues - use proven uncompressed method - compressed_len = 1 - call compress_with_uncompressed_blocks_efficient(input_data, input_len, compressed_data, compressed_len) - - ! Bounds check before copying compressed data - if (compressed_len > size(compressed_data)) then - print *, "ERROR: Compressed data size exceeds buffer:", compressed_len, "vs", size(compressed_data) - output_len = 0 - deallocate(compressed_data) - return - end if + ! Write zlib header + output_data(pos) = int(z'78', int8) ! CMF: 32K window, deflate + pos = pos + 1 + output_data(pos) = int(z'5E', int8) ! FLG: no preset dict, level 1 compression + pos = pos + 1 - if (pos + compressed_len - 1 > size(output_data)) then - print *, "ERROR: Output buffer overflow:", pos + compressed_len - 1, "vs", size(output_data) - output_len = 0 - deallocate(compressed_data) - return - end if - - ! Copy compressed data - output_data(pos:pos+compressed_len-1) = compressed_data(1:compressed_len) - pos = pos + compressed_len + ! Copy compressed block + output_data(pos:pos+compressed_block_len-1) = compressed_block(1:compressed_block_len) + pos = pos + compressed_block_len - ! Calculate and append Adler-32 checksum (4 bytes, big-endian) + ! Calculate and write Adler-32 checksum adler32_checksum = calculate_adler32(input_data, input_len) - output_data(pos) = int(iand(ishft(adler32_checksum, -24), z'FF'), int8) - output_data(pos+1) = int(iand(ishft(adler32_checksum, -16), z'FF'), int8) - output_data(pos+2) = int(iand(ishft(adler32_checksum, -8), z'FF'), int8) - output_data(pos+3) = int(iand(adler32_checksum, z'FF'), int8) - output_len = pos + 3 + ! Write Adler-32 in big-endian format + output_data(pos) = int(iand(ishft(adler32_checksum, -24), 255), int8) + pos = pos + 1 + output_data(pos) = int(iand(ishft(adler32_checksum, -16), 255), int8) + pos = pos + 1 + output_data(pos) = int(iand(ishft(adler32_checksum, -8), 255), int8) + pos = pos + 1 + output_data(pos) = int(iand(adler32_checksum, 255), int8) - deallocate(compressed_data) + deallocate(compressed_block) end function zlib_compress - + function calculate_adler32(data, data_len) result(adler32) - !! Calculate Adler-32 checksum for zlib format - !! Fixed for proper unsigned byte handling and modulo arithmetic + !! Calculate Adler-32 checksum for zlib integer(int8), intent(in) :: data(*) integer, intent(in) :: data_len integer(int32) :: adler32 - integer(int32) :: a, b - integer :: i, byte_val - a = 1_int32 - b = 0_int32 + integer(int32) :: s1, s2 + integer :: i + + s1 = 1_int32 + s2 = 0_int32 do i = 1, data_len - ! CRITICAL FIX: Properly convert signed int8 to unsigned 0-255 range - byte_val = int(data(i), int32) - if (byte_val < 0) byte_val = byte_val + 256 - - a = mod(a + byte_val, 65521_int32) - b = mod(b + a, 65521_int32) + ! Handle signed bytes properly + s1 = mod(s1 + iand(int(data(i), int32), 255_int32), 65521_int32) + s2 = mod(s2 + s1, 65521_int32) end do - ! Combine as big-endian 32-bit value - adler32 = ior(ishft(b, 16), a) + adler32 = ior(ishft(s2, 16), s1) end function calculate_adler32 - - subroutine compress_with_uncompressed_blocks(input_data, input_len, output_buffer, output_pos) - !! Create valid uncompressed deflate blocks - simplified reliable implementation + + subroutine deflate_compress(input_data, input_len, output_data, output_len) + !! Full deflate compression implementation with LZ77 and Huffman coding integer(int8), intent(in) :: input_data(*) integer, intent(in) :: input_len - integer(int8), intent(inout) :: output_buffer(:) - integer, intent(inout) :: output_pos + integer(int8), allocatable, intent(out) :: output_data(:) + integer, intent(out) :: output_len - integer :: pos, block_size, remaining - integer :: len_field, nlen_field - integer :: byte_pos + ! Hash table for LZ77 + integer :: hash_table(0:HASH_SIZE-1) + integer :: hash_chain(WINDOW_SIZE) - pos = 1 - byte_pos = output_pos - remaining = input_len + ! Huffman tables (fixed Huffman for simplicity) + integer :: literal_codes(0:285) + integer :: literal_lengths(0:285) + integer :: distance_codes(0:29) + integer :: distance_lengths(0:29) - do while (remaining > 0) - ! Use maximum block size to minimize complexity (65535 is max for uncompressed) - block_size = min(remaining, 65535) - - ! Write block header (BFINAL + BTYPE) - if (remaining <= block_size) then - ! Final block (BFINAL=1, BTYPE=00) - output_buffer(byte_pos) = 1_int8 - else - ! Non-final block (BFINAL=0, BTYPE=00) - output_buffer(byte_pos) = 0_int8 - end if - byte_pos = byte_pos + 1 - - ! Write LEN (2 bytes, little endian) - len_field = block_size - output_buffer(byte_pos) = int(iand(len_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(len_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 - - ! Write NLEN (one's complement of LEN, 2 bytes, little endian) - ! CRITICAL FIX: Proper NLEN calculation for deflate format - nlen_field = iand(ieor(len_field, z'FFFF'), z'FFFF') - output_buffer(byte_pos) = int(iand(nlen_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(nlen_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 - - ! Copy raw data - output_buffer(byte_pos:byte_pos + block_size - 1) = input_data(pos:pos + block_size - 1) - byte_pos = byte_pos + block_size - pos = pos + block_size - remaining = remaining - block_size - end do + ! Output bit buffer + integer(int8), allocatable :: bit_buffer(:) + integer :: bit_pos, byte_pos + integer :: i, pos, match_len, match_dist + integer :: hash_val - output_pos = byte_pos - end subroutine compress_with_uncompressed_blocks - - subroutine compress_with_uncompressed_blocks_improved(input_data, input_len, output_buffer, output_pos) - !! Create variable-size uncompressed deflate blocks with FFmpeg compatibility - !! This produces PNG files with sizes proportional to actual content length - integer(int8), intent(in) :: input_data(*) - integer, intent(in) :: input_len - integer(int8), intent(inout) :: output_buffer(:) - integer, intent(inout) :: output_pos + ! Initialize fixed Huffman tables + call init_fixed_huffman_tables(literal_codes, literal_lengths, distance_codes, distance_lengths) - integer :: pos, block_size, remaining - integer :: len_field, nlen_field - integer :: byte_pos + ! Initialize hash table + hash_table = -1 + hash_chain = -1 + + ! Allocate bit buffer (worst case: no compression, plus some overhead) + allocate(bit_buffer(max(64, input_len * 2))) + bit_pos = 0 + byte_pos = 1 + + ! Write deflate block header: BFINAL=1 (last block), BTYPE=01 (fixed Huffman) + call write_bits(bit_buffer, bit_pos, byte_pos, 1, 1) + call write_bits(bit_buffer, bit_pos, byte_pos, 1, 2) ! BTYPE=01 pos = 1 - byte_pos = output_pos - remaining = input_len - - do while (remaining > 0) - ! Use smaller block sizes to create larger file sizes through deflate overhead - ! More blocks = more deflate headers = larger compressed output - if (remaining < 4096) then - ! Small remaining data: use all of it - block_size = remaining - else if (remaining < 32768) then - ! Medium data: use small blocks to create size overhead - block_size = min(remaining, 8192) - else - ! Large data: use medium blocks with more overhead - block_size = min(remaining, 16384) - end if + do while (pos <= input_len) + ! Find longest match + call find_longest_match(input_data, pos, input_len, hash_table, hash_chain, match_len, match_dist) - ! Write block header (3 bits: BFINAL + BTYPE) - ! For uncompressed blocks, we need to align to byte boundary after 3-bit header - if (remaining <= block_size) then - ! BFINAL=1, BTYPE=00 (uncompressed, final block): bits 00000001 - output_buffer(byte_pos) = 1_int8 + if (match_len >= MIN_MATCH) then + ! Encode length-distance pair + call encode_length_distance(bit_buffer, bit_pos, byte_pos, match_len, match_dist, & + literal_codes, literal_lengths, distance_codes, distance_lengths) + + ! Update hash table for the matched string + do i = 0, match_len - 1 + if (pos + i <= input_len) then + hash_val = calculate_hash(input_data, pos + i, input_len) + call update_hash_table(hash_table, hash_chain, hash_val, pos + i) + end if + end do + pos = pos + match_len else - ! BFINAL=0, BTYPE=00 (uncompressed, not final): bits 00000000 - output_buffer(byte_pos) = 0_int8 + ! Encode literal + call encode_literal(bit_buffer, bit_pos, byte_pos, input_data(pos), literal_codes, literal_lengths) + + ! Update hash table + if (pos <= input_len) then + hash_val = calculate_hash(input_data, pos, input_len) + call update_hash_table(hash_table, hash_chain, hash_val, pos) + end if + pos = pos + 1 end if + end do + + ! Encode end-of-block marker (code 256) + call write_bits(bit_buffer, bit_pos, byte_pos, bit_reverse(literal_codes(256), literal_lengths(256)), literal_lengths(256)) + + ! Align to byte boundary + if (bit_pos > 0) then byte_pos = byte_pos + 1 - - ! For uncompressed blocks, skip any remaining bits in current byte to align to byte boundary - ! (This is required by RFC 1951 for uncompressed block format) - - ! Write LEN (2 bytes, little endian) - len_field = block_size - output_buffer(byte_pos) = int(iand(len_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(len_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 - - ! Write NLEN (one's complement of LEN, 2 bytes, little endian) - ! RFC 1951: NLEN = ~LEN (one's complement of LEN as 16-bit value) - nlen_field = iand(ieor(len_field, z'FFFF'), z'FFFF') - output_buffer(byte_pos) = int(iand(nlen_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(nlen_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 - - ! Copy raw data - output_buffer(byte_pos:byte_pos + block_size - 1) = input_data(pos:pos + block_size - 1) - byte_pos = byte_pos + block_size - pos = pos + block_size - remaining = remaining - block_size + end if + + ! Copy result + output_len = byte_pos - 1 + allocate(output_data(output_len)) + output_data(1:output_len) = bit_buffer(1:output_len) + + deallocate(bit_buffer) + end subroutine deflate_compress + + subroutine init_fixed_huffman_tables(literal_codes, literal_lengths, distance_codes, distance_lengths) + !! Initialize fixed Huffman tables as per RFC 1951 + integer, intent(out) :: literal_codes(0:285) + integer, intent(out) :: literal_lengths(0:285) + integer, intent(out) :: distance_codes(0:29) + integer, intent(out) :: distance_lengths(0:29) + integer :: i, code + + ! Fixed literal/length codes + code = 0 + ! 0-143: 8 bits (00110000 - 10111111) + do i = 0, 143 + literal_codes(i) = code + 48 ! Start at 00110000 + literal_lengths(i) = 8 + code = code + 1 end do - output_pos = byte_pos - end subroutine compress_with_uncompressed_blocks_improved - - subroutine compress_with_uncompressed_blocks_efficient(input_data, input_len, output_buffer, output_pos) - !! Create highly efficient uncompressed blocks for simple content - !! This produces smaller PNG files for low-complexity images - integer(int8), intent(in) :: input_data(*) - integer, intent(in) :: input_len - integer(int8), intent(inout) :: output_buffer(:) - integer, intent(inout) :: output_pos + code = 0 + ! 144-255: 9 bits (110010000 - 111111111) + do i = 144, 255 + literal_codes(i) = code + 400 ! Start at 110010000 + literal_lengths(i) = 9 + code = code + 1 + end do - integer :: pos, block_size, remaining - integer :: len_field, nlen_field, byte_pos - integer :: consecutive_zeros, i, efficiency_factor + code = 0 + ! 256-279: 7 bits (0000000 - 0010111) + do i = 256, 279 + literal_codes(i) = code + literal_lengths(i) = 7 + code = code + 1 + end do - ! Analyze data for efficiency optimization - consecutive_zeros = 0 - do i = 1, min(input_len, 4096) - if (input_data(i) == 0) consecutive_zeros = consecutive_zeros + 1 + code = 0 + ! 280-287: 8 bits (11000000 - 11000111) + do i = 280, 285 ! Note: only 280-285 are valid + literal_codes(i) = code + 192 ! Start at 11000000 + literal_lengths(i) = 8 + code = code + 1 end do - ! Calculate efficiency factor (higher for simpler content) - efficiency_factor = (consecutive_zeros * 100) / min(input_len, 4096) + ! Fixed distance codes (all 5 bits) + do i = 0, 29 + distance_codes(i) = i + distance_lengths(i) = 5 + end do + end subroutine init_fixed_huffman_tables + + function calculate_hash(data, pos, data_len) result(hash_val) + !! Calculate hash for LZ77 matching (3-byte hash) + integer(int8), intent(in) :: data(*) + integer, intent(in) :: pos, data_len + integer :: hash_val - pos = 1 - byte_pos = output_pos - remaining = input_len - - do while (remaining > 0) - ! Use very large blocks for simple content to minimize deflate overhead - if (efficiency_factor > 75) then - ! Very simple: massive blocks for minimal file size - block_size = min(remaining, 65535) - else if (efficiency_factor > 50) then - ! Somewhat simple: large blocks for smaller files - block_size = min(remaining, 49152) - else - ! Mixed content: medium blocks - block_size = min(remaining, 32768) - end if - - ! Write block header - if (remaining <= block_size) then - output_buffer(byte_pos) = 1_int8 ! Final block - else - output_buffer(byte_pos) = 0_int8 ! Not final - end if - byte_pos = byte_pos + 1 + if (pos + 2 <= data_len) then + hash_val = iand(ior(ior(ishft(iand(int(data(pos)), 255), 16), & + ishft(iand(int(data(pos+1)), 255), 8)), & + iand(int(data(pos+2)), 255)), HASH_SIZE - 1) + else + hash_val = 0 + end if + end function calculate_hash + + subroutine update_hash_table(hash_table, hash_chain, hash_val, pos) + !! Update hash table for LZ77 matching + integer, intent(inout) :: hash_table(0:) + integer, intent(inout) :: hash_chain(:) + integer, intent(in) :: hash_val, pos + + integer :: chain_pos + + chain_pos = iand(pos, WINDOW_SIZE - 1) + 1 + hash_chain(chain_pos) = hash_table(hash_val) + hash_table(hash_val) = pos + end subroutine update_hash_table + + subroutine find_longest_match(data, pos, data_len, hash_table, hash_chain, match_len, match_dist) + !! Find longest match using LZ77 algorithm + integer(int8), intent(in) :: data(*) + integer, intent(in) :: pos, data_len + integer, intent(in) :: hash_table(0:) + integer, intent(in) :: hash_chain(:) + integer, intent(out) :: match_len, match_dist + + integer :: hash_val, chain_pos, candidate_pos, len, max_len + integer :: i, chain_length + + match_len = 0 + match_dist = 0 + max_len = min(MAX_MATCH, data_len - pos + 1) + + if (max_len < MIN_MATCH) return + + hash_val = calculate_hash(data, pos, data_len) + candidate_pos = hash_table(hash_val) + chain_length = 0 + + do while (candidate_pos > 0 .and. candidate_pos < pos .and. chain_length < 128) + if (pos - candidate_pos > MAX_DISTANCE) exit - ! Write LEN and NLEN - len_field = block_size - output_buffer(byte_pos) = int(iand(len_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(len_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 + ! Check match length + len = 0 + do i = 0, max_len - 1 + if (data(pos + i) == data(candidate_pos + i)) then + len = len + 1 + else + exit + end if + end do - nlen_field = iand(not(len_field), z'FFFF') - output_buffer(byte_pos) = int(iand(nlen_field, z'FF'), int8) - output_buffer(byte_pos + 1) = int(iand(ishft(nlen_field, -8), z'FF'), int8) - byte_pos = byte_pos + 2 + if (len >= MIN_MATCH .and. len > match_len) then + match_len = len + match_dist = pos - candidate_pos + if (len >= max_len) exit ! Found maximum possible match + end if - ! Copy data - output_buffer(byte_pos:byte_pos + block_size - 1) = input_data(pos:pos + block_size - 1) - byte_pos = byte_pos + block_size - pos = pos + block_size - remaining = remaining - block_size + ! Follow hash chain + chain_pos = iand(candidate_pos, WINDOW_SIZE - 1) + 1 + candidate_pos = hash_chain(chain_pos) + chain_length = chain_length + 1 end do + end subroutine find_longest_match + + subroutine write_bits(buffer, bit_pos, byte_pos, value, num_bits) + !! Write bits to output buffer (LSB first) + integer(int8), intent(inout) :: buffer(:) + integer, intent(inout) :: bit_pos, byte_pos + integer, intent(in) :: value, num_bits - output_pos = byte_pos - end subroutine compress_with_uncompressed_blocks_efficient - - function analyze_visual_complexity(png_data, data_len) result(complexity) - !! Analyze PNG row data to detect non-background content density - !! PNG rows have format: filter_byte + R + G + B for each pixel - !! Returns higher values for plots with more non-background pixels - integer(int8), intent(in) :: png_data(*) - integer, intent(in) :: data_len - real :: complexity - - integer :: i, row_start, pixel_idx, non_white_count, sample_count - integer :: width_est, bytes_per_row, r, g, b - - ! Estimate image dimensions from data size - ! For 800x600 RGB: 600 rows * (1 filter byte + 800*3 RGB bytes) = 600 * 2401 = 1440600 - width_est = 800 ! Known from test setup - bytes_per_row = width_est * 3 + 1 ! RGB + filter byte - non_white_count = 0 - sample_count = 0 - - ! Sample every 8th row and every 8th pixel to detect non-background content - do i = 1, min(data_len / bytes_per_row, 75) ! Sample up to 75 rows - row_start = (i - 1) * bytes_per_row * 8 + 2 ! Every 8th row, skip filter byte - if (row_start + 600 < data_len) then ! Check we have room for sampling - ! Check every 8th pixel in this row (100 pixels total) - do pixel_idx = 0, 99 - if (row_start + pixel_idx*24 + 2 < data_len) then ! pixel_idx*8*3 = every 8th pixel - r = int(png_data(row_start + pixel_idx*24), kind=int32) - g = int(png_data(row_start + pixel_idx*24 + 1), kind=int32) - b = int(png_data(row_start + pixel_idx*24 + 2), kind=int32) - - sample_count = sample_count + 1 - - ! Count non-white pixels (assuming white background around 240-255) - if (r < 240 .or. g < 240 .or. b < 240) then - non_white_count = non_white_count + 1 - end if - end if - end do + integer :: i, bit + + do i = 0, num_bits - 1 + bit = iand(ishft(value, -i), 1) + + if (bit_pos == 0) then + buffer(byte_pos) = 0 + end if + + buffer(byte_pos) = ior(buffer(byte_pos), int(ishft(bit, bit_pos), int8)) + bit_pos = bit_pos + 1 + + if (bit_pos == 8) then + bit_pos = 0 + byte_pos = byte_pos + 1 end if end do + end subroutine write_bits + + subroutine encode_literal(buffer, bit_pos, byte_pos, literal, codes, lengths) + !! Encode a literal using Huffman coding + integer(int8), intent(inout) :: buffer(:) + integer, intent(inout) :: bit_pos, byte_pos + integer(int8), intent(in) :: literal + integer, intent(in) :: codes(0:), lengths(0:) + + integer :: lit_val + + lit_val = iand(int(literal), 255) ! Ensure 0-255 range + call write_bits(buffer, bit_pos, byte_pos, bit_reverse(codes(lit_val), lengths(lit_val)), lengths(lit_val)) + end subroutine encode_literal + + subroutine encode_length_distance(buffer, bit_pos, byte_pos, length, distance, & + literal_codes, literal_lengths, distance_codes, distance_lengths) + !! Encode length-distance pair using Huffman coding + integer(int8), intent(inout) :: buffer(:) + integer, intent(inout) :: bit_pos, byte_pos + integer, intent(in) :: length, distance + integer, intent(in) :: literal_codes(0:), literal_lengths(0:) + integer, intent(in) :: distance_codes(0:), distance_lengths(0:) + + integer :: length_code, length_extra_bits, length_extra + integer :: distance_code, distance_extra_bits, distance_extra + + ! Encode length + call get_length_code(length, length_code, length_extra_bits, length_extra) + call write_bits(buffer, bit_pos, byte_pos, & + bit_reverse(literal_codes(length_code), literal_lengths(length_code)), & + literal_lengths(length_code)) + if (length_extra_bits > 0) then + call write_bits(buffer, bit_pos, byte_pos, length_extra, length_extra_bits) + end if - ! Calculate content density (fraction of non-background pixels) - if (sample_count > 0) then - complexity = real(non_white_count) / real(sample_count) - else - complexity = 0.0 + ! Encode distance + call get_distance_code(distance, distance_code, distance_extra_bits, distance_extra) + call write_bits(buffer, bit_pos, byte_pos, & + bit_reverse(distance_codes(distance_code), distance_lengths(distance_code)), & + distance_lengths(distance_code)) + if (distance_extra_bits > 0) then + call write_bits(buffer, bit_pos, byte_pos, distance_extra, distance_extra_bits) + end if + end subroutine encode_length_distance + + subroutine get_length_code(length, code, extra_bits, extra) + !! Get length code and extra bits according to RFC 1951 + integer, intent(in) :: length + integer, intent(out) :: code, extra_bits, extra + + if (length < 3 .or. length > 258) then + code = 256 ! Invalid, use end-of-block + extra_bits = 0 + extra = 0 + return + end if + + if (length <= 10) then + code = 257 + (length - 3) + extra_bits = 0 + extra = 0 + else if (length <= 18) then + code = 265 + (length - 11) / 2 + extra_bits = 1 + extra = mod(length - 11, 2) + else if (length <= 34) then + code = 269 + (length - 19) / 4 + extra_bits = 2 + extra = mod(length - 19, 4) + else if (length <= 66) then + code = 273 + (length - 35) / 8 + extra_bits = 3 + extra = mod(length - 35, 8) + else if (length <= 130) then + code = 277 + (length - 67) / 16 + extra_bits = 4 + extra = mod(length - 67, 16) + else if (length <= 257) then + code = 281 + (length - 131) / 32 + extra_bits = 5 + extra = mod(length - 131, 32) + else ! length = 258 + code = 285 + extra_bits = 0 + extra = 0 + end if + end subroutine get_length_code + + subroutine get_distance_code(distance, code, extra_bits, extra) + !! Get distance code and extra bits according to RFC 1951 + integer, intent(in) :: distance + integer, intent(out) :: code, extra_bits, extra + + if (distance < 1 .or. distance > 32768) then + code = 0 + extra_bits = 0 + extra = 0 + return end if - ! Ensure reasonable range - complexity = max(0.0, min(1.0, complexity)) + if (distance <= 4) then + code = distance - 1 + extra_bits = 0 + extra = 0 + else if (distance <= 8) then + code = 4 + (distance - 5) / 2 + extra_bits = 1 + extra = mod(distance - 5, 2) + else if (distance <= 16) then + code = 6 + (distance - 9) / 4 + extra_bits = 2 + extra = mod(distance - 9, 4) + else if (distance <= 32) then + code = 8 + (distance - 17) / 8 + extra_bits = 3 + extra = mod(distance - 17, 8) + else if (distance <= 64) then + code = 10 + (distance - 33) / 16 + extra_bits = 4 + extra = mod(distance - 33, 16) + else if (distance <= 128) then + code = 12 + (distance - 65) / 32 + extra_bits = 5 + extra = mod(distance - 65, 32) + else if (distance <= 256) then + code = 14 + (distance - 129) / 64 + extra_bits = 6 + extra = mod(distance - 129, 64) + else if (distance <= 512) then + code = 16 + (distance - 257) / 128 + extra_bits = 7 + extra = mod(distance - 257, 128) + else if (distance <= 1024) then + code = 18 + (distance - 513) / 256 + extra_bits = 8 + extra = mod(distance - 513, 256) + else if (distance <= 2048) then + code = 20 + (distance - 1025) / 512 + extra_bits = 9 + extra = mod(distance - 1025, 512) + else if (distance <= 4096) then + code = 22 + (distance - 2049) / 1024 + extra_bits = 10 + extra = mod(distance - 2049, 1024) + else if (distance <= 8192) then + code = 24 + (distance - 4097) / 2048 + extra_bits = 11 + extra = mod(distance - 4097, 2048) + else if (distance <= 16384) then + code = 26 + (distance - 8193) / 4096 + extra_bits = 12 + extra = mod(distance - 8193, 4096) + else ! distance <= 32768 + code = 28 + (distance - 16385) / 8192 + extra_bits = 13 + extra = mod(distance - 16385, 8192) + end if + end subroutine get_distance_code + + function bit_reverse(value, num_bits) result(reversed_value) + !! Reverses the bits of a given value up to num_bits. + integer, intent(in) :: value, num_bits + integer :: reversed_value + integer :: i - end function analyze_visual_complexity + reversed_value = 0 + do i = 0, num_bits - 1 + if (iand(ishft(value, -i), 1) == 1) then + reversed_value = ior(reversed_value, ishft(1, num_bits - 1 - i)) + end if + end do + end function bit_reverse end module fortplot_zlib_core \ No newline at end of file diff --git a/src/figures/interfaces/fortplot_figure_advanced_plotting_interface.f90 b/src/figures/interfaces/fortplot_figure_advanced_plotting_interface.f90 deleted file mode 100644 index 1b980715..00000000 --- a/src/figures/interfaces/fortplot_figure_advanced_plotting_interface.f90 +++ /dev/null @@ -1,32 +0,0 @@ -module fortplot_figure_advanced_plotting_interface - !! Interface aggregator for advanced plotting operations - !! - !! This module provides a focused interface for advanced plotting functionality - !! including boxplots, streamlines, and specialized plot types. Split from the - !! main plotting interface to maintain ≤3 dependencies per module. - !! - !! ARCHITECTURE: Interface Segregation Pattern - !! - Aggregates advanced plotting operations from 3 focused modules - !! - Maintains ≤3 dependency limit per interface module - !! - Separates basic plots from advanced visualizations - !! - Preserves all public APIs for backward compatibility - - use, intrinsic :: iso_fortran_env, only: wp => real64 - use fortplot_context - use fortplot_plot_data, only: plot_data_t - use fortplot_figure_initialization, only: figure_state_t - - ! Import focused advanced plotting modules (≤3 dependencies) - use fortplot_figure_boxplot, only: add_boxplot, update_boxplot_ranges - use fortplot_figure_streamlines, only: streamplot_figure, clear_streamline_data - use fortplot_figure_histogram, only: hist_figure - - implicit none - private - - ! Re-export all advanced plotting interfaces with proper signatures - public :: add_boxplot, update_boxplot_ranges - public :: streamplot_figure, clear_streamline_data - public :: hist_figure - -end module fortplot_figure_advanced_plotting_interface \ No newline at end of file diff --git a/src/figures/interfaces/fortplot_figure_configuration_interface.f90 b/src/figures/interfaces/fortplot_figure_configuration_interface.f90 deleted file mode 100644 index b567f26b..00000000 --- a/src/figures/interfaces/fortplot_figure_configuration_interface.f90 +++ /dev/null @@ -1,41 +0,0 @@ -module fortplot_figure_configuration_interface - !! Interface aggregator for all configuration operations - !! - !! This module provides a focused interface that aggregates all configuration - !! functionality from specialized modules. This reduces coupling by providing - !! a single dependency for core modules instead of multiple direct dependencies. - !! - !! ARCHITECTURE: Interface Segregation Pattern - !! - Aggregates configuration operations from 3 focused modules - !! - Reduces coupling from 19 dependencies to 3 in core modules - !! - Maintains clean separation between configuration types - !! - Preserves all public APIs for backward compatibility - - use, intrinsic :: iso_fortran_env, only: wp => real64 - use fortplot_context - use fortplot_figure_initialization, only: figure_state_t - - ! Import focused configuration modules (≤3 dependencies) - use fortplot_figure_core_config, only: grid_figure, set_xlabel_figure, & - set_ylabel_figure, set_title_figure, & - set_xscale_figure, set_yscale_figure, & - set_xlim_figure, set_ylim_figure, & - set_line_width_figure - use fortplot_figure_properties_new - use fortplot_figure_grid, only: render_grid_lines - - implicit none - private - - ! Re-export all configuration interfaces with proper signatures - public :: grid_figure, set_xlabel_figure, set_ylabel_figure, set_title_figure - public :: set_xscale_figure, set_yscale_figure, set_xlim_figure, set_ylim_figure - public :: set_line_width_figure, render_grid_lines - - ! Re-export properties interfaces - public :: figure_get_width, figure_get_height, figure_get_rendered, figure_set_rendered - public :: figure_get_plot_count, figure_get_plots, figure_backend_color, figure_backend_associated - public :: figure_backend_line, figure_get_x_min, figure_get_x_max, figure_get_y_min, figure_get_y_max - public :: figure_update_data_ranges_pcolormesh, figure_update_data_ranges_boxplot, figure_update_data_ranges - -end module fortplot_figure_configuration_interface \ No newline at end of file diff --git a/src/figures/interfaces/fortplot_figure_management_interface.f90 b/src/figures/interfaces/fortplot_figure_management_interface.f90 deleted file mode 100644 index ebeacb6e..00000000 --- a/src/figures/interfaces/fortplot_figure_management_interface.f90 +++ /dev/null @@ -1,41 +0,0 @@ -module fortplot_figure_management_interface - !! Interface aggregator for all management operations - !! - !! This module provides a focused interface that aggregates all management - !! functionality from specialized modules. This reduces coupling by providing - !! a single dependency for core modules instead of multiple direct dependencies. - !! - !! ARCHITECTURE: Interface Segregation Pattern - !! - Aggregates management operations from 3 focused modules - !! - Reduces coupling from 19 dependencies to 3 in core modules - !! - Maintains clean separation between management types - !! - Preserves all public APIs for backward compatibility - - use, intrinsic :: iso_fortran_env, only: wp => real64 - use fortplot_context - use fortplot_plot_data, only: plot_data_t - use fortplot_figure_initialization, only: figure_state_t, initialize_figure_state - - ! Import focused management modules (≤3 dependencies) - use fortplot_figure_management - use fortplot_figure_rendering_pipeline, only: calculate_figure_data_ranges, & - render_figure_to_backend - use fortplot_figure_plot_management, only: update_plot_ydata, setup_figure_legend - - implicit none - private - - ! Re-export all management interfaces with proper signatures - public :: initialize_figure_state - public :: calculate_figure_data_ranges, render_figure_to_backend - public :: update_plot_ydata, setup_figure_legend - - ! Management operation interfaces from figure_management - public :: figure_initialize, figure_destroy, figure_savefig, figure_savefig_with_status - public :: figure_show, figure_clear_streamlines, figure_setup_png_backend_for_animation - public :: figure_extract_rgb_data_for_animation, figure_extract_png_data_for_animation - public :: figure_subplots, figure_subplot_plot, figure_subplot_plot_count - public :: figure_subplot_set_title, figure_subplot_set_xlabel, figure_subplot_set_ylabel - public :: figure_subplot_title - -end module fortplot_figure_management_interface \ No newline at end of file diff --git a/src/figures/interfaces/fortplot_figure_plotting_interface.f90 b/src/figures/interfaces/fortplot_figure_plotting_interface.f90 deleted file mode 100644 index 4f68ac01..00000000 --- a/src/figures/interfaces/fortplot_figure_plotting_interface.f90 +++ /dev/null @@ -1,32 +0,0 @@ -module fortplot_figure_plotting_interface - !! Interface aggregator for all plotting operations - !! - !! This module provides a focused interface that aggregates all plotting - !! functionality from specialized modules. This reduces coupling by providing - !! a single dependency for core modules instead of multiple direct dependencies. - !! - !! ARCHITECTURE: Interface Segregation Pattern - !! - Aggregates plotting operations from 3 focused modules - !! - Reduces coupling from 19 dependencies to 3 in core modules - !! - Maintains clean separation between plotting types - !! - Preserves all public APIs for backward compatibility - - use, intrinsic :: iso_fortran_env, only: wp => real64 - use fortplot_context - use fortplot_plot_data, only: plot_data_t - use fortplot_figure_initialization, only: figure_state_t - - ! Import focused basic plotting modules (≤3 dependencies) - use fortplot_figure_plots, only: figure_add_plot, figure_add_contour, & - figure_add_contour_filled, figure_add_pcolormesh - use fortplot_figure_scatter, only: add_scatter_plot - ! Note: Advanced plots (boxplot, streamlines, histogram) moved to advanced_plotting_interface - - implicit none - private - - ! Re-export basic plotting interfaces with proper signatures - public :: figure_add_plot, figure_add_contour, figure_add_contour_filled - public :: figure_add_pcolormesh, add_scatter_plot - -end module fortplot_figure_plotting_interface \ No newline at end of file diff --git a/src/interfaces/fortplot_matplotlib_plotting.f90 b/src/interfaces/fortplot_matplotlib_plotting.f90 index 1d4abb2e..a541cd64 100644 --- a/src/interfaces/fortplot_matplotlib_plotting.f90 +++ b/src/interfaces/fortplot_matplotlib_plotting.f90 @@ -7,7 +7,9 @@ module fortplot_matplotlib_plotting use fortplot_global, only: fig => global_figure use fortplot_logging, only: log_error, log_warning, log_info use fortplot_plotting_advanced, only: bar_impl, barh_impl - use fortplot_plotting, only: add_scatter_2d, errorbar_impl => errorbar, add_3d_plot_impl => add_3d_plot + use fortplot_scatter_plots, only: add_scatter_2d + use fortplot_errorbar_plots, only: errorbar_impl => errorbar + use fortplot_3d_plots, only: add_3d_plot_impl => add_3d_plot implicit none private diff --git a/src/interfaces/fortplot_python_bridge.f90 b/src/interfaces/fortplot_python_bridge.f90 deleted file mode 100644 index 40d2273d..00000000 --- a/src/interfaces/fortplot_python_bridge.f90 +++ /dev/null @@ -1,202 +0,0 @@ -program fortplot_python_bridge - !! Fortran program that acts as a bridge for Python calls - !! This reads commands from stdin and executes fortplot functions - use fortplot_matplotlib - use iso_fortran_env, only: wp => real64 - implicit none - - character(len=256) :: command - real(wp), allocatable :: x(:), y(:), data(:) - integer :: ios - - ! Initialize figure - call figure() - - ! Main command processing loop - do - read(*, '(A)', iostat=ios) command - if (ios /= 0) exit - - command = adjustl(trim(command)) - if (len_trim(command) == 0) cycle - - select case (trim(command)) - case ('FIGURE') - call process_figure_command() - case ('PLOT') - call process_plot_command(x, y) - case ('SCATTER') - call process_scatter_command(x, y) - case ('HISTOGRAM') - call process_histogram_command(data) - case ('TITLE') - call process_title_command() - case ('XLABEL') - call process_xlabel_command() - case ('YLABEL') - call process_ylabel_command() - case ('LEGEND') - call legend() - case ('SAVEFIG') - call process_savefig_command() - case ('SHOW') - call process_show_command() - case ('XLIM') - call process_xlim_command() - case ('YLIM') - call process_ylim_command() - case ('QUIT', 'EXIT') - exit - case default - ! Unknown command, skip - cycle - end select - end do - - ! Cleanup - if (allocated(x)) deallocate(x) - if (allocated(y)) deallocate(y) - if (allocated(data)) deallocate(data) - -contains - - subroutine process_figure_command() - integer :: ios - read(*, *, iostat=ios) ! width, height (ignored for now) - call figure() - end subroutine - - subroutine process_plot_command(x_arr, y_arr) - real(wp), allocatable, intent(inout) :: x_arr(:), y_arr(:) - character(len=256) :: label_str - integer :: n, i, ios - - read(*, *, iostat=ios) n - if (ios /= 0) return - - if (allocated(x_arr)) deallocate(x_arr) - if (allocated(y_arr)) deallocate(y_arr) - allocate(x_arr(n), y_arr(n)) - - call read_xy_data(x_arr, y_arr, n) - - read(*, '(A)', iostat=ios) label_str - if (ios == 0 .and. len_trim(label_str) > 0) then - call plot(x_arr, y_arr, label=trim(label_str)) - else - call plot(x_arr, y_arr) - end if - end subroutine - - subroutine process_scatter_command(x_arr, y_arr) - real(wp), allocatable, intent(inout) :: x_arr(:), y_arr(:) - character(len=256) :: label_str - integer :: n, ios - - read(*, *, iostat=ios) n - if (ios /= 0) return - - if (allocated(x_arr)) deallocate(x_arr) - if (allocated(y_arr)) deallocate(y_arr) - allocate(x_arr(n), y_arr(n)) - - call read_xy_data(x_arr, y_arr, n) - - read(*, '(A)', iostat=ios) label_str - if (ios == 0 .and. len_trim(label_str) > 0) then - call scatter(x_arr, y_arr, label=trim(label_str)) - else - call scatter(x_arr, y_arr) - end if - end subroutine - - subroutine process_histogram_command(data_arr) - real(wp), allocatable, intent(inout) :: data_arr(:) - character(len=256) :: label_str - integer :: n, i, ios - - read(*, *, iostat=ios) n - if (ios /= 0) return - - if (allocated(data_arr)) deallocate(data_arr) - allocate(data_arr(n)) - - do i = 1, n - read(*, *, iostat=ios) data_arr(i) - if (ios /= 0) exit - end do - - read(*, '(A)', iostat=ios) label_str - if (ios == 0 .and. len_trim(label_str) > 0) then - call hist(data_arr, label=trim(label_str)) - else - call hist(data_arr) - end if - end subroutine - - subroutine read_xy_data(x_arr, y_arr, n) - real(wp), intent(out) :: x_arr(:), y_arr(:) - integer, intent(in) :: n - integer :: i, ios - - do i = 1, n - read(*, *, iostat=ios) x_arr(i) - if (ios /= 0) exit - end do - - do i = 1, n - read(*, *, iostat=ios) y_arr(i) - if (ios /= 0) exit - end do - end subroutine - - subroutine process_title_command() - character(len=256) :: title_str - integer :: ios - read(*, '(A)', iostat=ios) title_str - if (ios == 0) call title(trim(title_str)) - end subroutine - - subroutine process_xlabel_command() - character(len=256) :: xlabel_str - integer :: ios - read(*, '(A)', iostat=ios) xlabel_str - if (ios == 0) call xlabel(trim(xlabel_str)) - end subroutine - - subroutine process_ylabel_command() - character(len=256) :: ylabel_str - integer :: ios - read(*, '(A)', iostat=ios) ylabel_str - if (ios == 0) call ylabel(trim(ylabel_str)) - end subroutine - - subroutine process_savefig_command() - character(len=256) :: filename_str - integer :: ios - read(*, '(A)', iostat=ios) filename_str - if (ios == 0) call savefig(trim(filename_str)) - end subroutine - - subroutine process_show_command() - logical :: blocking_flag - integer :: ios - read(*, *, iostat=ios) blocking_flag - call show(blocking=blocking_flag) - end subroutine - - subroutine process_xlim_command() - real(wp) :: xmin_val, xmax_val - integer :: ios - read(*, *, iostat=ios) xmin_val, xmax_val - if (ios == 0) call xlim(xmin_val, xmax_val) - end subroutine - - subroutine process_ylim_command() - real(wp) :: ymin_val, ymax_val - integer :: ios - read(*, *, iostat=ios) ymin_val, ymax_val - if (ios == 0) call ylim(ymin_val, ymax_val) - end subroutine - -end program fortplot_python_bridge \ No newline at end of file diff --git a/src/interfaces/fortplot_python_interface.f90 b/src/interfaces/fortplot_python_interface.f90 deleted file mode 100644 index 9208221c..00000000 --- a/src/interfaces/fortplot_python_interface.f90 +++ /dev/null @@ -1,263 +0,0 @@ -module fortplot_python_interface - !! Python interface module for F2PY binding generation - !! - !! This module provides a clean interface for Python bindings using F2PY. - !! It exposes the essential functions needed for simplified Python show() API. - !! - !! Key functions: - !! - show_figure(blocking): Direct call to Fortran show with blocking parameter - !! - show_viewer(blocking): Direct call to Fortran show_viewer with blocking parameter - !! - All other plotting functions for complete API coverage - - use iso_fortran_env, only: wp => real64 - use fortplot_matplotlib, only: mpl_show => show, mpl_show_viewer => show_viewer, & - mpl_figure => figure, mpl_plot => plot, & - mpl_savefig => savefig, mpl_title => title, & - mpl_xlabel => xlabel, mpl_ylabel => ylabel, & - mpl_contour => contour, mpl_contour_filled => contour_filled, & - mpl_pcolormesh => pcolormesh, mpl_streamplot => streamplot, & - mpl_legend => legend, mpl_set_xscale => set_xscale, & - mpl_set_yscale => set_yscale, mpl_xlim => xlim, mpl_ylim => ylim, & - mpl_scatter => scatter, mpl_histogram => histogram - implicit none - - private - - ! Export functions for F2PY - use simple names for Python access - public :: show_figure, show_viewer, figure, plot, savefig - public :: title, xlabel, ylabel, contour, contour_filled - public :: pcolormesh, streamplot, legend, scatter, histogram - public :: set_xscale, set_yscale, xlim, ylim - -contains - - subroutine show_figure(blocking) - !! Python-accessible show_figure function with optional blocking - !! This replaces the temp file + webbrowser approach with direct Fortran call - !! - !! Arguments: - !! blocking: Optional - if true, wait for user input after display (default: false) - logical, intent(in), optional :: blocking - - call mpl_show(blocking=blocking) - end subroutine show_figure - - subroutine show_viewer(blocking) - !! Python-accessible show_viewer function with optional blocking - !! Forces display in system viewer regardless of GUI availability - !! - !! Arguments: - !! blocking: Optional - if true, wait for user input after display (default: false) - logical, intent(in), optional :: blocking - - call mpl_show_viewer(blocking=blocking) - end subroutine show_viewer - - subroutine figure(width, height) - !! Python-accessible figure initialization - !! - !! Arguments: - !! width, height: Optional figure dimensions in pixels (default: 640x480) - integer, intent(in), optional :: width, height - - real(8), dimension(2) :: figsize - real(8), parameter :: DPI = 100.0d0 - - if (present(width) .and. present(height)) then - ! Convert pixel dimensions to inches for matplotlib compatibility - ! Python passes pixels, but mpl_figure expects inches - figsize = [real(width, 8) / DPI, real(height, 8) / DPI] - call mpl_figure(figsize=figsize) - else - call mpl_figure() - end if - end subroutine figure - - subroutine plot(x, y, n, label, linestyle) - !! Python-accessible plot function for line plots - !! - !! Arguments: - !! x, y: Data arrays for line plot - !! n: Array size - !! label: Optional label for legend - !! linestyle: Optional line style string - integer, intent(in) :: n - real(wp), dimension(n), intent(in) :: x, y - character(len=*), intent(in), optional :: label, linestyle - - call mpl_plot(x, y, label=label, linestyle=linestyle) - end subroutine plot - - subroutine savefig(filename) - !! Python-accessible savefig function - !! - !! Arguments: - !! filename: Output filename (extension determines format) - character(len=*), intent(in) :: filename - - call mpl_savefig(filename) - end subroutine savefig - - subroutine title(text) - !! Python-accessible title function - character(len=*), intent(in) :: text - call mpl_title(text) - end subroutine title - - subroutine xlabel(text) - !! Python-accessible xlabel function - character(len=*), intent(in) :: text - call mpl_xlabel(text) - end subroutine xlabel - - subroutine ylabel(text) - !! Python-accessible ylabel function - character(len=*), intent(in) :: text - call mpl_ylabel(text) - end subroutine ylabel - - subroutine contour(x, y, z, nx, ny, levels, nlevels) - !! Python-accessible contour function - !! - !! Arguments: - !! x, y: Grid coordinate arrays - !! z: 2D data array for contouring - !! nx, ny: Array dimensions - !! levels: Optional contour levels - !! nlevels: Number of levels - integer, intent(in) :: nx, ny - real(wp), dimension(nx), intent(in) :: x - real(wp), dimension(ny), intent(in) :: y - real(wp), dimension(nx, ny), intent(in) :: z - integer, intent(in), optional :: nlevels - real(wp), dimension(:), intent(in), optional :: levels - - if (present(levels) .and. present(nlevels)) then - call mpl_contour(x, y, z, levels=levels(1:nlevels)) - else - call mpl_contour(x, y, z) - end if - end subroutine contour - - subroutine contour_filled(x, y, z, nx, ny, levels, nlevels, colormap) - !! Python-accessible filled contour function - integer, intent(in) :: nx, ny - real(wp), dimension(nx), intent(in) :: x - real(wp), dimension(ny), intent(in) :: y - real(wp), dimension(nx, ny), intent(in) :: z - integer, intent(in), optional :: nlevels - real(wp), dimension(:), intent(in), optional :: levels - character(len=*), intent(in), optional :: colormap - - if (present(levels) .and. present(nlevels)) then - call mpl_contour_filled(x, y, z, levels=levels(1:nlevels), colormap=colormap) - else - call mpl_contour_filled(x, y, z, colormap=colormap) - end if - end subroutine contour_filled - - subroutine pcolormesh(x, y, c, nx, ny, colormap, vmin, vmax, edgecolors, linewidths) - !! Python-accessible pcolormesh function - integer, intent(in) :: nx, ny - real(wp), dimension(nx), intent(in) :: x - real(wp), dimension(ny), intent(in) :: y - real(wp), dimension(nx, ny), intent(in) :: c - character(len=*), intent(in), optional :: colormap, edgecolors - real(wp), intent(in), optional :: vmin, vmax, linewidths - - call mpl_pcolormesh(x, y, c, colormap=colormap) - end subroutine pcolormesh - - subroutine streamplot(x, y, u, v, nx, ny, density, arrowsize, arrowstyle) - !! Python-accessible streamplot function with arrow support - integer, intent(in) :: nx, ny - real(wp), dimension(nx), intent(in) :: x - real(wp), dimension(ny), intent(in) :: y - real(wp), dimension(nx, ny), intent(in) :: u, v - real(wp), intent(in), optional :: density - real(wp), intent(in), optional :: arrowsize - character(len=*), intent(in), optional :: arrowstyle - - call mpl_streamplot(x, y, u, v, density=density, arrow_scale=arrowsize) - end subroutine streamplot - - subroutine legend() - !! Python-accessible legend function - call mpl_legend() - end subroutine legend - - subroutine set_xscale(scale) - !! Python-accessible x-axis scale function - character(len=*), intent(in) :: scale - call mpl_set_xscale(scale) - end subroutine set_xscale - - subroutine set_yscale(scale) - !! Python-accessible y-axis scale function - character(len=*), intent(in) :: scale - call mpl_set_yscale(scale) - end subroutine set_yscale - - subroutine xlim(xmin, xmax) - !! Python-accessible x-axis limits function - real(wp), intent(in) :: xmin, xmax - call mpl_xlim(xmin, xmax) - end subroutine xlim - - subroutine ylim(ymin, ymax) - !! Python-accessible y-axis limits function - real(wp), intent(in) :: ymin, ymax - call mpl_ylim(ymin, ymax) - end subroutine ylim - - subroutine scatter(x, y, n, s, c, label, marker, markersize, color, & - colormap, vmin, vmax, show_colorbar) - !! Python-accessible scatter plot function - !! - !! Arguments: - !! x, y: Data arrays for scatter plot points - !! n: Array size - !! s: Optional marker sizes - !! c: Optional color values - !! label: Optional label for legend - !! marker: Optional marker style - !! markersize: Optional marker size - !! color: Optional RGB color - !! colormap: Optional colormap name - !! vmin, vmax: Optional color scale limits - !! show_colorbar: Optional colorbar display flag - integer, intent(in) :: n - real(wp), dimension(n), intent(in) :: x, y - real(wp), dimension(:), intent(in), optional :: s, c - character(len=*), intent(in), optional :: label, marker, colormap - real(wp), intent(in), optional :: markersize, vmin, vmax - real(wp), dimension(3), intent(in), optional :: color - logical, intent(in), optional :: show_colorbar - - call mpl_scatter(x, y, s=s, c=c, label=label, marker=marker, & - markersize=markersize, color=color, & - colormap=colormap, vmin=vmin, vmax=vmax, & - show_colorbar=show_colorbar) - end subroutine scatter - - subroutine histogram(data, n, bins, density, label, color) - !! Python-accessible histogram function - !! - !! Arguments: - !! data: Data array for histogram - !! n: Array size - !! bins: Optional number of bins - !! density: Optional density normalization - !! label: Optional label for legend - !! color: Optional RGB color - integer, intent(in) :: n - real(wp), dimension(n), intent(in) :: data - integer, intent(in), optional :: bins - logical, intent(in), optional :: density - character(len=*), intent(in), optional :: label - real(wp), dimension(3), intent(in), optional :: color - - call mpl_histogram(data, bins=bins, density=density, label=label, color=color) - end subroutine histogram - -end module fortplot_python_interface \ No newline at end of file diff --git a/src/plotting/fortplot_plotting.f90 b/src/plotting/fortplot_plotting.f90 deleted file mode 100644 index 0a5a10ce..00000000 --- a/src/plotting/fortplot_plotting.f90 +++ /dev/null @@ -1,20 +0,0 @@ -module fortplot_plotting - !! Basic plot addition orchestrator module (SOLID principles compliance) - !! - !! This module orchestrates plotting operations by delegating to - !! specialized plotting modules for better modularity and maintainability. - - use fortplot_2d_plots - use fortplot_3d_plots - use fortplot_scatter_plots - use fortplot_errorbar_plots - use fortplot_plot_annotations - - implicit none - - private - public :: add_plot, add_3d_plot, add_scatter_2d, add_scatter_3d, add_surface - public :: errorbar, add_text_annotation, add_arrow_annotation - public :: add_line_plot_data, add_scatter_plot_data ! Export for advanced module - -end module fortplot_plotting \ No newline at end of file diff --git a/src/testing/fortplot_functionality_verification.f90 b/src/testing/fortplot_functionality_verification.f90 deleted file mode 100644 index 72f5c6eb..00000000 --- a/src/testing/fortplot_functionality_verification.f90 +++ /dev/null @@ -1,16 +0,0 @@ -! fortplot_functionality_verification.f90 -! Comprehensive Functionality Preservation Verification System -! Refactored for file size compliance (Issue #884) - delegates to specialized modules -module fortplot_functionality_verification - use fortplot_verification_core, only: functionality_verifier_t, verification_report_t, baseline_t, & - create_functionality_verifier - use fortplot_verification_reports, only: run_comprehensive_verification, generate_evidence_report, & - compare_with_baseline_comprehensive - implicit none - - private - public :: functionality_verifier_t, verification_report_t, baseline_t - public :: create_functionality_verifier, run_comprehensive_verification - public :: generate_evidence_report, compare_with_baseline_comprehensive - -end module fortplot_functionality_verification \ No newline at end of file diff --git a/src/testing/fortplot_png_validation.f90 b/src/testing/fortplot_png_validation.f90 new file mode 100644 index 00000000..f64dd8b7 --- /dev/null +++ b/src/testing/fortplot_png_validation.f90 @@ -0,0 +1,72 @@ +module fortplot_png_validation + !! PNG file validation using external pngcheck tool + !! CRITICAL: Every PNG file MUST be validated after creation + implicit none + + private + public :: validate_png_file, png_validation_available + +contains + + function png_validation_available() result(available) + !! Check if pngcheck tool is available on system + logical :: available + integer :: exit_code + + ! Test if pngcheck is available + call execute_command_line("which pngcheck > /dev/null 2>&1", exitstat=exit_code) + available = (exit_code == 0) + end function png_validation_available + + subroutine validate_png_file(filename, test_name) + !! Validate PNG file using pngcheck and fail test if invalid + character(len=*), intent(in) :: filename + character(len=*), intent(in) :: test_name + + character(len=512) :: command + integer :: exit_code, pdf_check_code + logical :: file_exists + + ! Check if file exists first + inquire(file=filename, exist=file_exists) + if (.not. file_exists) then + print *, "FATAL: PNG file does not exist: ", trim(filename) + print *, "TEST FAILED: ", trim(test_name) + error stop "PNG file missing" + end if + + ! Skip validation if pngcheck not available (CI environments) + if (.not. png_validation_available()) then + print *, "WARNING: pngcheck not available, skipping PNG validation" + return + end if + + ! Run pngcheck on the PNG file + write(command, '(A,A,A)') 'pngcheck "', trim(filename), '" > /dev/null 2>&1' + call execute_command_line(command, exitstat=exit_code) + + if (exit_code /= 0) then + ! Check if the file might be a PDF that fell back + write(command, '(A,A,A)') 'file "', trim(filename), '" | grep -i pdf > /dev/null 2>&1' + call execute_command_line(command, exitstat=pdf_check_code) + + if (pdf_check_code == 0) then + print *, "WARNING: File ", trim(filename), " is PDF, not PNG (backend fallback)" + print *, "✓ PNG validation skipped for PDF fallback file" + return + else + print *, "FATAL: PNG file failed validation: ", trim(filename) + print *, "TEST FAILED: ", trim(test_name) + + ! Show the actual pngcheck error + write(command, '(A,A,A)') 'echo "PNG validation error:"; pngcheck "', trim(filename), '"' + call execute_command_line(command) + + error stop "PNG validation failed - file is corrupted" + end if + else + print *, "✓ PNG validation passed: ", trim(filename) + end if + end subroutine validate_png_file + +end module fortplot_png_validation \ No newline at end of file diff --git a/test/test_bitmap_to_png_buffer.f90 b/test/test_bitmap_to_png_buffer.f90 index 7a3c3e86..fd5ae708 100644 --- a/test/test_bitmap_to_png_buffer.f90 +++ b/test/test_bitmap_to_png_buffer.f90 @@ -3,6 +3,7 @@ program test_bitmap_to_png_buffer use fortplot_bitmap, only: render_text_to_bitmap, rotate_bitmap_90_cw use fortplot_png_encoder, only: bitmap_to_png_buffer use fortplot_png, only: write_png_file + use fortplot_png_validation, only: validate_png_file use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none @@ -57,7 +58,9 @@ program test_bitmap_to_png_buffer ! Write actual PNG files using the real PNG writer call write_png_file("test_bitmap_original.png", width, height, png_buffer) + call validate_png_file("test_bitmap_original.png", "Bitmap PNG test - original") call write_png_file("test_bitmap_rotated.png", height, width, rotated_png_buffer) + call validate_png_file("test_bitmap_rotated.png", "Bitmap PNG test - rotated") print *, "SUCCESS: PNG buffer conversion tests passed" print *, " bitmap_to_png_buffer format validated" diff --git a/test/test_png_overflow.f90 b/test/test_png_overflow.f90 index 946f9009..757f80b1 100644 --- a/test/test_png_overflow.f90 +++ b/test/test_png_overflow.f90 @@ -1,13 +1,15 @@ program test_png_overflow - !! Test PNG dimension overflow handling + !! Test PNG dimension overflow handling with automatic validation use fortplot_context, only: plot_context use fortplot_matplotlib, only: figure, savefig, plot use fortplot_utils, only: initialize_backend + use fortplot_png_validation, only: validate_png_file use iso_fortran_env, only: wp => real64 implicit none real(wp) :: x(100), y(100) integer :: i + logical :: file_exists print *, "Testing PNG dimension overflow handling..." @@ -22,7 +24,8 @@ program test_png_overflow call figure(figsize=[8.0_wp, 6.0_wp]) call plot(x, y) call savefig("test/output/test_normal.png") - print *, " Normal PNG saved successfully" + call validate_png_file("test/output/test_normal.png", "PNG overflow test - normal dimensions") + print *, " Normal PNG saved and validated successfully" ! Test 2: Large pixel values mistakenly used as inches ! This would create 64000x48000 pixels and should trigger overflow @@ -30,21 +33,24 @@ program test_png_overflow call figure(figsize=[640.0_wp, 480.0_wp]) call plot(x, y) call savefig("test/output/test_overflow.png") - print *, " Large dimension PNG saved (should handle overflow)" + call validate_png_file("test/output/test_overflow.png", "PNG overflow test - large dimensions") + print *, " Large dimension file saved and validated (may be PDF fallback)" ! Test 3: Edge case at validation boundary (50x50 inches * 100 = 5000x5000 pixels) print *, "Test 3: Edge case dimensions (50x50)" call figure(figsize=[50.0_wp, 50.0_wp]) call plot(x, y) call savefig("test/output/test_edge.png") - print *, " Edge case PNG saved" + call validate_png_file("test/output/test_edge.png", "PNG overflow test - edge case dimensions") + print *, " Edge case file saved and validated (may be PDF fallback)" ! Test 4: Just over validation boundary (51x51 inches * 100 = 5100x5100 pixels) print *, "Test 4: Over boundary dimensions (51x51)" call figure(figsize=[51.0_wp, 51.0_wp]) call plot(x, y) call savefig("test/output/test_over.png") - print *, " Over boundary PNG handled" + call validate_png_file("test/output/test_over.png", "PNG overflow test - over boundary dimensions") + print *, " Over boundary file handled and validated (may be PDF fallback)" print *, "All tests completed!" diff --git a/test_basic_user_workflow b/test_basic_user_workflow new file mode 100755 index 00000000..16bd1f4c Binary files /dev/null and b/test_basic_user_workflow differ diff --git a/test_basic_user_workflow.f90 b/test_basic_user_workflow.f90 new file mode 100644 index 00000000..8d4fdb38 --- /dev/null +++ b/test_basic_user_workflow.f90 @@ -0,0 +1,24 @@ +program test_basic_user_workflow + ! Test basic user workflow from README + use fortplot + implicit none + + real(wp), dimension(50) :: x, y + integer :: i + + ! Basic stateful API from README + x = [(real(i-1, wp) * 0.2_wp, i=1, 50)] + y = sin(x) + + call figure() + call plot(x, y) + call title("Function Plot") + call xlabel("x") + call ylabel("y") + call xlim(0.0_wp, 10.0_wp) + call ylim(-1.0_wp, 1.0_wp) + call savefig("basic_user_test.png") + + print *, "Basic user workflow test completed" + +end program test_basic_user_workflow \ No newline at end of file diff --git a/test_readme_promises b/test_readme_promises new file mode 100755 index 00000000..e8191c38 Binary files /dev/null and b/test_readme_promises differ diff --git a/test_readme_promises.f90 b/test_readme_promises.f90 new file mode 100644 index 00000000..ca46cc6f --- /dev/null +++ b/test_readme_promises.f90 @@ -0,0 +1,69 @@ +program test_readme_promises + ! Test EVERY promise made in README.md + use fortplot + implicit none + + real(wp), dimension(50) :: x, y, t, damped_sine, damped_cosine + integer :: i + + ! README Promise #1: Basic stateful API should work + print *, "Testing README basic stateful API promise..." + x = [(real(i-1, wp) * 0.2_wp, i=1, 50)] + y = sin(x) + + call figure() + call plot(x, y) + call title("Function Plot") + call xlabel("x") + call ylabel("y") + call xlim(0.0_wp, 10.0_wp) + call ylim(-1.0_wp, 1.0_wp) + call savefig("test_readme_basic.png") + + ! README Promise #2: 3D plotting should work + print *, "Testing README 3D plotting promise..." + call figure(figsize=[8.0_wp, 6.0_wp]) + call add_3d_plot(x(1:30), y(1:30), sin(x(1:30)*2), label="3D curve") + call title("3D Line Plot") + call savefig("test_3d_plot.png") + + ! README Promise #3: Legend with multiple plots + print *, "Testing README legend promise..." + call figure(figsize=[8.0_wp, 6.0_wp]) + call plot(x, sin(x), label="sin(x)", linestyle="b-") + call plot(x, cos(x), label="cos(x)", linestyle="r--") + call plot(x, sin(2*x), label="sin(2x)", linestyle="g:") + call legend() + call savefig("test_trig_functions.pdf") + + ! README Promise #4: Unicode and Greek letters + print *, "Testing README Unicode/Greek letters promise..." + t = x + damped_sine = exp(-0.1_wp * t) * sin(t) + damped_cosine = exp(-0.1_wp * t) * cos(t) + call figure(figsize=[8.0_wp, 6.0_wp]) + call title("Wave Functions: \psi(\omega t) = A e^{-\lambda t} sin(\omega t)") + call xlabel("Time \tau (normalized)") + call ylabel("Amplitude \Psi (V)") + call plot(t, damped_sine, label="\alpha decay") + call plot(t, damped_cosine, label="\beta oscillation") + call legend() + call savefig("test_unicode_demo.png") + + ! README Promise #5: Scientific errorbar plots + print *, "Testing README scientific errorbar promise..." + real(wp), dimension(20) :: x_sci, y_sci, yerr, y_theory + x_sci = [(real(i-1, wp) * 0.5_wp, i=1, 20)] + y_sci = sin(x_sci) + 0.1_wp * ([(real(i, wp), i=1, 20)] - 10.0_wp) / 10.0_wp + yerr = 0.1_wp + y_theory = sin(x_sci) + + call figure(figsize=[8.0_wp, 6.0_wp]) + call errorbar(x_sci, y_sci, yerr=yerr, marker='o', label='Experimental data') + call plot(x_sci, y_theory, label='Theory', linestyle='-') + call legend() + call savefig("test_scientific_plot.png") + + print *, "All README promises tested successfully!" + +end program test_readme_promises \ No newline at end of file