From 634fead8cebca2df098d40e4fa81ad0636b31106 Mon Sep 17 00:00:00 2001 From: retropikzel Date: Thu, 25 Dec 2025 17:29:38 +0200 Subject: [PATCH] Bring in spite --- retropikzel/spite.scm | 386 +++++++++++++++++++++ retropikzel/spite.sld | 36 ++ retropikzel/spite/README.md | 202 +++++++++++ retropikzel/spite/test.scm | 57 +++ test-resources/charmap-cellphone_black.png | Bin 0 -> 1597 bytes test-resources/icons.png | Bin 0 -> 11306 bytes 6 files changed, 681 insertions(+) create mode 100644 retropikzel/spite.scm create mode 100644 retropikzel/spite.sld create mode 100644 retropikzel/spite/README.md create mode 100644 retropikzel/spite/test.scm create mode 100644 test-resources/charmap-cellphone_black.png create mode 100644 test-resources/icons.png diff --git a/retropikzel/spite.scm b/retropikzel/spite.scm new file mode 100644 index 0000000..b7babd0 --- /dev/null +++ b/retropikzel/spite.scm @@ -0,0 +1,386 @@ +(define spite-inited? #f) +(define started? #f) +(define exit? #f) +(define scale-x 1.0) +(define scale-y 1.0) +(define events (list)) +(define current-bitmap-font #f) +(define current-line-size 1) +(define-c-library sdl2* + '("SDL2/SDL.h") + "SDL2-2.0" + `((additional-paths ("retropikzel/spite" + "snow/retropikzel/spite")) + (additional-versions ("0")))) +(define-c-library sdl2-image* + '("SDL2/SDL_image.h") + "SDL2_image-2.0" + `((additional-paths ("retropikzel/spite" + "snow/retropikzel/spite")) + (additional-versions ("0")))) + +(define-c-procedure sdl-init sdl2* 'SDL_Init 'int '(int)) +(define-c-procedure sdl-get-window-flags sdl2* 'SDL_GetWindowFlags 'int '(pointer)) +(define-c-procedure sdl-create-window sdl2* 'SDL_CreateWindow 'pointer '(pointer int int int int int)) +(define-c-procedure sdl-create-renderer sdl2* 'SDL_CreateRenderer 'pointer '(pointer int int)) +(define-c-procedure sdl-render-setlogial-size sdl2* 'SDL_RenderSetLogicalSize 'int '(pointer int int)) +(define-c-procedure sdl-render-set-integer-scale sdl2* 'SDL_RenderSetIntegerScale 'int '(pointer int)) +(define-c-procedure sdl-set-render-draw-color sdl2* 'SDL_SetRenderDrawColor 'int '(pointer int int int int)) +(define-c-procedure sdl-render-clear sdl2* 'SDL_RenderClear 'int '(pointer)) +(define-c-procedure sdl-render-present sdl2* 'SDL_RenderPresent 'void '(pointer)) +(define-c-procedure sdl-get-key-from-scancode sdl2* 'SDL_GetKeyFromScancode 'int '(int)) +(define-c-procedure sdl-get-key-name sdl2* 'SDL_GetKeyName 'pointer '(int)) +(define-c-procedure sdl-poll-event sdl2* 'SDL_PollEvent 'int '(pointer)) +(define-c-procedure sdl-img-load-texture sdl2-image* 'IMG_LoadTexture 'pointer '(pointer pointer)) +(define-c-procedure sdl-render-copy sdl2* 'SDL_RenderCopy 'int '(pointer pointer pointer pointer)) +(define-c-procedure sdl-render-draw-line sdl2* 'SDL_RenderDrawLine 'int '(pointer int int int int)) +(define-c-procedure sdl-render-draw-rect sdl2* 'SDL_RenderDrawRect 'int '(pointer pointer)) +(define-c-procedure sdl-render-fill-rect sdl2* 'SDL_RenderFillRect 'int '(pointer pointer)) +(define-c-procedure sdl-render-set-scale sdl2* 'SDL_RenderSetScale 'int '(pointer float float)) +(define-c-procedure sdl-render-fill-rect sdl2* 'SDL_RenderFillRect 'int '(pointer pointer)) +(define-c-procedure sdl-create-texture-from-surface sdl2* 'SDL_CreateTextureFromSurface 'pointer '(pointer pointer)) +(define-c-procedure sdl-set-window-resizable sdl2* 'SDL_SetWindowResizable 'void '(pointer int)) +(define-c-procedure sdl-render-get-scale sdl2* 'SDL_RenderGetScale 'void '(pointer pointer pointer)) + +(define window* #f) +(define renderer* #f) +(define event* (make-c-bytevector 4000)) +(define draw-rect* (make-c-bytevector (* (c-type-size 'int) 4))) +(define draw-slice-rect* (make-c-bytevector (* (c-type-size 'int) 4))) +(define null* (make-c-null)) + +(define update-procedure #f) +(define draw-procedure #f) + +(define main-loop-start-time 0) +(define delta-time 0) +(define main-loop + (lambda () + (set! main-loop-start-time (current-jiffy)) + (sdl2-events-get) + (update-procedure delta-time (poll-events!)) + (render-clear) + (draw-procedure) + (render-present) + (set! delta-time (/ (- (current-jiffy) main-loop-start-time) (jiffies-per-second))) + (unless exit? (main-loop)))) + +(define sdl2-event->spite-event + (lambda (event) + (let ((type (c-bytevector-sint-ref event 0 (native-endianness) (c-type-size 'int)))) + (cond + ((= type 256) + (let ((type 'quit)) + (list (cons 'type type)))) + ((or (= type 768) (= type 769)) + (let* + ((type (if (= type 768) 'key-down 'key-up)) + (scancode (c-bytevector-sint-ref event + (+ (* (c-type-size 'int) 3) + (* (c-type-size 'uint8) 4)) + (native-endianness) + (c-type-size 'int))) + (keycode (sdl-get-key-from-scancode scancode)) + (key (c-utf8->string (sdl-get-key-name keycode))) + (repeat? (= (c-bytevector-u8-ref + event + (+ (* (c-type-size 'int) 3) + (c-type-size 'uint8))) + 1))) + (list (cons 'type type) + (cons 'key key) + (cons 'scancode scancode) + (cons 'repeat? repeat?)))) + ((= type 1024) + (let ((type 'mouse-motion) + (x (c-bytevector-sint-ref event + (+ (* (c-type-size 'int) 5)) + (native-endianness) + (c-type-size 'int))) + (y (c-bytevector-sint-ref event + (+ (* (c-type-size 'int) 6)) + (native-endianness) + (c-type-size 'int)))) + (list (cons 'type type) + (cons 'x x) + (cons 'y y)))) + ((or (= type 1025) (= type 1026)) + (let ((type (if (= type 1025) 'mouse-button-down 'mouse-button-up)) + (x (c-bytevector-sint-ref event + (+ (* (c-type-size 'int) 4) + (* (c-type-size 'uint8) 4)) + (native-endianness) + (c-type-size 'int))) + (y (c-bytevector-sint-ref event + (+ (* (c-type-size 'int) 4) + (* (c-type-size 'uint8) 4) + (c-type-size 'int)) + (native-endianness) + (c-type-size 'int))) + (button (c-bytevector-sint-ref event + (+ (* (c-type-size 'uint32) 4)) + (native-endianness) + (c-type-size 'uint8))) + (clicks (c-bytevector-sint-ref event + (+ (* (c-type-size 'uint32) 4) + (* (c-type-size 'uint8) 2)) + (native-endianness) + (c-type-size 'uint8)))) + (list (cons 'type type) + (cons 'x x) + (cons 'y y) + (cons 'button button) + (cons 'clicks clicks)))) + (else + (list (cons 'type 'sdl2-event) + (cons 'sdl2-type-number type))))))) + +(define sdl2-events-get + (lambda () + (let ((poll-result (sdl-poll-event event*))) + (cond + ((= poll-result 1) + (let ((event (sdl2-event->spite-event event*))) + + (cond ((equal? (cdr (assoc 'type event)) 'quit) (set! exit? #t))) + (push-event event) + (sdl2-events-get))))))) + +(define render-clear + (lambda () + (sdl-set-render-draw-color renderer* 255 255 255 255) + (sdl-render-clear renderer*))) + +(define render-present + (lambda () + (sdl-render-present renderer*))) + +(define-record-type image + (make-image pointer path) + image? + (pointer image-pointer) + (path image-path)) + +(define load-image + (lambda (path) + (when (not spite-inited?) (error "Can not load images until spite is inited." path)) + (when (not (string? path)) (error "Load path must be string" path)) + (when (not (file-exists? path)) (error (string-append "Could not load image, no such file: " path))) + (make-image (sdl-img-load-texture renderer* (string->c-utf8 path)) path))) + +(define draw-image + (lambda (image x y width height) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 0) x (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 1) y (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 2) width (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 3) height (native-endianness) (c-type-size 'int)) + (sdl-render-copy renderer* (image-pointer image) (make-c-null) draw-rect*))) + +(define draw-image-slice + (lambda (image x y width height slice-x slice-y slice-width slice-height) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 0) x (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 1) y (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 2) width (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 3) height (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-slice-rect* (* (c-type-size 'int) 0) slice-x (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-slice-rect* (* (c-type-size 'int) 1) slice-y (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-slice-rect* (* (c-type-size 'int) 2) slice-width (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-slice-rect* (* (c-type-size 'int) 3) slice-height (native-endianness) (c-type-size 'int)) + (sdl-render-copy renderer* (image-pointer image) draw-slice-rect* draw-rect*))) + +(define (set-draw-color r g b . a) + (sdl-set-render-draw-color renderer* r g b (if (null? a) 255 (car a)))) + +(define (set-line-size size) + (set! current-line-size size) + (sdl-render-set-scale renderer* (inexact (/ size 1)) (inexact (/ size 1)))) + +(define (draw-point x y) + (sdl-render-draw-line renderer* + (exact (round (/ x current-line-size))) + (exact (round (/ y current-linesize))) + (exact (round (/ x current-line-size))) + (exact (round (/ y current-line-size))))) + +(define (draw-line x1 y1 x2 y2) + (sdl-render-draw-line renderer* + (exact (round (/ x1 current-line-size))) + (exact (round (/ y1 current-line-size))) + (exact (round (/ x2 current-line-size))) + (exact (round (/ y2 current-line-size))))) + +(define (draw-rectangle x y width height) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 0) x (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 1) y (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 2) width (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 3) height (native-endianness) (c-type-size 'int)) + (sdl-render-draw-rect renderer* draw-rect*)) + +(define (fill-rectangle x y width height) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 0) x (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 1) y (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 2) width (native-endianness) (c-type-size 'int)) + (c-bytevector-sint-set! draw-rect* (* (c-type-size 'int) 3) height (native-endianness) (c-type-size 'int)) + (sdl-render-fill-rect renderer* draw-rect*)) + +(define (spite-option-set! name . value) + (cond + ((equal? name 'allow-window-resizing) + (cond + ((equal? value '(#t)) + (sdl-set-window-resizable window* 1)) + ((equal? value '(#f)) + (sdl-set-window-resizable window* 0)) + (else (error "Wrong option value for 'allow-window-resizing, must be #t or #f" + value)))) + ((equal? name 'renderer-size) + (if (and (= (length value) 2) + (number? (car value)) + (number? (cadr value))) + (sdl-render-setlogial-size renderer* (car value) (cadr value)) + (error "Wrong option value for renderer-size, must be two numbers"))) + (else (error "No such option!" name)))) + +;; TODO Move to options, add spite-option-get +(define spite-renderer-scale-get + (lambda () + (let ((x (make-c-bytevector (c-type-size 'float))) + (y (make-c-bytevector (c-type-size 'float)))) + (sdl-render-get-scale renderer* x y) + (list (cons 'x (c-bytevector-ieee-single-ref x 0 (native-endianness))) + (cons 'y (c-bytevector-ieee-single-ref y 0 (native-endianness))))))) + +(define spite-start + (lambda (new-update-procedure new-draw-procedure) + (set! update-procedure new-update-procedure) + (set! draw-procedure new-draw-procedure) + (cond + ((not started?) + (set! started? #t) + (main-loop))))) + +(define spite-init + (lambda (title width height) + (cond + ((not started?) + (sdl-init 32) + (set! window* (sdl-create-window (string->c-utf8 title) 0 0 width height 4)) + (set! renderer* (sdl-create-renderer window* -1 2)) + (sdl-render-setlogial-size renderer* width height) + (sdl-render-set-integer-scale renderer* 1) + (render-clear) + (render-present) + (set! spite-inited? #t))))) + +(define poll-events! + (lambda () + (let ((events-copy (list-copy events))) + (set! events (list)) + events-copy))) + +(define wait-for-event! + (lambda () + (if (not (= (length events) 0)) + (poll-events!) + (wait-for-event!)))) + +(define push-event + (lambda (event) + (set! events (append events (list event))))) + +(define clear-events! + (lambda () + (set! events (list)))) + +(define-record-type bitmap-font + (internal-make-bitmap-font data) + bitmap-font? + (data bitmap-font-data)) + +(define (bitmap-font-get key bitmap) + (cdr (assoc key (bitmap-font-data bitmap)))) + +(define-record-type bitmap-char + (make-bitmap-char char x y) + bitmap-char? + (char bitmap-char-char) + (x bitmap-char-x) + (y bitmap-char-y)) + +(define (make-bitmap-font image character-width character-height draw-width draw-height character-lines) + (let* ((line-items-count (string-length (car character-lines))) + (characters (apply string-append character-lines)) + (index -1) + (character-indexes (list)) + (character-positions + (map (lambda (character) + (set! index (+ index 1)) + (set! character-indexes (append character-indexes (list character index))) + (list character + (* (modulo index line-items-count) + character-width) + (* (floor (/ index line-items-count)) + character-height))) + (string->list characters)))) + (internal-make-bitmap-font + `((image . ,image) + (character-width . ,character-width) + (character-height . ,character-height) + (character-draw-width . ,draw-width) + (character-draw-height . ,draw-height) + (line-items-count . ,line-items-count) + (characters . ,characters) + (character-indexes . ,character-indexes) + (character-positions . ,character-positions))))) + +(define (set-bitmap-font font) + (set! current-bitmap-font font)) + +(define (make-bitmap-text text font) + (map + (lambda (c) + (make-bitmap-char + c + (cadr (assq c (bitmap-font-get 'character-positions font))) + (cadr (cdr (assq c (bitmap-font-get 'character-positions font)))))) + (string->list text))) + + +(define draw-bitmap-text + (lambda (text x y) + (when (not current-bitmap-font) + (error "Current bitmap font not set, use make-bitmap-font and set-bitmap-font")) + (let ((offset-x x)) + (for-each + (lambda (bitmap-char) + (draw-image-slice (bitmap-font-get 'image current-bitmap-font) + offset-x + y + (bitmap-font-get 'character-draw-width current-bitmap-font) + (bitmap-font-get 'character-draw-height current-bitmap-font) + (bitmap-char-x bitmap-char) + (bitmap-char-y bitmap-char) + (bitmap-font-get 'character-width current-bitmap-font) + (bitmap-font-get 'character-height current-bitmap-font)) + (set! offset-x (+ offset-x (bitmap-font-get 'character-draw-width current-bitmap-font)))) + (make-bitmap-text text current-bitmap-font))))) + +(define (draw-polygon points) + (let* ((first-point #f) + (previous-point #f)) + (for-each + (lambda (point) + (when (not first-point) + (set! first-point point) + (set! previous-point first-point)) + (draw-line (car previous-point) + (cdr previous-point) + (car point) + (cdr point)) + (set! previous-point point)) + points) + (when first-point + (draw-line (car previous-point) + (cdr previous-point) + (car first-point) + (cdr first-point))))) diff --git a/retropikzel/spite.sld b/retropikzel/spite.sld new file mode 100644 index 0000000..71a4bda --- /dev/null +++ b/retropikzel/spite.sld @@ -0,0 +1,36 @@ +(define-library + (retropikzel spite) + (import (scheme base) + (scheme write) + (scheme complex) + (scheme process-context) + (scheme file) + (scheme load) + (scheme time) + (foreign c)) + (export spite-init + spite-start + spite-option-set! + + load-image + image? + image-path + + draw-image + draw-image-slice + + set-draw-color + draw-point + draw-line + draw-rectangle + fill-rectangle + draw-circle + draw-polygon + + push-event + clear-events! + + make-bitmap-font + set-bitmap-font + draw-bitmap-text) + (include "spite.scm")) diff --git a/retropikzel/spite/README.md b/retropikzel/spite/README.md new file mode 100644 index 0000000..40568c0 --- /dev/null +++ b/retropikzel/spite/README.md @@ -0,0 +1,202 @@ +Game library inspired by some other game library named after emotion built on +top of [(foreign c)](https://sr.ht/~retropikzel/foreign-c/). + +Please note that Spite is currently in **alpha** stage. + + +[Issue tracker](https://todo.sr.ht/~retropikzel/Spite) + +[Mailing lists](https://sr.ht/~retropikzel/Spite/lists) + +[Source](https://git.sr.ht/~retropikzel/spite) + + +## Documentation - Spite + + + +(**spite-init** title width height) + +This needs to be called first. title is a string you want to be used as +the game window title. width and height are the desired window size. + +It will initialize spite, loading SDL2 libraries and such and then opens a +window for you. + +The renderer size is set to same size as window size, you can change it with: + + (spite-option-set! 'renderer-size width height) + + + +(**spite-start** update-procedure draw-procedure) + +Starts the update and draw loop. Needs to be called for anything to happen. + +update-procedure is the procedure which is run in the main loop before the +draw procedure. Where the logic happens. draw-procedure is where you should +do all your drawing. + + + +(**spite-option-set! name . value) + +Sets different options of Spite. name is the name of option. value is the +value or values of the option. + +Options and possible values: + +- allow-window-resizing + - #t #f +- renderer-size + - Width and height + + + +(**load-image** path) + +Loads image from the path, supported filetypes are same as supported by +SDLimage [https://wiki.libsdl.org/SDL2image/FrontPage](https://wiki.libsdl.org/SDL2image/FrontPage) + +Returns an image record. Which can be used with draw-image and +draw-image-slice. + + + +(**image?** object) + +Returns #t if object is image, otherwise #f. + + + +(**draw-image** image-index x y width height) + +Draws given image of image-index, returned by **load-image**. To position x +and y, left top corner. Size of width and height. + + + +(**draw-image-slice** image-index x y width height slize-x slice-y slice-width slice-height) + +Draws given slice of image-index, returned by **load-image**. To position x +and y, left top corner. Size of width and height. Clipped from slice-x +and slice-y (top left corner) of size slice-width slice-height. + + + +(**make-color** r g b . a) + +Makes a color record of red(r) green(g) blue(b) and optionally of alpha(a). +If a is not given it defaults to 255. + + + +(**color?** object) + +Returns #t of object is color, otherwise #f. + + + +(**color:r** color) + +Return red of color. + + + +(**color:r!** color r) + +Set the red of color + + + +(**color:g** color) + +Return green of color. + + + +(**color:g!** color g) + +Set the green of color + + + +(**color:b** color) + +Return blue of color. + + + +(**color:b!** color b) + +Set the blue of color + + + +(**color:a** color) + +Return alpha of color. + + + +(**color:a!** color a) + +Set the alpha of color + + + +(**draw-point** x y size color) + +Draws a point of size and color on x and y. + + + +(**draw-line** x1 y1 x2 y2 line-size color) + +Draws a line from point x1 y1 to x2 y2 with line-size of color. + + + +(**make-event** type data) + +Make new event with given type and data. + + + +(**push-event** type data) + +Make and push event of given type and data. The type should be a symbol, and +data can be anything. + + + +(**event:type** event) + +Returns the type of the event. + + + +(**event:data** event) + +Returns the data of event. + + + +(**clear-events!**) + +Removes all events in the event queue. + + + +(**make-bitmap-font** image character-width character-height draw-width draw-height characters) + + + +(**draw-bitmap-text** text x y font) + + + +(**draw-polygon** x y polygon) + +Draw given polygon at position x y. + diff --git a/retropikzel/spite/test.scm b/retropikzel/spite/test.scm new file mode 100644 index 0000000..b6a8c2b --- /dev/null +++ b/retropikzel/spite/test.scm @@ -0,0 +1,57 @@ + +(spite-init "Spite Test" 800 800) + +(define player-x 100) +(define player-y 100) + +(define font-image (load-image "test-resources/charmap-cellphone_black.png")) + +(define black '(0 0 0)) +(define blue '(0 0 255)) + +(define character-width 7) +(define character-height 9) +(define draw-width 14) +(define draw-height 18) +(define character-lines (list " !\"#¤%&/()*+,-./01" + "23456789:;<=>?@ABC" + "DEFGHIJKLMNOPQRSTU" + "VWXYZ[\\]^_´abcdefg" + "hijklmnopqrstuvwxy" + "z{|}~")) +(define font (make-bitmap-font font-image + character-width + character-height + draw-width + draw-height + character-lines)) +(set-bitmap-font font) + +(define update + (lambda (delta-time events) + (for-each + (lambda (event) + (when (symbol=? (cdr (assoc 'type event)) 'key-down) + (let ((key (cdr (assoc 'key event)))) + (when (string=? key "W") (set! player-y (- player-y 5))) + (when (string=? key "A") (set! player-x (- player-x 5))) + (when (string=? key "S") (set! player-y (+ player-y 5))) + (when (string=? key "D") (set! player-x (+ player-x 5))) + ))) + events) + #t)) + +(define draw + (lambda () + (draw-bitmap-text "Cool beans!" 100 100) + (apply set-draw-color black) + (draw-line 50 50 100 100) + (apply set-draw-color blue) + (draw-line 150 150 200 200) + (apply set-draw-color black) + (draw-rectangle player-x player-y 64 64) + (fill-rectangle (+ player-x 32) (+ player-y 32) 16 16) + (draw-polygon '((300 . 332) (332 . 400) (432 . 400))) + )) + +(spite-start update draw) diff --git a/test-resources/charmap-cellphone_black.png b/test-resources/charmap-cellphone_black.png new file mode 100644 index 0000000000000000000000000000000000000000..85f9324bd558d0b9948c341ccc78b998cc293b1d GIT binary patch literal 1597 zcmV-D2EzG?P)$V8|qiFtkJboR2qObA&=$xG$ybw*s6Ul;jTDh5=2n+U}g5T;p3^!({ z=uC2ejso6@{&9P*0zB>=NlT^i#*Y#n;)i6;9IAuY42{Jl3Ic}Jv;z!4E0x3wbkJGa z@wsE*LwO{J!mpFXW(?P%RrK(DnQf*W04d=egBb=Xo<26Ge}TeIy6Sgo++yCBQmVMIm1Ts`ZdHd(8MlKjn_a zqM}>HXsE{fW>Dez_!4sO0TiSzdPm9t7u9{M^@YH96X;W^?{<~6md&C*(V!k6O6y1- zmFEgeSJErH%dqygQ1z?u^!?0B2~dPr1Y;e#3MO(YTgz=SJfiPVHGDEWlGCQk+z9P~+4`j$8JDXIZ?5a3bwYgPzB5Zyfz$y|&IEpINLyk`0 zM`%<|RE48xSB=g_S#-vt*y_hHh@3#l4xQedVND&t3WP=-#GFO3YMe^c*{l%t5y+V` zIa%)!on;xo8opyprbidoh;C{V$eP8KLaMwv5x~;4?J0bz%Z!?352h32jdmF-MlZp3 z>;Wp?kAy?b$9Pa_9F6K>8F#&gIsp6)nzJIMlU1_J=x6f3f!9+8U><8%pNgPo7{Zp4Zp3!`6|b|`PjwJKVW>+u zRrji#dl95~^M&tRvIn@Zy_T!??Ep5t_!Jp^_VaV}*{3%9*_M+iC9`tF8lbF9b(*Ix zO^41}w^OP*G}9SiC>kk@&Z5Op&7GC4lfi7j`S;}> zpgVUXg*%a$rN3ElEA%UzxbmLWU0E<$-t7L50}O*ihUuPVC1peq^6IL$SMQI=uyUw8 zltowMB$f`<=B%7lGmpLhQX;;-v41tJ6N(q1kxRfu^xbMW1YQODpLrb@%;*Zdl6Kz?9J{@XMmgGbvboQ4uF;$ zQH}qhBD&9pL5& v++6*g0k#9&9D$pwzcavgfSV(5bM^lKl#Y;SuU;x700000NkvXXu0mjfrCkx{ literal 0 HcmV?d00001 diff --git a/test-resources/icons.png b/test-resources/icons.png new file mode 100644 index 0000000000000000000000000000000000000000..c3719b30922ee7405372697974db9f28b1eb43a1 GIT binary patch literal 11306 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 dEc|+o5z*snn2jf`{%7YSL|I#@O2IPh{{i