diff --git a/Makefile b/Makefile index 05d64d3bc8..baa2a6b92e 100644 --- a/Makefile +++ b/Makefile @@ -207,15 +207,45 @@ gfx/pokemon/girafarig/front.animated.tilemap: gfx/pokemon/girafarig/front.2bpp g tools/pokemon_animation_graphics --girafarig -t $@ $^ -### Misc file-specific graphics rules +### Pokemon and trainer sprite rules + +gfx/pokemon/%/back.2bpp: rgbgfx += --columns +gfx/pokemon/%/back.2bpp: gfx/pokemon/%/back.png gfx/pokemon/%/normal.gbcpal + $(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $< +gfx/pokemon/%/front.2bpp: gfx/pokemon/%/front.png gfx/pokemon/%/normal.gbcpal + $(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $< +gfx/pokemon/%/normal.gbcpal: gfx/pokemon/%/front.gbcpal gfx/pokemon/%/back.gbcpal + tools/gbcpal $(tools/gbcpal) $@ $^ + +gfx/trainers/%.2bpp: rgbgfx += --columns +gfx/trainers/%.2bpp: gfx/trainers/%.png gfx/trainers/%.gbcpal + $(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $< + +# Egg does not have a back sprite, so it only uses front.gbcpal +gfx/pokemon/egg/front.2bpp: gfx/pokemon/egg/front.png gfx/pokemon/egg/front.gbcpal +gfx/pokemon/egg/front.2bpp: rgbgfx += --colors gbc:$(word 2,$^) -gfx/pokemon/%/back.2bpp: rgbgfx += --columns --colors embedded -gfx/pokemon/%/front.2bpp: rgbgfx += --colors embedded +# Unown letters share one normal.pal, so they don't already build each normal.gbcpal +$(foreach png, $(wildcard gfx/pokemon/unown_*/front.png),\ + $(eval $(png:.png=.2bpp): $(png) $(png:front.png=normal.gbcpal))) +gfx/pokemon/unown_%/front.2bpp: rgbgfx += --colors gbc:$(@:front.2bpp=normal.gbcpal) -gfx/trainers/%.2bpp: rgbgfx += --columns --colors embedded + +### Misc file-specific graphics rules gfx/pokemon/egg/unused_front.2bpp: rgbgfx += --columns +gfx/pokemon/spearow/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/fearow/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/farfetch_d/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/hitmonlee/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/scyther/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/jynx/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/porygon/normal.gbcpal: tools/gbcpal += --reverse +gfx/pokemon/porygon2/normal.gbcpal: tools/gbcpal += --reverse + +gfx/trainers/swimmer_m.gbcpal: tools/gbcpal += --reverse + gfx/new_game/shrink1.2bpp: rgbgfx += --columns gfx/new_game/shrink2.2bpp: rgbgfx += --columns @@ -307,15 +337,16 @@ gfx/mobile/stadium2_n64.2bpp: tools/gfx += --trim-whitespace %.2bpp: %.png $(RGBGFX) $(rgbgfx) -o $@ $< $(if $(tools/gfx),\ - tools/gfx $(tools/gfx) -o $@ $@) + tools/gfx $(tools/gfx) -o $@ $@ || $$($(RM) $@ && false)) %.1bpp: %.png $(RGBGFX) $(rgbgfx) --depth 1 -o $@ $< $(if $(tools/gfx),\ - tools/gfx $(tools/gfx) --depth 1 -o $@ $@) + tools/gfx $(tools/gfx) --depth 1 -o $@ $@ || $$($(RM) $@ && false)) %.gbcpal: %.png - $(RGBGFX) --colors embedded -p $@ $< + $(RGBGFX) -p $@ $< + tools/gbcpal $(tools/gbcpal) $@ $@ || $$($(RM) $@ && false) %.dimensions: %.png tools/png_dimensions $< $@ diff --git a/data/pokemon/palettes.asm b/data/pokemon/palettes.asm index 5cefbd38ea..1ea96302d3 100644 --- a/data/pokemon/palettes.asm +++ b/data/pokemon/palettes.asm @@ -1,11 +1,11 @@ PokemonPalettes: ; entries correspond to Pokémon species, two apiece -; Each front.gbcpal is generated from the corresponding .png, and +; Each normal.gbcpal is generated from the corresponding .png, and ; only the middle two colors are included, not black or white. ; Shiny palettes are defined directly, not generated. - ; 2 middle palettes, front and shiny, with 2 colors each + ; 2 middle palettes, normal and shiny, with 2 colors each table_width PAL_COLOR_SIZE * 2 * 2, PokemonPalettes ; 000 @@ -15,507 +15,507 @@ PokemonPalettes: RGB 30, 22, 17 RGB 16, 14, 19 -INCBIN "gfx/pokemon/bulbasaur/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/bulbasaur/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/bulbasaur/shiny.pal" -INCBIN "gfx/pokemon/ivysaur/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ivysaur/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ivysaur/shiny.pal" -INCBIN "gfx/pokemon/venusaur/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/venusaur/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/venusaur/shiny.pal" -INCBIN "gfx/pokemon/charmander/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/charmander/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/charmander/shiny.pal" -INCBIN "gfx/pokemon/charmeleon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/charmeleon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/charmeleon/shiny.pal" -INCBIN "gfx/pokemon/charizard/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/charizard/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/charizard/shiny.pal" -INCBIN "gfx/pokemon/squirtle/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/squirtle/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/squirtle/shiny.pal" -INCBIN "gfx/pokemon/wartortle/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/wartortle/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/wartortle/shiny.pal" -INCBIN "gfx/pokemon/blastoise/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/blastoise/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/blastoise/shiny.pal" -INCBIN "gfx/pokemon/caterpie/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/caterpie/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/caterpie/shiny.pal" -INCBIN "gfx/pokemon/metapod/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/metapod/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/metapod/shiny.pal" -INCBIN "gfx/pokemon/butterfree/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/butterfree/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/butterfree/shiny.pal" -INCBIN "gfx/pokemon/weedle/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/weedle/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/weedle/shiny.pal" -INCBIN "gfx/pokemon/kakuna/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kakuna/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kakuna/shiny.pal" -INCBIN "gfx/pokemon/beedrill/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/beedrill/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/beedrill/shiny.pal" -INCBIN "gfx/pokemon/pidgey/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pidgey/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pidgey/shiny.pal" -INCBIN "gfx/pokemon/pidgeotto/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pidgeotto/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pidgeotto/shiny.pal" -INCBIN "gfx/pokemon/pidgeot/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pidgeot/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pidgeot/shiny.pal" -INCBIN "gfx/pokemon/rattata/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/rattata/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/rattata/shiny.pal" -INCBIN "gfx/pokemon/raticate/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/raticate/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/raticate/shiny.pal" -INCBIN "gfx/pokemon/spearow/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/spearow/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/spearow/shiny.pal" -INCBIN "gfx/pokemon/fearow/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/fearow/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/fearow/shiny.pal" -INCBIN "gfx/pokemon/ekans/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ekans/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ekans/shiny.pal" -INCBIN "gfx/pokemon/arbok/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/arbok/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/arbok/shiny.pal" -INCBIN "gfx/pokemon/pikachu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pikachu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pikachu/shiny.pal" -INCBIN "gfx/pokemon/raichu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/raichu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/raichu/shiny.pal" -INCBIN "gfx/pokemon/sandshrew/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sandshrew/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sandshrew/shiny.pal" -INCBIN "gfx/pokemon/sandslash/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sandslash/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sandslash/shiny.pal" -INCBIN "gfx/pokemon/nidoran_f/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidoran_f/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidoran_f/shiny.pal" -INCBIN "gfx/pokemon/nidorina/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidorina/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidorina/shiny.pal" -INCBIN "gfx/pokemon/nidoqueen/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidoqueen/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidoqueen/shiny.pal" -INCBIN "gfx/pokemon/nidoran_m/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidoran_m/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidoran_m/shiny.pal" -INCBIN "gfx/pokemon/nidorino/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidorino/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidorino/shiny.pal" -INCBIN "gfx/pokemon/nidoking/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/nidoking/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/nidoking/shiny.pal" -INCBIN "gfx/pokemon/clefairy/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/clefairy/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/clefairy/shiny.pal" -INCBIN "gfx/pokemon/clefable/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/clefable/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/clefable/shiny.pal" -INCBIN "gfx/pokemon/vulpix/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/vulpix/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/vulpix/shiny.pal" -INCBIN "gfx/pokemon/ninetales/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ninetales/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ninetales/shiny.pal" -INCBIN "gfx/pokemon/jigglypuff/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/jigglypuff/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/jigglypuff/shiny.pal" -INCBIN "gfx/pokemon/wigglytuff/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/wigglytuff/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/wigglytuff/shiny.pal" -INCBIN "gfx/pokemon/zubat/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/zubat/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/zubat/shiny.pal" -INCBIN "gfx/pokemon/golbat/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/golbat/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/golbat/shiny.pal" -INCBIN "gfx/pokemon/oddish/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/oddish/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/oddish/shiny.pal" -INCBIN "gfx/pokemon/gloom/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/gloom/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/gloom/shiny.pal" -INCBIN "gfx/pokemon/vileplume/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/vileplume/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/vileplume/shiny.pal" -INCBIN "gfx/pokemon/paras/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/paras/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/paras/shiny.pal" -INCBIN "gfx/pokemon/parasect/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/parasect/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/parasect/shiny.pal" -INCBIN "gfx/pokemon/venonat/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/venonat/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/venonat/shiny.pal" -INCBIN "gfx/pokemon/venomoth/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/venomoth/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/venomoth/shiny.pal" -INCBIN "gfx/pokemon/diglett/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/diglett/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/diglett/shiny.pal" -INCBIN "gfx/pokemon/dugtrio/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dugtrio/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dugtrio/shiny.pal" -INCBIN "gfx/pokemon/meowth/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/meowth/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/meowth/shiny.pal" -INCBIN "gfx/pokemon/persian/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/persian/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/persian/shiny.pal" -INCBIN "gfx/pokemon/psyduck/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/psyduck/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/psyduck/shiny.pal" -INCBIN "gfx/pokemon/golduck/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/golduck/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/golduck/shiny.pal" -INCBIN "gfx/pokemon/mankey/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mankey/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mankey/shiny.pal" -INCBIN "gfx/pokemon/primeape/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/primeape/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/primeape/shiny.pal" -INCBIN "gfx/pokemon/growlithe/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/growlithe/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/growlithe/shiny.pal" -INCBIN "gfx/pokemon/arcanine/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/arcanine/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/arcanine/shiny.pal" -INCBIN "gfx/pokemon/poliwag/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/poliwag/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/poliwag/shiny.pal" -INCBIN "gfx/pokemon/poliwhirl/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/poliwhirl/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/poliwhirl/shiny.pal" -INCBIN "gfx/pokemon/poliwrath/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/poliwrath/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/poliwrath/shiny.pal" -INCBIN "gfx/pokemon/abra/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/abra/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/abra/shiny.pal" -INCBIN "gfx/pokemon/kadabra/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kadabra/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kadabra/shiny.pal" -INCBIN "gfx/pokemon/alakazam/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/alakazam/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/alakazam/shiny.pal" -INCBIN "gfx/pokemon/machop/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/machop/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/machop/shiny.pal" -INCBIN "gfx/pokemon/machoke/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/machoke/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/machoke/shiny.pal" -INCBIN "gfx/pokemon/machamp/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/machamp/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/machamp/shiny.pal" -INCBIN "gfx/pokemon/bellsprout/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/bellsprout/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/bellsprout/shiny.pal" -INCBIN "gfx/pokemon/weepinbell/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/weepinbell/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/weepinbell/shiny.pal" -INCBIN "gfx/pokemon/victreebel/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/victreebel/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/victreebel/shiny.pal" -INCBIN "gfx/pokemon/tentacool/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tentacool/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tentacool/shiny.pal" -INCBIN "gfx/pokemon/tentacruel/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tentacruel/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tentacruel/shiny.pal" -INCBIN "gfx/pokemon/geodude/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/geodude/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/geodude/shiny.pal" -INCBIN "gfx/pokemon/graveler/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/graveler/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/graveler/shiny.pal" -INCBIN "gfx/pokemon/golem/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/golem/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/golem/shiny.pal" -INCBIN "gfx/pokemon/ponyta/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ponyta/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ponyta/shiny.pal" -INCBIN "gfx/pokemon/rapidash/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/rapidash/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/rapidash/shiny.pal" -INCBIN "gfx/pokemon/slowpoke/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/slowpoke/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/slowpoke/shiny.pal" -INCBIN "gfx/pokemon/slowbro/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/slowbro/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/slowbro/shiny.pal" -INCBIN "gfx/pokemon/magnemite/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magnemite/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magnemite/shiny.pal" -INCBIN "gfx/pokemon/magneton/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magneton/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magneton/shiny.pal" -INCBIN "gfx/pokemon/farfetch_d/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/farfetch_d/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/farfetch_d/shiny.pal" -INCBIN "gfx/pokemon/doduo/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/doduo/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/doduo/shiny.pal" -INCBIN "gfx/pokemon/dodrio/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dodrio/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dodrio/shiny.pal" -INCBIN "gfx/pokemon/seel/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/seel/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/seel/shiny.pal" -INCBIN "gfx/pokemon/dewgong/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dewgong/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dewgong/shiny.pal" -INCBIN "gfx/pokemon/grimer/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/grimer/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/grimer/shiny.pal" -INCBIN "gfx/pokemon/muk/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/muk/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/muk/shiny.pal" -INCBIN "gfx/pokemon/shellder/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/shellder/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/shellder/shiny.pal" -INCBIN "gfx/pokemon/cloyster/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/cloyster/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/cloyster/shiny.pal" -INCBIN "gfx/pokemon/gastly/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/gastly/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/gastly/shiny.pal" -INCBIN "gfx/pokemon/haunter/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/haunter/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/haunter/shiny.pal" -INCBIN "gfx/pokemon/gengar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/gengar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/gengar/shiny.pal" -INCBIN "gfx/pokemon/onix/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/onix/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/onix/shiny.pal" -INCBIN "gfx/pokemon/drowzee/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/drowzee/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/drowzee/shiny.pal" -INCBIN "gfx/pokemon/hypno/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hypno/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hypno/shiny.pal" -INCBIN "gfx/pokemon/krabby/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/krabby/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/krabby/shiny.pal" -INCBIN "gfx/pokemon/kingler/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kingler/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kingler/shiny.pal" -INCBIN "gfx/pokemon/voltorb/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/voltorb/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/voltorb/shiny.pal" -INCBIN "gfx/pokemon/electrode/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/electrode/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/electrode/shiny.pal" -INCBIN "gfx/pokemon/exeggcute/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/exeggcute/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/exeggcute/shiny.pal" -INCBIN "gfx/pokemon/exeggutor/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/exeggutor/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/exeggutor/shiny.pal" -INCBIN "gfx/pokemon/cubone/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/cubone/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/cubone/shiny.pal" -INCBIN "gfx/pokemon/marowak/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/marowak/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/marowak/shiny.pal" -INCBIN "gfx/pokemon/hitmonlee/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hitmonlee/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hitmonlee/shiny.pal" -INCBIN "gfx/pokemon/hitmonchan/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hitmonchan/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hitmonchan/shiny.pal" -INCBIN "gfx/pokemon/lickitung/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/lickitung/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/lickitung/shiny.pal" -INCBIN "gfx/pokemon/koffing/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/koffing/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/koffing/shiny.pal" -INCBIN "gfx/pokemon/weezing/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/weezing/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/weezing/shiny.pal" -INCBIN "gfx/pokemon/rhyhorn/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/rhyhorn/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/rhyhorn/shiny.pal" -INCBIN "gfx/pokemon/rhydon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/rhydon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/rhydon/shiny.pal" -INCBIN "gfx/pokemon/chansey/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/chansey/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/chansey/shiny.pal" -INCBIN "gfx/pokemon/tangela/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tangela/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tangela/shiny.pal" -INCBIN "gfx/pokemon/kangaskhan/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kangaskhan/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kangaskhan/shiny.pal" -INCBIN "gfx/pokemon/horsea/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/horsea/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/horsea/shiny.pal" -INCBIN "gfx/pokemon/seadra/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/seadra/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/seadra/shiny.pal" -INCBIN "gfx/pokemon/goldeen/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/goldeen/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/goldeen/shiny.pal" -INCBIN "gfx/pokemon/seaking/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/seaking/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/seaking/shiny.pal" -INCBIN "gfx/pokemon/staryu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/staryu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/staryu/shiny.pal" -INCBIN "gfx/pokemon/starmie/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/starmie/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/starmie/shiny.pal" -INCBIN "gfx/pokemon/mr__mime/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mr__mime/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mr__mime/shiny.pal" -INCBIN "gfx/pokemon/scyther/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/scyther/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/scyther/shiny.pal" -INCBIN "gfx/pokemon/jynx/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/jynx/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/jynx/shiny.pal" -INCBIN "gfx/pokemon/electabuzz/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/electabuzz/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/electabuzz/shiny.pal" -INCBIN "gfx/pokemon/magmar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magmar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magmar/shiny.pal" -INCBIN "gfx/pokemon/pinsir/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pinsir/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pinsir/shiny.pal" -INCBIN "gfx/pokemon/tauros/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tauros/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tauros/shiny.pal" -INCBIN "gfx/pokemon/magikarp/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magikarp/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magikarp/shiny.pal" -INCBIN "gfx/pokemon/gyarados/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/gyarados/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/gyarados/shiny.pal" -INCBIN "gfx/pokemon/lapras/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/lapras/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/lapras/shiny.pal" -INCBIN "gfx/pokemon/ditto/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ditto/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ditto/shiny.pal" -INCBIN "gfx/pokemon/eevee/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/eevee/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/eevee/shiny.pal" -INCBIN "gfx/pokemon/vaporeon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/vaporeon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/vaporeon/shiny.pal" -INCBIN "gfx/pokemon/jolteon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/jolteon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/jolteon/shiny.pal" -INCBIN "gfx/pokemon/flareon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/flareon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/flareon/shiny.pal" -INCBIN "gfx/pokemon/porygon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/porygon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/porygon/shiny.pal" -INCBIN "gfx/pokemon/omanyte/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/omanyte/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/omanyte/shiny.pal" -INCBIN "gfx/pokemon/omastar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/omastar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/omastar/shiny.pal" -INCBIN "gfx/pokemon/kabuto/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kabuto/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kabuto/shiny.pal" -INCBIN "gfx/pokemon/kabutops/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kabutops/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kabutops/shiny.pal" -INCBIN "gfx/pokemon/aerodactyl/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/aerodactyl/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/aerodactyl/shiny.pal" -INCBIN "gfx/pokemon/snorlax/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/snorlax/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/snorlax/shiny.pal" -INCBIN "gfx/pokemon/articuno/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/articuno/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/articuno/shiny.pal" -INCBIN "gfx/pokemon/zapdos/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/zapdos/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/zapdos/shiny.pal" -INCBIN "gfx/pokemon/moltres/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/moltres/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/moltres/shiny.pal" -INCBIN "gfx/pokemon/dratini/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dratini/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dratini/shiny.pal" -INCBIN "gfx/pokemon/dragonair/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dragonair/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dragonair/shiny.pal" -INCBIN "gfx/pokemon/dragonite/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dragonite/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dragonite/shiny.pal" -INCBIN "gfx/pokemon/mewtwo/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mewtwo/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mewtwo/shiny.pal" -INCBIN "gfx/pokemon/mew/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mew/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mew/shiny.pal" -INCBIN "gfx/pokemon/chikorita/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/chikorita/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/chikorita/shiny.pal" -INCBIN "gfx/pokemon/bayleef/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/bayleef/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/bayleef/shiny.pal" -INCBIN "gfx/pokemon/meganium/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/meganium/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/meganium/shiny.pal" -INCBIN "gfx/pokemon/cyndaquil/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/cyndaquil/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/cyndaquil/shiny.pal" -INCBIN "gfx/pokemon/quilava/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/quilava/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/quilava/shiny.pal" -INCBIN "gfx/pokemon/typhlosion/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/typhlosion/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/typhlosion/shiny.pal" -INCBIN "gfx/pokemon/totodile/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/totodile/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/totodile/shiny.pal" -INCBIN "gfx/pokemon/croconaw/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/croconaw/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/croconaw/shiny.pal" -INCBIN "gfx/pokemon/feraligatr/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/feraligatr/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/feraligatr/shiny.pal" -INCBIN "gfx/pokemon/sentret/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sentret/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sentret/shiny.pal" -INCBIN "gfx/pokemon/furret/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/furret/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/furret/shiny.pal" -INCBIN "gfx/pokemon/hoothoot/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hoothoot/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hoothoot/shiny.pal" -INCBIN "gfx/pokemon/noctowl/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/noctowl/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/noctowl/shiny.pal" -INCBIN "gfx/pokemon/ledyba/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ledyba/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ledyba/shiny.pal" -INCBIN "gfx/pokemon/ledian/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ledian/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ledian/shiny.pal" -INCBIN "gfx/pokemon/spinarak/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/spinarak/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/spinarak/shiny.pal" -INCBIN "gfx/pokemon/ariados/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ariados/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ariados/shiny.pal" -INCBIN "gfx/pokemon/crobat/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/crobat/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/crobat/shiny.pal" -INCBIN "gfx/pokemon/chinchou/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/chinchou/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/chinchou/shiny.pal" -INCBIN "gfx/pokemon/lanturn/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/lanturn/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/lanturn/shiny.pal" -INCBIN "gfx/pokemon/pichu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pichu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pichu/shiny.pal" -INCBIN "gfx/pokemon/cleffa/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/cleffa/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/cleffa/shiny.pal" -INCBIN "gfx/pokemon/igglybuff/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/igglybuff/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/igglybuff/shiny.pal" -INCBIN "gfx/pokemon/togepi/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/togepi/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/togepi/shiny.pal" -INCBIN "gfx/pokemon/togetic/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/togetic/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/togetic/shiny.pal" -INCBIN "gfx/pokemon/natu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/natu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/natu/shiny.pal" -INCBIN "gfx/pokemon/xatu/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/xatu/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/xatu/shiny.pal" -INCBIN "gfx/pokemon/mareep/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mareep/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mareep/shiny.pal" -INCBIN "gfx/pokemon/flaaffy/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/flaaffy/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/flaaffy/shiny.pal" -INCBIN "gfx/pokemon/ampharos/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ampharos/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ampharos/shiny.pal" -INCBIN "gfx/pokemon/bellossom/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/bellossom/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/bellossom/shiny.pal" -INCBIN "gfx/pokemon/marill/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/marill/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/marill/shiny.pal" -INCBIN "gfx/pokemon/azumarill/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/azumarill/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/azumarill/shiny.pal" -INCBIN "gfx/pokemon/sudowoodo/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sudowoodo/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sudowoodo/shiny.pal" -INCBIN "gfx/pokemon/politoed/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/politoed/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/politoed/shiny.pal" -INCBIN "gfx/pokemon/hoppip/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hoppip/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hoppip/shiny.pal" -INCBIN "gfx/pokemon/skiploom/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/skiploom/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/skiploom/shiny.pal" -INCBIN "gfx/pokemon/jumpluff/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/jumpluff/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/jumpluff/shiny.pal" -INCBIN "gfx/pokemon/aipom/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/aipom/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/aipom/shiny.pal" -INCBIN "gfx/pokemon/sunkern/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sunkern/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sunkern/shiny.pal" -INCBIN "gfx/pokemon/sunflora/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sunflora/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sunflora/shiny.pal" -INCBIN "gfx/pokemon/yanma/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/yanma/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/yanma/shiny.pal" -INCBIN "gfx/pokemon/wooper/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/wooper/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/wooper/shiny.pal" -INCBIN "gfx/pokemon/quagsire/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/quagsire/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/quagsire/shiny.pal" -INCBIN "gfx/pokemon/espeon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/espeon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/espeon/shiny.pal" -INCBIN "gfx/pokemon/umbreon/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/umbreon/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/umbreon/shiny.pal" -INCBIN "gfx/pokemon/murkrow/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/murkrow/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/murkrow/shiny.pal" -INCBIN "gfx/pokemon/slowking/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/slowking/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/slowking/shiny.pal" -INCBIN "gfx/pokemon/misdreavus/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/misdreavus/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/misdreavus/shiny.pal" -INCLUDE "gfx/pokemon/unown/normal.pal" ; not front.gbcpal +INCLUDE "gfx/pokemon/unown/normal.pal" ; not normal.gbcpal INCLUDE "gfx/pokemon/unown/shiny.pal" -INCBIN "gfx/pokemon/wobbuffet/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/wobbuffet/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/wobbuffet/shiny.pal" -INCBIN "gfx/pokemon/girafarig/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/girafarig/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/girafarig/shiny.pal" -INCBIN "gfx/pokemon/pineco/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pineco/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pineco/shiny.pal" -INCBIN "gfx/pokemon/forretress/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/forretress/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/forretress/shiny.pal" -INCBIN "gfx/pokemon/dunsparce/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/dunsparce/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/dunsparce/shiny.pal" -INCBIN "gfx/pokemon/gligar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/gligar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/gligar/shiny.pal" -INCBIN "gfx/pokemon/steelix/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/steelix/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/steelix/shiny.pal" -INCBIN "gfx/pokemon/snubbull/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/snubbull/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/snubbull/shiny.pal" -INCBIN "gfx/pokemon/granbull/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/granbull/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/granbull/shiny.pal" -INCBIN "gfx/pokemon/qwilfish/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/qwilfish/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/qwilfish/shiny.pal" -INCBIN "gfx/pokemon/scizor/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/scizor/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/scizor/shiny.pal" -INCBIN "gfx/pokemon/shuckle/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/shuckle/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/shuckle/shiny.pal" -INCBIN "gfx/pokemon/heracross/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/heracross/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/heracross/shiny.pal" -INCBIN "gfx/pokemon/sneasel/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/sneasel/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/sneasel/shiny.pal" -INCBIN "gfx/pokemon/teddiursa/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/teddiursa/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/teddiursa/shiny.pal" -INCBIN "gfx/pokemon/ursaring/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ursaring/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ursaring/shiny.pal" -INCBIN "gfx/pokemon/slugma/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/slugma/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/slugma/shiny.pal" -INCBIN "gfx/pokemon/magcargo/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magcargo/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magcargo/shiny.pal" -INCBIN "gfx/pokemon/swinub/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/swinub/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/swinub/shiny.pal" -INCBIN "gfx/pokemon/piloswine/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/piloswine/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/piloswine/shiny.pal" -INCBIN "gfx/pokemon/corsola/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/corsola/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/corsola/shiny.pal" -INCBIN "gfx/pokemon/remoraid/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/remoraid/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/remoraid/shiny.pal" -INCBIN "gfx/pokemon/octillery/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/octillery/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/octillery/shiny.pal" -INCBIN "gfx/pokemon/delibird/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/delibird/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/delibird/shiny.pal" -INCBIN "gfx/pokemon/mantine/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/mantine/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/mantine/shiny.pal" -INCBIN "gfx/pokemon/skarmory/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/skarmory/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/skarmory/shiny.pal" -INCBIN "gfx/pokemon/houndour/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/houndour/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/houndour/shiny.pal" -INCBIN "gfx/pokemon/houndoom/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/houndoom/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/houndoom/shiny.pal" -INCBIN "gfx/pokemon/kingdra/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/kingdra/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/kingdra/shiny.pal" -INCBIN "gfx/pokemon/phanpy/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/phanpy/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/phanpy/shiny.pal" -INCBIN "gfx/pokemon/donphan/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/donphan/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/donphan/shiny.pal" -INCBIN "gfx/pokemon/porygon2/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/porygon2/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/porygon2/shiny.pal" -INCBIN "gfx/pokemon/stantler/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/stantler/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/stantler/shiny.pal" -INCBIN "gfx/pokemon/smeargle/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/smeargle/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/smeargle/shiny.pal" -INCBIN "gfx/pokemon/tyrogue/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tyrogue/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tyrogue/shiny.pal" -INCBIN "gfx/pokemon/hitmontop/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/hitmontop/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/hitmontop/shiny.pal" -INCBIN "gfx/pokemon/smoochum/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/smoochum/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/smoochum/shiny.pal" -INCBIN "gfx/pokemon/elekid/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/elekid/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/elekid/shiny.pal" -INCBIN "gfx/pokemon/magby/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/magby/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/magby/shiny.pal" -INCBIN "gfx/pokemon/miltank/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/miltank/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/miltank/shiny.pal" -INCBIN "gfx/pokemon/blissey/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/blissey/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/blissey/shiny.pal" -INCBIN "gfx/pokemon/raikou/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/raikou/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/raikou/shiny.pal" -INCBIN "gfx/pokemon/entei/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/entei/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/entei/shiny.pal" -INCBIN "gfx/pokemon/suicune/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/suicune/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/suicune/shiny.pal" -INCBIN "gfx/pokemon/larvitar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/larvitar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/larvitar/shiny.pal" -INCBIN "gfx/pokemon/pupitar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/pupitar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/pupitar/shiny.pal" -INCBIN "gfx/pokemon/tyranitar/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/tyranitar/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/tyranitar/shiny.pal" -INCBIN "gfx/pokemon/lugia/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/lugia/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/lugia/shiny.pal" -INCBIN "gfx/pokemon/ho_oh/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/ho_oh/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/ho_oh/shiny.pal" -INCBIN "gfx/pokemon/celebi/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/celebi/normal.gbcpal", middle_colors INCLUDE "gfx/pokemon/celebi/shiny.pal" assert_table_length NUM_POKEMON + 1 @@ -527,7 +527,7 @@ INCLUDE "gfx/pokemon/celebi/shiny.pal" RGB 30, 26, 11 RGB 23, 16, 00 -INCBIN "gfx/pokemon/egg/front.gbcpal", middle_colors +INCBIN "gfx/pokemon/egg/front.gbcpal", middle_colors ; not normal.gbcpal INCLUDE "gfx/pokemon/egg/shiny.pal" assert_table_length EGG + 1 diff --git a/tools/.gitignore b/tools/.gitignore index 080309fc29..7f8ecffa8f 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -1,3 +1,4 @@ +gbcpal gfx lzcomp make_patch diff --git a/tools/Makefile b/tools/Makefile index 395ece24e1..d17d10ec9f 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -4,6 +4,7 @@ CC := gcc CFLAGS := -O3 -flto -std=c11 -Wall -Wextra -pedantic tools := \ + gbcpal \ gfx \ lzcomp \ make_patch \ diff --git a/tools/gbcpal b/tools/gbcpal deleted file mode 100755 index 2f3f262ad0..0000000000 Binary files a/tools/gbcpal and /dev/null differ diff --git a/tools/gbcpal.c b/tools/gbcpal.c new file mode 100644 index 0000000000..88bee494e4 --- /dev/null +++ b/tools/gbcpal.c @@ -0,0 +1,141 @@ +#define PROGRAM_NAME "gbcpal" +#define USAGE_OPTS "[-h|--help] [-r|--reverse] out.gbcpal in.gbcpal..." + +#include "common.h" + +bool reverse; + +void parse_args(int argc, char *argv[]) { + struct option long_options[] = { + {"reverse", no_argument, 0, 'r'}, + {"help", no_argument, 0, 'h'}, + {0} + }; + for (int opt; (opt = getopt_long(argc, argv, "rh", long_options)) != -1;) { + switch (opt) { + case 'r': + reverse = true; + break; + case 'h': + usage_exit(0); + break; + default: + usage_exit(1); + } + } +} + +struct Color { + uint8_t r, g, b; +}; + +const struct Color BLACK = {0, 0, 0}; +const struct Color WHITE = {31, 31, 31}; + +uint16_t pack_color(struct Color color) { + return (color.b << 10) | (color.g << 5) | color.r; +} + +struct Color unpack_color(uint16_t gbc_color) { + return (struct Color){ + .r = gbc_color & 0x1f, + .g = (gbc_color >> 5) & 0x1f, + .b = (gbc_color >> 10) & 0x1f, + }; +} + +double luminance(struct Color color) { + return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b; +} + +int compare_colors(const void *color1, const void *color2) { + double lum1 = luminance(*(const struct Color *)color1); + double lum2 = luminance(*(const struct Color *)color2); + // sort lightest to darkest, or darkest to lightest if reversed + return reverse ? (lum1 > lum2) - (lum1 < lum2) : (lum1 < lum2) - (lum1 > lum2); +} + +void read_gbcpal(const char *filename, struct Color **colors, size_t *num_colors) { + long filesize; + uint8_t *bytes = read_u8(filename, &filesize); + if (filesize == 0) { + error_exit("%s: empty gbcpal file\n", filename); + } + if (filesize % 2) { + error_exit("%s: invalid gbcpal file\n", filename); + } + + size_t new_colors = filesize / 2; + *colors = xrealloc(*colors, (sizeof **colors) * (*num_colors + new_colors)); + for (size_t i = 0; i < new_colors; i++) { + uint16_t gbc_color = (bytes[i * 2 + 1] << 8) | bytes[i * 2]; + (*colors)[*num_colors + i] = unpack_color(gbc_color); + } + *num_colors += new_colors; + + free(bytes); +} + +void filter_colors(struct Color *colors, size_t *num_colors) { + size_t num_filtered = 0; + // filter out black, white, and duplicate colors + for (size_t i = 0; i < *num_colors; i++) { + struct Color color = colors[i]; + if (color.r == BLACK.r && color.g == BLACK.g && color.b == BLACK.b) { + continue; + } + if (color.r == WHITE.r && color.g == WHITE.g && color.b == WHITE.b) { + continue; + } + if (num_filtered > 0) { + struct Color last = colors[num_filtered - 1]; + if (color.r == last.r && color.g == last.g && color.b == last.b) { + continue; + } + } + colors[num_filtered++] = color; + } + *num_colors = num_filtered; +} + +int main(int argc, char *argv[]) { + parse_args(argc, argv); + + argc -= optind; + argv += optind; + if (argc < 2) { + usage_exit(1); + } + + const char *out_filename = argv[0]; + + struct Color *colors = NULL; + size_t num_colors = 0; + for (int i = 1; i < argc; i++) { + read_gbcpal(argv[i], &colors, &num_colors); + } + + qsort(colors, num_colors, sizeof(*colors), compare_colors); + filter_colors(colors, &num_colors); + + struct Color pal_colors[4] = { + WHITE, + num_colors > 0 ? colors[0] : WHITE, + num_colors > 1 ? colors[1] : num_colors > 0 ? colors[0] : BLACK, + BLACK, + }; + if (num_colors > 2) { + error_exit("%s: more than 2 colors besides black and white (%zu)\n", out_filename, num_colors); + } + + uint8_t bytes[COUNTOF(pal_colors) * 2] = {0}; + for (size_t i = 0; i < COUNTOF(pal_colors); i++) { + uint16_t packed_color = pack_color(pal_colors[i]); + bytes[2 * i] = packed_color & 0xff; + bytes[2 * i + 1] = packed_color >> 8; + } + write_u8(out_filename, bytes, COUNTOF(bytes)); + + free(colors); + return 0; +} diff --git a/tools/make_patch.c b/tools/make_patch.c index 85ffb34bfa..3a36accdd8 100644 --- a/tools/make_patch.c +++ b/tools/make_patch.c @@ -417,7 +417,7 @@ struct Buffer *process_template(const char *template_filename, const char *patch int compare_patch(const void *patch1, const void *patch2) { unsigned int offset1 = ((const struct Patch *)patch1)->offset; unsigned int offset2 = ((const struct Patch *)patch2)->offset; - return offset1 > offset2 ? 1 : offset1 < offset2 ? -1 : 0; + return (offset1 > offset2) - (offset1 < offset2); } bool verify_completeness(FILE *restrict orig_rom, FILE *restrict new_rom, struct Buffer *patches) { diff --git a/tools/palfix.py b/tools/palfix.py deleted file mode 100755 index a0b431da70..0000000000 --- a/tools/palfix.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Usage: python palfix.py image.png - -Fix the palette format of the input image. Colored images (Pokémon or trainer -sprites) will become indexed, with a palette sorted {white, light color, dark -color, black}. Grayscale images will become two-bit grayscale. -""" - -import sys - -import png - -def rgb8_to_rgb5(c): - r, g, b = c - return (r // 8, g // 8, b // 8) - -def rgb5_to_rgb8(c): - r, g, b = c - return (r * 8 + r // 4, g * 8 + g // 4, b * 8 + b // 4) - -def invert(c): - r, g, b = c - return (31 - r, 31 - g, 31 - b) - -def luminance(c): - r, g, b = c - return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2 - -def rgb5_pixels(row): - yield from (rgb8_to_rgb5(row[x:x+3]) for x in range(0, len(row), 4)) - -def is_grayscale(palette): - return (palette == ((31, 31, 31), (21, 21, 21), (10, 10, 10), (0, 0, 0)) or - palette == ((31, 31, 31), (20, 20, 20), (10, 10, 10), (0, 0, 0))) - -def fix_pal(filename): - with open(filename, 'rb') as file: - width, height, rows = png.Reader(file).asRGBA8()[:3] - rows = list(rows) - b_and_w = {(0, 0, 0), (31, 31, 31)} - colors = {c for row in rows for c in rgb5_pixels(row)} - b_and_w - if not colors: - colors = {(21, 21, 21), (10, 10, 10)} - elif len(colors) == 1: - c = colors.pop() - colors = {c, invert(c)} - elif len(colors) != 2: - return False - palette = tuple(sorted(colors | b_and_w, key=luminance, reverse=True)) - assert len(palette) == 4 - rows = [list(map(palette.index, rgb5_pixels(row))) for row in rows] - if is_grayscale(palette): - rows = [[3 - c for c in row] for row in rows] - writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9) - else: - palette = tuple(map(rgb5_to_rgb8, palette)) - writer = png.Writer(width, height, palette=palette, bitdepth=8, compression=9) - with open(filename, 'wb') as file: - writer.write(file, rows) - return True - -def main(): - if len(sys.argv) < 2: - print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr) - sys.exit(1) - for filename in sys.argv[1:]: - if not filename.lower().endswith('.png'): - print(f'{filename} is not a .png file!', file=sys.stderr) - elif not fix_pal(filename): - print(f'{filename} has too many colors!', file=sys.stderr) - -if __name__ == '__main__': - main() diff --git a/tools/png.py b/tools/png.py deleted file mode 100755 index dd9746bf0e..0000000000 --- a/tools/png.py +++ /dev/null @@ -1,2357 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# png.py - PNG encoder/decoder in pure Python -# -# Copyright (C) 2006 Johann C. Rocholl -# Portions Copyright (C) 2009 David Jones -# And probably portions Copyright (C) 2006 Nicko van Someren -# -# Original concept by Johann C. Rocholl. -# -# LICENCE (MIT) -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -""" -The ``png`` module can read and write PNG files. - -Installation and Overview -------------------------- - -``pip install pypng`` - -For help, type ``import png; help(png)`` in your python interpreter. - -A good place to start is the :class:`Reader` and :class:`Writer` classes. - -Coverage of PNG formats is fairly complete; -all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and -colour combinations are supported: - -- greyscale (1/2/4/8/16 bit); -- RGB, RGBA, LA (greyscale with alpha) with 8/16 bits per channel; -- colour mapped images (1/2/4/8 bit). - -Interlaced images, -which support a progressive display when downloading, -are supported for both reading and writing. - -A number of optional chunks can be specified (when writing) -and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. - -The ``sBIT`` chunk can be used to specify precision for -non-native bit depths. - -Requires Python 3.5 or higher. -Installation is trivial, -but see the ``README.txt`` file (with the source distribution) for details. - -Full use of all features will need some reading of the PNG specification -http://www.w3.org/TR/2003/REC-PNG-20031110/. - -The package also comes with command line utilities. - -- ``pripamtopng`` converts - `Netpbm `_ PAM/PNM files to PNG; -- ``pripngtopam`` converts PNG to file PAM/PNM. - -There are a few more for simple PNG manipulations. - -Spelling and Terminology ------------------------- - -Generally British English spelling is used in the documentation. -So that's "greyscale" and "colour". -This not only matches the author's native language, -it's also used by the PNG specification. - -Colour Models -------------- - -The major colour models supported by PNG (and hence by PyPNG) are: - -- greyscale; -- greyscale--alpha; -- RGB; -- RGB--alpha. - -Also referred to using the abbreviations: L, LA, RGB, RGBA. -Each letter codes a single channel: -*L* is for Luminance or Luma or Lightness (greyscale images); -*A* stands for Alpha, the opacity channel -(used for transparency effects, but higher values are more opaque, -so it makes sense to call it opacity); -*R*, *G*, *B* stand for Red, Green, Blue (colour image). - -Lists, arrays, sequences, and so on ------------------------------------ - -When getting pixel data out of this module (reading) and -presenting data to this module (writing) there are -a number of ways the data could be represented as a Python value. - -The preferred format is a sequence of *rows*, -which each row being a sequence of *values*. -In this format, the values are in pixel order, -with all the values from all the pixels in a row -being concatenated into a single sequence for that row. - -Consider an image that is 3 pixels wide by 2 pixels high, and each pixel -has RGB components: - -Sequence of rows:: - - list([R,G,B, R,G,B, R,G,B], - [R,G,B, R,G,B, R,G,B]) - -Each row appears as its own list, -but the pixels are flattened so that three values for one pixel -simply follow the three values for the previous pixel. - -This is the preferred because -it provides a good compromise between space and convenience. -PyPNG regards itself as at liberty to replace any sequence type with -any sufficiently compatible other sequence type; -in practice each row is an array (``bytearray`` or ``array.array``). - -To allow streaming the outer list is sometimes -an iterator rather than an explicit list. - -An alternative format is a single array holding all the values. - -Array of values:: - - [R,G,B, R,G,B, R,G,B, - R,G,B, R,G,B, R,G,B] - -The entire image is one single giant sequence of colour values. -Generally an array will be used (to save space), not a list. - -The top row comes first, -and within each row the pixels are ordered from left-to-right. -Within a pixel the values appear in the order R-G-B-A -(or L-A for greyscale--alpha). - -There is another format, which should only be used with caution. -It is mentioned because it is used internally, -is close to what lies inside a PNG file itself, -and has some support from the public API. -This format is called *packed*. -When packed, each row is a sequence of bytes (integers from 0 to 255), -just as it is before PNG scanline filtering is applied. -When the bit depth is 8 this is the same as a sequence of rows; -when the bit depth is less than 8 (1, 2 and 4), -several pixels are packed into each byte; -when the bit depth is 16 each pixel value is decomposed into 2 bytes -(and `packed` is a misnomer). -This format is used by the :meth:`Writer.write_packed` method. -It isn't usually a convenient format, -but may be just right if the source data for -the PNG image comes from something that uses a similar format -(for example, 1-bit BMPs, or another PNG file). -""" - -__version__ = "0.0.21" - -import collections -import io # For io.BytesIO -import itertools -import math -# http://www.python.org/doc/2.4.4/lib/module-operator.html -import operator -import re -import struct -import sys -# http://www.python.org/doc/2.4.4/lib/module-warnings.html -import warnings -import zlib - -from array import array - - -__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] - - -# The PNG signature. -# http://www.w3.org/TR/PNG/#5PNG-file-signature -signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10) - -# The xstart, ystart, xstep, ystep for the Adam7 interlace passes. -adam7 = ((0, 0, 8, 8), - (4, 0, 8, 8), - (0, 4, 4, 8), - (2, 0, 4, 4), - (0, 2, 2, 4), - (1, 0, 2, 2), - (0, 1, 1, 2)) - - -def adam7_generate(width, height): - """ - Generate the coordinates for the reduced scanlines - of an Adam7 interlaced image - of size `width` by `height` pixels. - - Yields a generator for each pass, - and each pass generator yields a series of (x, y, xstep) triples, - each one identifying a reduced scanline consisting of - pixels starting at (x, y) and taking every xstep pixel to the right. - """ - - for xstart, ystart, xstep, ystep in adam7: - if xstart >= width: - continue - yield ((xstart, y, xstep) for y in range(ystart, height, ystep)) - - -# Models the 'pHYs' chunk (used by the Reader) -Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter') - - -def group(s, n): - return list(zip(* [iter(s)] * n)) - - -def isarray(x): - return isinstance(x, array) - - -def check_palette(palette): - """ - Check a palette argument (to the :class:`Writer` class) for validity. - Returns the palette as a list if okay; - raises an exception otherwise. - """ - - # None is the default and is allowed. - if palette is None: - return None - - p = list(palette) - if not (0 < len(p) <= 256): - raise ProtocolError( - "a palette must have between 1 and 256 entries," - " see https://www.w3.org/TR/PNG/#11PLTE") - seen_triple = False - for i, t in enumerate(p): - if len(t) not in (3, 4): - raise ProtocolError( - "palette entry %d: entries must be 3- or 4-tuples." % i) - if len(t) == 3: - seen_triple = True - if seen_triple and len(t) == 4: - raise ProtocolError( - "palette entry %d: all 4-tuples must precede all 3-tuples" % i) - for x in t: - if int(x) != x or not(0 <= x <= 255): - raise ProtocolError( - "palette entry %d: " - "values must be integer: 0 <= x <= 255" % i) - return p - - -def check_sizes(size, width, height): - """ - Check that these arguments, if supplied, are consistent. - Return a (width, height) pair. - """ - - if not size: - return width, height - - if len(size) != 2: - raise ProtocolError( - "size argument should be a pair (width, height)") - if width is not None and width != size[0]: - raise ProtocolError( - "size[0] (%r) and width (%r) should match when both are used." - % (size[0], width)) - if height is not None and height != size[1]: - raise ProtocolError( - "size[1] (%r) and height (%r) should match when both are used." - % (size[1], height)) - return size - - -def check_color(c, greyscale, which): - """ - Checks that a colour argument for transparent or background options - is the right form. - Returns the colour - (which, if it's a bare integer, is "corrected" to a 1-tuple). - """ - - if c is None: - return c - if greyscale: - try: - len(c) - except TypeError: - c = (c,) - if len(c) != 1: - raise ProtocolError("%s for greyscale must be 1-tuple" % which) - if not is_natural(c[0]): - raise ProtocolError( - "%s colour for greyscale must be integer" % which) - else: - if not (len(c) == 3 and - is_natural(c[0]) and - is_natural(c[1]) and - is_natural(c[2])): - raise ProtocolError( - "%s colour must be a triple of integers" % which) - return c - - -class Error(Exception): - def __str__(self): - return self.__class__.__name__ + ': ' + ' '.join(self.args) - - -class FormatError(Error): - """ - Problem with input file format. - In other words, PNG file does not conform to - the specification in some way and is invalid. - """ - - -class ProtocolError(Error): - """ - Problem with the way the programming interface has been used, - or the data presented to it. - """ - - -class ChunkError(FormatError): - pass - - -class Default: - """The default for the greyscale parameter.""" - - -class Writer: - """ - PNG encoder in pure Python. - """ - - def __init__(self, width=None, height=None, - size=None, - greyscale=Default, - alpha=False, - bitdepth=8, - palette=None, - transparent=None, - background=None, - gamma=None, - compression=None, - interlace=False, - planes=None, - colormap=None, - maxval=None, - chunk_limit=2**20, - x_pixels_per_unit=None, - y_pixels_per_unit=None, - unit_is_meter=False): - """ - Create a PNG encoder object. - - Arguments: - - width, height - Image size in pixels, as two separate arguments. - size - Image size (w,h) in pixels, as single argument. - greyscale - Pixels are greyscale, not RGB. - alpha - Input data has alpha channel (RGBA or LA). - bitdepth - Bit depth: from 1 to 16 (for each channel). - palette - Create a palette for a colour mapped image (colour type 3). - transparent - Specify a transparent colour (create a ``tRNS`` chunk). - background - Specify a default background colour (create a ``bKGD`` chunk). - gamma - Specify a gamma value (create a ``gAMA`` chunk). - compression - zlib compression level: 0 (none) to 9 (more compressed); - default: -1 or None. - interlace - Create an interlaced image. - chunk_limit - Write multiple ``IDAT`` chunks to save memory. - x_pixels_per_unit - Number of pixels a unit along the x axis (write a - `pHYs` chunk). - y_pixels_per_unit - Number of pixels a unit along the y axis (write a - `pHYs` chunk). Along with `x_pixel_unit`, this gives - the pixel size ratio. - unit_is_meter - `True` to indicate that the unit (for the `pHYs` - chunk) is metre. - - The image size (in pixels) can be specified either by using the - `width` and `height` arguments, or with the single `size` - argument. - If `size` is used it should be a pair (*width*, *height*). - - The `greyscale` argument indicates whether input pixels - are greyscale (when true), or colour (when false). - The default is true unless `palette=` is used. - - The `alpha` argument (a boolean) specifies - whether input pixels have an alpha channel (or not). - - `bitdepth` specifies the bit depth of the source pixel values. - Each channel may have a different bit depth. - Each source pixel must have values that are - an integer between 0 and ``2**bitdepth-1``, where - `bitdepth` is the bit depth for the corresponding channel. - For example, 8-bit images have values between 0 and 255. - PNG only stores images with bit depths of - 1,2,4,8, or 16 (the same for all channels). - When `bitdepth` is not one of these values or where - channels have different bit depths, - the next highest valid bit depth is selected, - and an ``sBIT`` (significant bits) chunk is generated - that specifies the original precision of the source image. - In this case the supplied pixel values will be rescaled to - fit the range of the selected bit depth. - - The PNG file format supports many bit depth / colour model - combinations, but not all. - The details are somewhat arcane - (refer to the PNG specification for full details). - Briefly: - Bit depths < 8 (1,2,4) are only allowed with greyscale and - colour mapped images; - colour mapped images cannot have bit depth 16. - - For colour mapped images - (in other words, when the `palette` argument is specified) - the `bitdepth` argument must match one of - the valid PNG bit depths: 1, 2, 4, or 8. - (It is valid to have a PNG image with a palette and - an ``sBIT`` chunk, but the meaning is slightly different; - it would be awkward to use the `bitdepth` argument for this.) - - The `palette` option, when specified, - causes a colour mapped image to be created: - the PNG colour type is set to 3; - `greyscale` must not be true; `alpha` must not be true; - `transparent` must not be set. - The bit depth must be 1,2,4, or 8. - When a colour mapped image is created, - the pixel values are palette indexes and - the `bitdepth` argument specifies the size of these indexes - (not the size of the colour values in the palette). - - The palette argument value should be a sequence of 3- or - 4-tuples. - 3-tuples specify RGB palette entries; - 4-tuples specify RGBA palette entries. - All the 4-tuples (if present) must come before all the 3-tuples. - A ``PLTE`` chunk is created; - if there are 4-tuples then a ``tRNS`` chunk is created as well. - The ``PLTE`` chunk will contain all the RGB triples in the same - sequence; - the ``tRNS`` chunk will contain the alpha channel for - all the 4-tuples, in the same sequence. - Palette entries are always 8-bit. - - If specified, the `transparent` and `background` parameters must be - a tuple with one element for each channel in the image. - Either a 3-tuple of integer (RGB) values for a colour image, or - a 1-tuple of a single integer for a greyscale image. - - If specified, the `gamma` parameter must be a positive number - (generally, a `float`). - A ``gAMA`` chunk will be created. - Note that this will not change the values of the pixels as - they appear in the PNG file, - they are assumed to have already - been converted appropriately for the gamma specified. - - The `compression` argument specifies the compression level to - be used by the ``zlib`` module. - Values from 1 to 9 (highest) specify compression. - 0 means no compression. - -1 and ``None`` both mean that the ``zlib`` module uses - the default level of compression (which is generally acceptable). - - If `interlace` is true then an interlaced image is created - (using PNG's so far only interlace method, *Adam7*). - This does not affect how the pixels should be passed in, - rather it changes how they are arranged into the PNG file. - On slow connexions interlaced images can be - partially decoded by the browser to give - a rough view of the image that is - successively refined as more image data appears. - - .. note :: - - Enabling the `interlace` option requires the entire image - to be processed in working memory. - - `chunk_limit` is used to limit the amount of memory used whilst - compressing the image. - In order to avoid using large amounts of memory, - multiple ``IDAT`` chunks may be created. - """ - - # At the moment the `planes` argument is ignored; - # its purpose is to act as a dummy so that - # ``Writer(x, y, **info)`` works, where `info` is a dictionary - # returned by Reader.read and friends. - # Ditto for `colormap`. - - width, height = check_sizes(size, width, height) - del size - - if not is_natural(width) or not is_natural(height): - raise ProtocolError("width and height must be integers") - if width <= 0 or height <= 0: - raise ProtocolError("width and height must be greater than zero") - # http://www.w3.org/TR/PNG/#7Integers-and-byte-order - if width > 2 ** 31 - 1 or height > 2 ** 31 - 1: - raise ProtocolError("width and height cannot exceed 2**31-1") - - if alpha and transparent is not None: - raise ProtocolError( - "transparent colour not allowed with alpha channel") - - # bitdepth is either single integer, or tuple of integers. - # Convert to tuple. - try: - len(bitdepth) - except TypeError: - bitdepth = (bitdepth, ) - for b in bitdepth: - valid = is_natural(b) and 1 <= b <= 16 - if not valid: - raise ProtocolError( - "each bitdepth %r must be a positive integer <= 16" % - (bitdepth,)) - - # Calculate channels, and - # expand bitdepth to be one element per channel. - palette = check_palette(palette) - alpha = bool(alpha) - colormap = bool(palette) - if greyscale is Default and palette: - greyscale = False - greyscale = bool(greyscale) - if colormap: - color_planes = 1 - planes = 1 - else: - color_planes = (3, 1)[greyscale] - planes = color_planes + alpha - if len(bitdepth) == 1: - bitdepth *= planes - - bitdepth, self.rescale = check_bitdepth_rescale( - palette, - bitdepth, - transparent, alpha, greyscale) - - # These are assertions, because above logic should have - # corrected or raised all problematic cases. - if bitdepth < 8: - assert greyscale or palette - assert not alpha - if bitdepth > 8: - assert not palette - - transparent = check_color(transparent, greyscale, 'transparent') - background = check_color(background, greyscale, 'background') - - # It's important that the true boolean values - # (greyscale, alpha, colormap, interlace) are converted - # to bool because Iverson's convention is relied upon later on. - self.width = width - self.height = height - self.transparent = transparent - self.background = background - self.gamma = gamma - self.greyscale = greyscale - self.alpha = alpha - self.colormap = colormap - self.bitdepth = int(bitdepth) - self.compression = compression - self.chunk_limit = chunk_limit - self.interlace = bool(interlace) - self.palette = palette - self.x_pixels_per_unit = x_pixels_per_unit - self.y_pixels_per_unit = y_pixels_per_unit - self.unit_is_meter = bool(unit_is_meter) - - self.color_type = (4 * self.alpha + - 2 * (not greyscale) + - 1 * self.colormap) - assert self.color_type in (0, 2, 3, 4, 6) - - self.color_planes = color_planes - self.planes = planes - # :todo: fix for bitdepth < 8 - self.psize = (self.bitdepth / 8) * self.planes - - def write(self, outfile, rows): - """ - Write a PNG image to the output file. - `rows` should be an iterable that yields each row - (each row is a sequence of values). - The rows should be the rows of the original image, - so there should be ``self.height`` rows of - ``self.width * self.planes`` values. - If `interlace` is specified (when creating the instance), - then an interlaced PNG file will be written. - Supply the rows in the normal image order; - the interlacing is carried out internally. - - .. note :: - - Interlacing requires the entire image to be in working memory. - """ - - # Values per row - vpr = self.width * self.planes - - def check_rows(rows): - """ - Yield each row in rows, - but check each row first (for correct width). - """ - for i, row in enumerate(rows): - try: - wrong_length = len(row) != vpr - except TypeError: - # When using an itertools.ichain object or - # other generator not supporting __len__, - # we set this to False to skip the check. - wrong_length = False - if wrong_length: - # Note: row numbers start at 0. - raise ProtocolError( - "Expected %d values but got %d values, in row %d" % - (vpr, len(row), i)) - yield row - - if self.interlace: - fmt = 'BH'[self.bitdepth > 8] - a = array(fmt, itertools.chain(*check_rows(rows))) - return self.write_array(outfile, a) - - nrows = self.write_passes(outfile, check_rows(rows)) - if nrows != self.height: - raise ProtocolError( - "rows supplied (%d) does not match height (%d)" % - (nrows, self.height)) - return nrows - - def write_passes(self, outfile, rows): - """ - Write a PNG image to the output file. - - Most users are expected to find the :meth:`write` or - :meth:`write_array` method more convenient. - - The rows should be given to this method in the order that - they appear in the output file. - For straightlaced images, this is the usual top to bottom ordering. - For interlaced images the rows should have been interlaced before - passing them to this function. - - `rows` should be an iterable that yields each row - (each row being a sequence of values). - """ - - # Ensure rows are scaled (to 4-/8-/16-bit), - # and packed into bytes. - - if self.rescale: - rows = rescale_rows(rows, self.rescale) - - if self.bitdepth < 8: - rows = pack_rows(rows, self.bitdepth) - elif self.bitdepth == 16: - rows = unpack_rows(rows) - - return self.write_packed(outfile, rows) - - def write_packed(self, outfile, rows): - """ - Write PNG file to `outfile`. - `rows` should be an iterator that yields each packed row; - a packed row being a sequence of packed bytes. - - The rows have a filter byte prefixed and - are then compressed into one or more IDAT chunks. - They are not processed any further, - so if bitdepth is other than 1, 2, 4, 8, 16, - the pixel values should have been scaled - before passing them to this method. - - This method does work for interlaced images but it is best avoided. - For interlaced images, the rows should be - presented in the order that they appear in the file. - """ - - self.write_preamble(outfile) - - # http://www.w3.org/TR/PNG/#11IDAT - if self.compression is not None: - compressor = zlib.compressobj(self.compression) - else: - compressor = zlib.compressobj() - - # data accumulates bytes to be compressed for the IDAT chunk; - # it's compressed when sufficiently large. - data = bytearray() - - # raise i scope out of the for loop. set to -1, because the for loop - # sets i to 0 on the first pass - i = -1 - for i, row in enumerate(rows): - # Add "None" filter type. - # Currently, it's essential that this filter type be used - # for every scanline as - # we do not mark the first row of a reduced pass image; - # that means we could accidentally compute - # the wrong filtered scanline if we used - # "up", "average", or "paeth" on such a line. - data.append(0) - data.extend(row) - if len(data) > self.chunk_limit: - compressed = compressor.compress(data) - if len(compressed): - write_chunk(outfile, b'IDAT', compressed) - data = bytearray() - - compressed = compressor.compress(bytes(data)) - flushed = compressor.flush() - if len(compressed) or len(flushed): - write_chunk(outfile, b'IDAT', compressed + flushed) - # http://www.w3.org/TR/PNG/#11IEND - write_chunk(outfile, b'IEND') - return i + 1 - - def write_preamble(self, outfile): - # http://www.w3.org/TR/PNG/#5PNG-file-signature - outfile.write(signature) - - # http://www.w3.org/TR/PNG/#11IHDR - write_chunk(outfile, b'IHDR', - struct.pack("!2I5B", self.width, self.height, - self.bitdepth, self.color_type, - 0, 0, self.interlace)) - - # See :chunk:order - # http://www.w3.org/TR/PNG/#11gAMA - if self.gamma is not None: - write_chunk(outfile, b'gAMA', - struct.pack("!L", int(round(self.gamma * 1e5)))) - - # See :chunk:order - # http://www.w3.org/TR/PNG/#11sBIT - if self.rescale: - write_chunk( - outfile, b'sBIT', - struct.pack('%dB' % self.planes, - * [s[0] for s in self.rescale])) - - # :chunk:order: Without a palette (PLTE chunk), - # ordering is relatively relaxed. - # With one, gAMA chunk must precede PLTE chunk - # which must precede tRNS and bKGD. - # See http://www.w3.org/TR/PNG/#5ChunkOrdering - if self.palette: - p, t = make_palette_chunks(self.palette) - write_chunk(outfile, b'PLTE', p) - if t: - # tRNS chunk is optional; - # Only needed if palette entries have alpha. - write_chunk(outfile, b'tRNS', t) - - # http://www.w3.org/TR/PNG/#11tRNS - if self.transparent is not None: - if self.greyscale: - fmt = "!1H" - else: - fmt = "!3H" - write_chunk(outfile, b'tRNS', - struct.pack(fmt, *self.transparent)) - - # http://www.w3.org/TR/PNG/#11bKGD - if self.background is not None: - if self.greyscale: - fmt = "!1H" - else: - fmt = "!3H" - write_chunk(outfile, b'bKGD', - struct.pack(fmt, *self.background)) - - # http://www.w3.org/TR/PNG/#11pHYs - if (self.x_pixels_per_unit is not None and - self.y_pixels_per_unit is not None): - tup = (self.x_pixels_per_unit, - self.y_pixels_per_unit, - int(self.unit_is_meter)) - write_chunk(outfile, b'pHYs', struct.pack("!LLB", *tup)) - - def write_array(self, outfile, pixels): - """ - Write an array that holds all the image values - as a PNG file on the output file. - See also :meth:`write` method. - """ - - if self.interlace: - if type(pixels) != array: - # Coerce to array type - fmt = 'BH'[self.bitdepth > 8] - pixels = array(fmt, pixels) - return self.write_passes( - outfile, - self.array_scanlines_interlace(pixels) - ) - else: - return self.write_passes( - outfile, - self.array_scanlines(pixels) - ) - - def array_scanlines(self, pixels): - """ - Generates rows (each a sequence of values) from - a single array of values. - """ - - # Values per row - vpr = self.width * self.planes - stop = 0 - for y in range(self.height): - start = stop - stop = start + vpr - yield pixels[start:stop] - - def array_scanlines_interlace(self, pixels): - """ - Generator for interlaced scanlines from an array. - `pixels` is the full source image as a single array of values. - The generator yields each scanline of the reduced passes in turn, - each scanline being a sequence of values. - """ - - # http://www.w3.org/TR/PNG/#8InterlaceMethods - # Array type. - fmt = 'BH'[self.bitdepth > 8] - # Value per row - vpr = self.width * self.planes - - # Each iteration generates a scanline starting at (x, y) - # and consisting of every xstep pixels. - for lines in adam7_generate(self.width, self.height): - for x, y, xstep in lines: - # Pixels per row (of reduced image) - ppr = int(math.ceil((self.width - x) / float(xstep))) - # Values per row (of reduced image) - reduced_row_len = ppr * self.planes - if xstep == 1: - # Easy case: line is a simple slice. - offset = y * vpr - yield pixels[offset: offset + vpr] - continue - # We have to step by xstep, - # which we can do one plane at a time - # using the step in Python slices. - row = array(fmt) - # There's no easier way to set the length of an array - row.extend(pixels[0:reduced_row_len]) - offset = y * vpr + x * self.planes - end_offset = (y + 1) * vpr - skip = self.planes * xstep - for i in range(self.planes): - row[i::self.planes] = \ - pixels[offset + i: end_offset: skip] - yield row - - -def write_chunk(outfile, tag, data=b''): - """ - Write a PNG chunk to the output file, including length and - checksum. - """ - - data = bytes(data) - # http://www.w3.org/TR/PNG/#5Chunk-layout - outfile.write(struct.pack("!I", len(data))) - outfile.write(tag) - outfile.write(data) - checksum = zlib.crc32(tag) - checksum = zlib.crc32(data, checksum) - checksum &= 2 ** 32 - 1 - outfile.write(struct.pack("!I", checksum)) - - -def write_chunks(out, chunks): - """Create a PNG file by writing out the chunks.""" - - out.write(signature) - for chunk in chunks: - write_chunk(out, *chunk) - - -def rescale_rows(rows, rescale): - """ - Take each row in rows (an iterator) and yield - a fresh row with the pixels scaled according to - the rescale parameters in the list `rescale`. - Each element of `rescale` is a tuple of - (source_bitdepth, target_bitdepth), - with one element per channel. - """ - - # One factor for each channel - fs = [float(2 ** s[1] - 1)/float(2 ** s[0] - 1) - for s in rescale] - - # Assume all target_bitdepths are the same - target_bitdepths = set(s[1] for s in rescale) - assert len(target_bitdepths) == 1 - (target_bitdepth, ) = target_bitdepths - typecode = 'BH'[target_bitdepth > 8] - - # Number of channels - n_chans = len(rescale) - - for row in rows: - rescaled_row = array(typecode, iter(row)) - for i in range(n_chans): - channel = array( - typecode, - (int(round(fs[i] * x)) for x in row[i::n_chans])) - rescaled_row[i::n_chans] = channel - yield rescaled_row - - -def pack_rows(rows, bitdepth): - """Yield packed rows that are a byte array. - Each byte is packed with the values from several pixels. - """ - - assert bitdepth < 8 - assert 8 % bitdepth == 0 - - # samples per byte - spb = int(8 / bitdepth) - - def make_byte(block): - """Take a block of (2, 4, or 8) values, - and pack them into a single byte. - """ - - res = 0 - for v in block: - res = (res << bitdepth) + v - return res - - for row in rows: - a = bytearray(row) - # Adding padding bytes so we can group into a whole - # number of spb-tuples. - n = float(len(a)) - extra = math.ceil(n / spb) * spb - n - a.extend([0] * int(extra)) - # Pack into bytes. - # Each block is the samples for one byte. - blocks = group(a, spb) - yield bytearray(make_byte(block) for block in blocks) - - -def unpack_rows(rows): - """Unpack each row from being 16-bits per value, - to being a sequence of bytes. - """ - for row in rows: - fmt = '!%dH' % len(row) - yield bytearray(struct.pack(fmt, *row)) - - -def make_palette_chunks(palette): - """ - Create the byte sequences for a ``PLTE`` and - if necessary a ``tRNS`` chunk. - Returned as a pair (*p*, *t*). - *t* will be ``None`` if no ``tRNS`` chunk is necessary. - """ - - p = bytearray() - t = bytearray() - - for x in palette: - p.extend(x[0:3]) - if len(x) > 3: - t.append(x[3]) - if t: - return p, t - return p, None - - -def check_bitdepth_rescale( - palette, bitdepth, transparent, alpha, greyscale): - """ - Returns (bitdepth, rescale) pair. - """ - - if palette: - if len(bitdepth) != 1: - raise ProtocolError( - "with palette, only a single bitdepth may be used") - (bitdepth, ) = bitdepth - if bitdepth not in (1, 2, 4, 8): - raise ProtocolError( - "with palette, bitdepth must be 1, 2, 4, or 8") - if transparent is not None: - raise ProtocolError("transparent and palette not compatible") - if alpha: - raise ProtocolError("alpha and palette not compatible") - if greyscale: - raise ProtocolError("greyscale and palette not compatible") - return bitdepth, None - - # No palette, check for sBIT chunk generation. - - if greyscale and not alpha: - # Single channel, L. - (bitdepth,) = bitdepth - if bitdepth in (1, 2, 4, 8, 16): - return bitdepth, None - if bitdepth > 8: - targetbitdepth = 16 - elif bitdepth == 3: - targetbitdepth = 4 - else: - assert bitdepth in (5, 6, 7) - targetbitdepth = 8 - return targetbitdepth, [(bitdepth, targetbitdepth)] - - assert alpha or not greyscale - - depth_set = tuple(set(bitdepth)) - if depth_set in [(8,), (16,)]: - # No sBIT required. - (bitdepth, ) = depth_set - return bitdepth, None - - targetbitdepth = (8, 16)[max(bitdepth) > 8] - return targetbitdepth, [(b, targetbitdepth) for b in bitdepth] - - -# Regex for decoding mode string -RegexModeDecode = re.compile("(LA?|RGBA?);?([0-9]*)", flags=re.IGNORECASE) - - -def from_array(a, mode=None, info={}): - """ - Create a PNG :class:`Image` object from a 2-dimensional array. - One application of this function is easy PIL-style saving: - ``png.from_array(pixels, 'L').save('foo.png')``. - - Unless they are specified using the *info* parameter, - the PNG's height and width are taken from the array size. - The first axis is the height; the second axis is the - ravelled width and channel index. - The array is treated is a sequence of rows, - each row being a sequence of values (``width*channels`` in number). - So an RGB image that is 16 pixels high and 8 wide will - occupy a 2-dimensional array that is 16x24 - (each row will be 8*3 = 24 sample values). - - *mode* is a string that specifies the image colour format in a - PIL-style mode. It can be: - - ``'L'`` - greyscale (1 channel) - ``'LA'`` - greyscale with alpha (2 channel) - ``'RGB'`` - colour image (3 channel) - ``'RGBA'`` - colour image with alpha (4 channel) - - The mode string can also specify the bit depth - (overriding how this function normally derives the bit depth, - see below). - Appending ``';16'`` to the mode will cause the PNG to be - 16 bits per channel; - any decimal from 1 to 16 can be used to specify the bit depth. - - When a 2-dimensional array is used *mode* determines how many - channels the image has, and so allows the width to be derived from - the second array dimension. - - The array is expected to be a ``numpy`` array, - but it can be any suitable Python sequence. - For example, a list of lists can be used: - ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. - The exact rules are: ``len(a)`` gives the first dimension, height; - ``len(a[0])`` gives the second dimension. - It's slightly more complicated than that because - an iterator of rows can be used, and it all still works. - Using an iterator allows data to be streamed efficiently. - - The bit depth of the PNG is normally taken from - the array element's datatype - (but if *mode* specifies a bitdepth then that is used instead). - The array element's datatype is determined in a way which - is supposed to work both for ``numpy`` arrays and for Python - ``array.array`` objects. - A 1 byte datatype will give a bit depth of 8, - a 2 byte datatype will give a bit depth of 16. - If the datatype does not have an implicit size, - like the above example where it is a plain Python list of lists, - then a default of 8 is used. - - The *info* parameter is a dictionary that can - be used to specify metadata (in the same style as - the arguments to the :class:`png.Writer` class). - For this function the keys that are useful are: - - height - overrides the height derived from the array dimensions and - allows *a* to be an iterable. - width - overrides the width derived from the array dimensions. - bitdepth - overrides the bit depth derived from the element datatype - (but must match *mode* if that also specifies a bit depth). - - Generally anything specified in the *info* dictionary will - override any implicit choices that this function would otherwise make, - but must match any explicit ones. - For example, if the *info* dictionary has a ``greyscale`` key then - this must be true when mode is ``'L'`` or ``'LA'`` and - false when mode is ``'RGB'`` or ``'RGBA'``. - """ - - # We abuse the *info* parameter by modifying it. Take a copy here. - # (Also typechecks *info* to some extent). - info = dict(info) - - # Syntax check mode string. - match = RegexModeDecode.match(mode) - if not match: - raise Error("mode string should be 'RGB' or 'L;16' or similar.") - - mode, bitdepth = match.groups() - if bitdepth: - bitdepth = int(bitdepth) - - # Colour format. - if 'greyscale' in info: - if bool(info['greyscale']) != ('L' in mode): - raise ProtocolError("info['greyscale'] should match mode.") - info['greyscale'] = 'L' in mode - - alpha = 'A' in mode - if 'alpha' in info: - if bool(info['alpha']) != alpha: - raise ProtocolError("info['alpha'] should match mode.") - info['alpha'] = alpha - - # Get bitdepth from *mode* if possible. - if bitdepth: - if info.get("bitdepth") and bitdepth != info['bitdepth']: - raise ProtocolError( - "bitdepth (%d) should match bitdepth of info (%d)." % - (bitdepth, info['bitdepth'])) - info['bitdepth'] = bitdepth - - # Fill in and/or check entries in *info*. - # Dimensions. - width, height = check_sizes( - info.get("size"), - info.get("width"), - info.get("height")) - if width: - info["width"] = width - if height: - info["height"] = height - - if "height" not in info: - try: - info['height'] = len(a) - except TypeError: - raise ProtocolError( - "len(a) does not work, supply info['height'] instead.") - - planes = len(mode) - if 'planes' in info: - if info['planes'] != planes: - raise Error("info['planes'] should match mode.") - - # In order to work out whether we the array is 2D or 3D we need its - # first row, which requires that we take a copy of its iterator. - # We may also need the first row to derive width and bitdepth. - a, t = itertools.tee(a) - row = next(t) - del t - - testelement = row - if 'width' not in info: - width = len(row) // planes - info['width'] = width - - if 'bitdepth' not in info: - try: - dtype = testelement.dtype - # goto the "else:" clause. Sorry. - except AttributeError: - try: - # Try a Python array.array. - bitdepth = 8 * testelement.itemsize - except AttributeError: - # We can't determine it from the array element's datatype, - # use a default of 8. - bitdepth = 8 - else: - # If we got here without exception, - # we now assume that the array is a numpy array. - if dtype.kind == 'b': - bitdepth = 1 - else: - bitdepth = 8 * dtype.itemsize - info['bitdepth'] = bitdepth - - for thing in ["width", "height", "bitdepth", "greyscale", "alpha"]: - assert thing in info - - return Image(a, info) - - -# So that refugee's from PIL feel more at home. Not documented. -fromarray = from_array - - -class Image: - """A PNG image. You can create an :class:`Image` object from - an array of pixels by calling :meth:`png.from_array`. It can be - saved to disk with the :meth:`save` method. - """ - - def __init__(self, rows, info): - """ - .. note :: - - The constructor is not public. Please do not call it. - """ - - self.rows = rows - self.info = info - - def save(self, file): - """Save the image to the named *file*. - - See `.write()` if you already have an open file object. - - In general, you can only call this method once; - after it has been called the first time the PNG image is written, - the source data will have been streamed, and - cannot be streamed again. - """ - - w = Writer(**self.info) - - with open(file, 'wb') as fd: - w.write(fd, self.rows) - - def write(self, file): - """Write the image to the open file object. - - See `.save()` if you have a filename. - - In general, you can only call this method once; - after it has been called the first time the PNG image is written, - the source data will have been streamed, and - cannot be streamed again. - """ - - w = Writer(**self.info) - w.write(file, self.rows) - - -class Reader: - """ - Pure Python PNG decoder in pure Python. - """ - - def __init__(self, _guess=None, filename=None, file=None, bytes=None): - """ - The constructor expects exactly one keyword argument. - If you supply a positional argument instead, - it will guess the input type. - Choose from the following keyword arguments: - - filename - Name of input file (a PNG file). - file - A file-like object (object with a read() method). - bytes - ``bytes`` or ``bytearray`` with PNG data. - - """ - keywords_supplied = ( - (_guess is not None) + - (filename is not None) + - (file is not None) + - (bytes is not None)) - if keywords_supplied != 1: - raise TypeError("Reader() takes exactly 1 argument") - - # Will be the first 8 bytes, later on. See validate_signature. - self.signature = None - self.transparent = None - # A pair of (len,type) if a chunk has been read but its data and - # checksum have not (in other words the file position is just - # past the 4 bytes that specify the chunk type). - # See preamble method for how this is used. - self.atchunk = None - - if _guess is not None: - if isarray(_guess): - bytes = _guess - elif isinstance(_guess, str): - filename = _guess - elif hasattr(_guess, 'read'): - file = _guess - - if bytes is not None: - self.file = io.BytesIO(bytes) - elif filename is not None: - self.file = open(filename, "rb") - elif file is not None: - self.file = file - else: - raise ProtocolError("expecting filename, file or bytes array") - - def chunk(self, lenient=False): - """ - Read the next PNG chunk from the input file; - returns a (*type*, *data*) tuple. - *type* is the chunk's type as a byte string - (all PNG chunk types are 4 bytes long). - *data* is the chunk's data content, as a byte string. - - If the optional `lenient` argument evaluates to `True`, - checksum failures will raise warnings rather than exceptions. - """ - - self.validate_signature() - - # http://www.w3.org/TR/PNG/#5Chunk-layout - if not self.atchunk: - self.atchunk = self._chunk_len_type() - if not self.atchunk: - raise ChunkError("No more chunks.") - length, type = self.atchunk - self.atchunk = None - - data = self.file.read(length) - if len(data) != length: - raise ChunkError( - 'Chunk %s too short for required %i octets.' - % (type, length)) - checksum = self.file.read(4) - if len(checksum) != 4: - raise ChunkError('Chunk %s too short for checksum.' % type) - verify = zlib.crc32(type) - verify = zlib.crc32(data, verify) - verify = struct.pack('!I', verify) - if checksum != verify: - (a, ) = struct.unpack('!I', checksum) - (b, ) = struct.unpack('!I', verify) - message = ("Checksum error in %s chunk: 0x%08X != 0x%08X." - % (type.decode('ascii'), a, b)) - if lenient: - warnings.warn(message, RuntimeWarning) - else: - raise ChunkError(message) - return type, data - - def chunks(self): - """Return an iterator that will yield each chunk as a - (*chunktype*, *content*) pair. - """ - - while True: - t, v = self.chunk() - yield t, v - if t == b'IEND': - break - - def undo_filter(self, filter_type, scanline, previous): - """ - Undo the filter for a scanline. - `scanline` is a sequence of bytes that - does not include the initial filter type byte. - `previous` is decoded previous scanline - (for straightlaced images this is the previous pixel row, - but for interlaced images, it is - the previous scanline in the reduced image, - which in general is not the previous pixel row in the final image). - When there is no previous scanline - (the first row of a straightlaced image, - or the first row in one of the passes in an interlaced image), - then this argument should be ``None``. - - The scanline will have the effects of filtering removed; - the result will be returned as a fresh sequence of bytes. - """ - - # :todo: Would it be better to update scanline in place? - result = scanline - - if filter_type == 0: - return result - - if filter_type not in (1, 2, 3, 4): - raise FormatError( - 'Invalid PNG Filter Type. ' - 'See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .') - - # Filter unit. The stride from one pixel to the corresponding - # byte from the previous pixel. Normally this is the pixel - # size in bytes, but when this is smaller than 1, the previous - # byte is used instead. - fu = max(1, self.psize) - - # For the first line of a pass, synthesize a dummy previous - # line. An alternative approach would be to observe that on the - # first line 'up' is the same as 'null', 'paeth' is the same - # as 'sub', with only 'average' requiring any special case. - if not previous: - previous = bytearray([0] * len(scanline)) - - # Call appropriate filter algorithm. Note that 0 has already - # been dealt with. - fn = (None, - undo_filter_sub, - undo_filter_up, - undo_filter_average, - undo_filter_paeth)[filter_type] - fn(fu, scanline, previous, result) - return result - - def _deinterlace(self, raw): - """ - Read raw pixel data, undo filters, deinterlace, and flatten. - Return a single array of values. - """ - - # Values per row (of the target image) - vpr = self.width * self.planes - - # Values per image - vpi = vpr * self.height - # Interleaving writes to the output array randomly - # (well, not quite), so the entire output array must be in memory. - # Make a result array, and make it big enough. - if self.bitdepth > 8: - a = array('H', [0] * vpi) - else: - a = bytearray([0] * vpi) - source_offset = 0 - - for lines in adam7_generate(self.width, self.height): - # The previous (reconstructed) scanline. - # `None` at the beginning of a pass - # to indicate that there is no previous line. - recon = None - for x, y, xstep in lines: - # Pixels per row (reduced pass image) - ppr = int(math.ceil((self.width - x) / float(xstep))) - # Row size in bytes for this pass. - row_size = int(math.ceil(self.psize * ppr)) - - filter_type = raw[source_offset] - source_offset += 1 - scanline = raw[source_offset: source_offset + row_size] - source_offset += row_size - recon = self.undo_filter(filter_type, scanline, recon) - # Convert so that there is one element per pixel value - flat = self._bytes_to_values(recon, width=ppr) - if xstep == 1: - assert x == 0 - offset = y * vpr - a[offset: offset + vpr] = flat - else: - offset = y * vpr + x * self.planes - end_offset = (y + 1) * vpr - skip = self.planes * xstep - for i in range(self.planes): - a[offset + i: end_offset: skip] = \ - flat[i:: self.planes] - - return a - - def _iter_bytes_to_values(self, byte_rows): - """ - Iterator that yields each scanline; - each scanline being a sequence of values. - `byte_rows` should be an iterator that yields - the bytes of each row in turn. - """ - - for row in byte_rows: - yield self._bytes_to_values(row) - - def _bytes_to_values(self, bs, width=None): - """Convert a packed row of bytes into a row of values. - Result will be a freshly allocated object, - not shared with the argument. - """ - - if self.bitdepth == 8: - return bytearray(bs) - if self.bitdepth == 16: - return array('H', - struct.unpack('!%dH' % (len(bs) // 2), bs)) - - assert self.bitdepth < 8 - if width is None: - width = self.width - # Samples per byte - spb = 8 // self.bitdepth - out = bytearray() - mask = 2**self.bitdepth - 1 - shifts = [self.bitdepth * i - for i in reversed(list(range(spb)))] - for o in bs: - out.extend([mask & (o >> i) for i in shifts]) - return out[:width] - - def _iter_straight_packed(self, byte_blocks): - """Iterator that undoes the effect of filtering; - yields each row as a sequence of packed bytes. - Assumes input is straightlaced. - `byte_blocks` should be an iterable that yields the raw bytes - in blocks of arbitrary size. - """ - - # length of row, in bytes - rb = self.row_bytes - a = bytearray() - # The previous (reconstructed) scanline. - # None indicates first line of image. - recon = None - for some_bytes in byte_blocks: - a.extend(some_bytes) - while len(a) >= rb + 1: - filter_type = a[0] - scanline = a[1: rb + 1] - del a[: rb + 1] - recon = self.undo_filter(filter_type, scanline, recon) - yield recon - if len(a) != 0: - # :file:format We get here with a file format error: - # when the available bytes (after decompressing) do not - # pack into exact rows. - raise FormatError('Wrong size for decompressed IDAT chunk.') - assert len(a) == 0 - - def validate_signature(self): - """ - If signature (header) has not been read then read and - validate it; otherwise do nothing. - No signature (empty read()) will raise EOFError; - An invalid signature will raise FormatError. - EOFError is raised to make possible the case where - a program can read multiple PNG files from the same stream. - The end of the stream can be distinguished from non-PNG files - or corrupted PNG files. - """ - - if self.signature: - return - self.signature = self.file.read(8) - if len(self.signature) == 0: - raise EOFError("End of PNG stream.") - if self.signature != signature: - raise FormatError("PNG file has invalid signature.") - - def preamble(self, lenient=False): - """ - Extract the image metadata by reading - the initial part of the PNG file up to - the start of the ``IDAT`` chunk. - All the chunks that precede the ``IDAT`` chunk are - read and either processed for metadata or discarded. - - If the optional `lenient` argument evaluates to `True`, - checksum failures will raise warnings rather than exceptions. - """ - - self.validate_signature() - - while True: - if not self.atchunk: - self.atchunk = self._chunk_len_type() - if self.atchunk is None: - raise FormatError('This PNG file has no IDAT chunks.') - if self.atchunk[1] == b'IDAT': - return - self.process_chunk(lenient=lenient) - - def _chunk_len_type(self): - """ - Reads just enough of the input to - determine the next chunk's length and type; - return a (*length*, *type*) pair where *type* is a byte sequence. - If there are no more chunks, ``None`` is returned. - """ - - x = self.file.read(8) - if not x: - return None - if len(x) != 8: - raise FormatError( - 'End of file whilst reading chunk length and type.') - length, type = struct.unpack('!I4s', x) - if length > 2 ** 31 - 1: - raise FormatError('Chunk %s is too large: %d.' % (type, length)) - # Check that all bytes are in valid ASCII range. - # https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout - type_bytes = set(bytearray(type)) - if not(type_bytes <= set(range(65, 91)) | set(range(97, 123))): - raise FormatError( - 'Chunk %r has invalid Chunk Type.' - % list(type)) - return length, type - - def process_chunk(self, lenient=False): - """ - Process the next chunk and its data. - This only processes the following chunk types: - ``IHDR``, ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``. - All other chunk types are ignored. - - If the optional `lenient` argument evaluates to `True`, - checksum failures will raise warnings rather than exceptions. - """ - - type, data = self.chunk(lenient=lenient) - method = '_process_' + type.decode('ascii') - m = getattr(self, method, None) - if m: - m(data) - - def _process_IHDR(self, data): - # http://www.w3.org/TR/PNG/#11IHDR - if len(data) != 13: - raise FormatError('IHDR chunk has incorrect length.') - (self.width, self.height, self.bitdepth, self.color_type, - self.compression, self.filter, - self.interlace) = struct.unpack("!2I5B", data) - - check_bitdepth_colortype(self.bitdepth, self.color_type) - - if self.compression != 0: - raise FormatError( - "Unknown compression method %d" % self.compression) - if self.filter != 0: - raise FormatError( - "Unknown filter method %d," - " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." - % self.filter) - if self.interlace not in (0, 1): - raise FormatError( - "Unknown interlace method %d, see " - "http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods" - " ." - % self.interlace) - - # Derived values - # http://www.w3.org/TR/PNG/#6Colour-values - colormap = bool(self.color_type & 1) - greyscale = not(self.color_type & 2) - alpha = bool(self.color_type & 4) - color_planes = (3, 1)[greyscale or colormap] - planes = color_planes + alpha - - self.colormap = colormap - self.greyscale = greyscale - self.alpha = alpha - self.color_planes = color_planes - self.planes = planes - self.psize = float(self.bitdepth) / float(8) * planes - if int(self.psize) == self.psize: - self.psize = int(self.psize) - self.row_bytes = int(math.ceil(self.width * self.psize)) - # Stores PLTE chunk if present, and is used to check - # chunk ordering constraints. - self.plte = None - # Stores tRNS chunk if present, and is used to check chunk - # ordering constraints. - self.trns = None - # Stores sBIT chunk if present. - self.sbit = None - - def _process_PLTE(self, data): - # http://www.w3.org/TR/PNG/#11PLTE - if self.plte: - warnings.warn("Multiple PLTE chunks present.") - self.plte = data - if len(data) % 3 != 0: - raise FormatError( - "PLTE chunk's length should be a multiple of 3.") - if len(data) > (2 ** self.bitdepth) * 3: - raise FormatError("PLTE chunk is too long.") - if len(data) == 0: - raise FormatError("Empty PLTE is not allowed.") - - def _process_bKGD(self, data): - try: - if self.colormap: - if not self.plte: - warnings.warn( - "PLTE chunk is required before bKGD chunk.") - self.background = struct.unpack('B', data) - else: - self.background = struct.unpack("!%dH" % self.color_planes, - data) - except struct.error: - raise FormatError("bKGD chunk has incorrect length.") - - def _process_tRNS(self, data): - # http://www.w3.org/TR/PNG/#11tRNS - self.trns = data - if self.colormap: - if not self.plte: - warnings.warn("PLTE chunk is required before tRNS chunk.") - else: - if len(data) > len(self.plte) / 3: - # Was warning, but promoted to Error as it - # would otherwise cause pain later on. - raise FormatError("tRNS chunk is too long.") - else: - if self.alpha: - raise FormatError( - "tRNS chunk is not valid with colour type %d." % - self.color_type) - try: - self.transparent = \ - struct.unpack("!%dH" % self.color_planes, data) - except struct.error: - raise FormatError("tRNS chunk has incorrect length.") - - def _process_gAMA(self, data): - try: - self.gamma = struct.unpack("!L", data)[0] / 100000.0 - except struct.error: - raise FormatError("gAMA chunk has incorrect length.") - - def _process_sBIT(self, data): - self.sbit = data - if (self.colormap and len(data) != 3 or - not self.colormap and len(data) != self.planes): - raise FormatError("sBIT chunk has incorrect length.") - - def _process_pHYs(self, data): - # http://www.w3.org/TR/PNG/#11pHYs - self.phys = data - fmt = "!LLB" - if len(data) != struct.calcsize(fmt): - raise FormatError("pHYs chunk has incorrect length.") - self.x_pixels_per_unit, self.y_pixels_per_unit, unit = \ - struct.unpack(fmt, data) - self.unit_is_meter = bool(unit) - - def read(self, lenient=False): - """ - Read the PNG file and decode it. - Returns (`width`, `height`, `rows`, `info`). - - May use excessive memory. - - `rows` is a sequence of rows; - each row is a sequence of values. - - If the optional `lenient` argument evaluates to True, - checksum failures will raise warnings rather than exceptions. - """ - - def iteridat(): - """Iterator that yields all the ``IDAT`` chunks as strings.""" - while True: - type, data = self.chunk(lenient=lenient) - if type == b'IEND': - # http://www.w3.org/TR/PNG/#11IEND - break - if type != b'IDAT': - continue - # type == b'IDAT' - # http://www.w3.org/TR/PNG/#11IDAT - if self.colormap and not self.plte: - warnings.warn("PLTE chunk is required before IDAT chunk") - yield data - - self.preamble(lenient=lenient) - raw = decompress(iteridat()) - - if self.interlace: - def rows_from_interlace(): - """Yield each row from an interlaced PNG.""" - # It's important that this iterator doesn't read - # IDAT chunks until it yields the first row. - bs = bytearray(itertools.chain(*raw)) - arraycode = 'BH'[self.bitdepth > 8] - # Like :meth:`group` but - # producing an array.array object for each row. - values = self._deinterlace(bs) - vpr = self.width * self.planes - for i in range(0, len(values), vpr): - row = array(arraycode, values[i:i+vpr]) - yield row - rows = rows_from_interlace() - else: - rows = self._iter_bytes_to_values(self._iter_straight_packed(raw)) - info = dict() - for attr in 'greyscale alpha planes bitdepth interlace'.split(): - info[attr] = getattr(self, attr) - info['size'] = (self.width, self.height) - for attr in 'gamma transparent background'.split(): - a = getattr(self, attr, None) - if a is not None: - info[attr] = a - if getattr(self, 'x_pixels_per_unit', None): - info['physical'] = Resolution(self.x_pixels_per_unit, - self.y_pixels_per_unit, - self.unit_is_meter) - if self.plte: - info['palette'] = self.palette() - return self.width, self.height, rows, info - - def read_flat(self): - """ - Read a PNG file and decode it into a single array of values. - Returns (*width*, *height*, *values*, *info*). - - May use excessive memory. - - `values` is a single array. - - The :meth:`read` method is more stream-friendly than this, - because it returns a sequence of rows. - """ - - x, y, pixel, info = self.read() - arraycode = 'BH'[info['bitdepth'] > 8] - pixel = array(arraycode, itertools.chain(*pixel)) - return x, y, pixel, info - - def palette(self, alpha='natural'): - """ - Returns a palette that is a sequence of 3-tuples or 4-tuples, - synthesizing it from the ``PLTE`` and ``tRNS`` chunks. - These chunks should have already been processed (for example, - by calling the :meth:`preamble` method). - All the tuples are the same size: - 3-tuples if there is no ``tRNS`` chunk, - 4-tuples when there is a ``tRNS`` chunk. - - Assumes that the image is colour type - 3 and therefore a ``PLTE`` chunk is required. - - If the `alpha` argument is ``'force'`` then an alpha channel is - always added, forcing the result to be a sequence of 4-tuples. - """ - - if not self.plte: - raise FormatError( - "Required PLTE chunk is missing in colour type 3 image.") - plte = group(array('B', self.plte), 3) - if self.trns or alpha == 'force': - trns = array('B', self.trns or []) - trns.extend([255] * (len(plte) - len(trns))) - plte = list(map(operator.add, plte, group(trns, 1))) - return plte - - def asDirect(self): - """ - Returns the image data as a direct representation of - an ``x * y * planes`` array. - This removes the need for callers to deal with - palettes and transparency themselves. - Images with a palette (colour type 3) are converted to RGB or RGBA; - images with transparency (a ``tRNS`` chunk) are converted to - LA or RGBA as appropriate. - When returned in this format the pixel values represent - the colour value directly without needing to refer - to palettes or transparency information. - - Like the :meth:`read` method this method returns a 4-tuple: - - (*width*, *height*, *rows*, *info*) - - This method normally returns pixel values with - the bit depth they have in the source image, but - when the source PNG has an ``sBIT`` chunk it is inspected and - can reduce the bit depth of the result pixels; - pixel values will be reduced according to the bit depth - specified in the ``sBIT`` chunk. - PNG nerds should note a single result bit depth is - used for all channels: - the maximum of the ones specified in the ``sBIT`` chunk. - An RGB565 image will be rescaled to 6-bit RGB666. - - The *info* dictionary that is returned reflects - the `direct` format and not the original source image. - For example, an RGB source image with a ``tRNS`` chunk - to represent a transparent colour, - will start with ``planes=3`` and ``alpha=False`` for the - source image, - but the *info* dictionary returned by this method - will have ``planes=4`` and ``alpha=True`` because - an alpha channel is synthesized and added. - - *rows* is a sequence of rows; - each row being a sequence of values - (like the :meth:`read` method). - - All the other aspects of the image data are not changed. - """ - - self.preamble() - - # Simple case, no conversion necessary. - if not self.colormap and not self.trns and not self.sbit: - return self.read() - - x, y, pixels, info = self.read() - - if self.colormap: - info['colormap'] = False - info['alpha'] = bool(self.trns) - info['bitdepth'] = 8 - info['planes'] = 3 + bool(self.trns) - plte = self.palette() - - def iterpal(pixels): - for row in pixels: - row = [plte[x] for x in row] - yield array('B', itertools.chain(*row)) - pixels = iterpal(pixels) - elif self.trns: - # It would be nice if there was some reasonable way - # of doing this without generating a whole load of - # intermediate tuples. But tuples does seem like the - # easiest way, with no other way clearly much simpler or - # much faster. (Actually, the L to LA conversion could - # perhaps go faster (all those 1-tuples!), but I still - # wonder whether the code proliferation is worth it) - it = self.transparent - maxval = 2 ** info['bitdepth'] - 1 - planes = info['planes'] - info['alpha'] = True - info['planes'] += 1 - typecode = 'BH'[info['bitdepth'] > 8] - - def itertrns(pixels): - for row in pixels: - # For each row we group it into pixels, then form a - # characterisation vector that says whether each - # pixel is opaque or not. Then we convert - # True/False to 0/maxval (by multiplication), - # and add it as the extra channel. - row = group(row, planes) - opa = map(it.__ne__, row) - opa = map(maxval.__mul__, opa) - opa = list(zip(opa)) # convert to 1-tuples - yield array( - typecode, - itertools.chain(*map(operator.add, row, opa))) - pixels = itertrns(pixels) - targetbitdepth = None - if self.sbit: - sbit = struct.unpack('%dB' % len(self.sbit), self.sbit) - targetbitdepth = max(sbit) - if targetbitdepth > info['bitdepth']: - raise Error('sBIT chunk %r exceeds bitdepth %d' % - (sbit, self.bitdepth)) - if min(sbit) <= 0: - raise Error('sBIT chunk %r has a 0-entry' % sbit) - if targetbitdepth: - shift = info['bitdepth'] - targetbitdepth - info['bitdepth'] = targetbitdepth - - def itershift(pixels): - for row in pixels: - yield [p >> shift for p in row] - pixels = itershift(pixels) - return x, y, pixels, info - - def _as_rescale(self, get, targetbitdepth): - """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" - - width, height, pixels, info = get() - maxval = 2**info['bitdepth'] - 1 - targetmaxval = 2**targetbitdepth - 1 - factor = float(targetmaxval) / float(maxval) - info['bitdepth'] = targetbitdepth - - def iterscale(): - for row in pixels: - yield [int(round(x * factor)) for x in row] - if maxval == targetmaxval: - return width, height, pixels, info - else: - return width, height, iterscale(), info - - def asRGB8(self): - """ - Return the image data as an RGB pixels with 8-bits per sample. - This is like the :meth:`asRGB` method except that - this method additionally rescales the values so that - they are all between 0 and 255 (8-bit). - In the case where the source image has a bit depth < 8 - the transformation preserves all the information; - where the source image has bit depth > 8, then - rescaling to 8-bit values loses precision. - No dithering is performed. - Like :meth:`asRGB`, - an alpha channel in the source image will raise an exception. - - This function returns a 4-tuple: - (*width*, *height*, *rows*, *info*). - *width*, *height*, *info* are as per the :meth:`read` method. - - *rows* is the pixel data as a sequence of rows. - """ - - return self._as_rescale(self.asRGB, 8) - - def asRGBA8(self): - """ - Return the image data as RGBA pixels with 8-bits per sample. - This method is similar to :meth:`asRGB8` and :meth:`asRGBA`: - The result pixels have an alpha channel, *and* - values are rescaled to the range 0 to 255. - The alpha channel is synthesized if necessary - (with a small speed penalty). - """ - - return self._as_rescale(self.asRGBA, 8) - - def asRGB(self): - """ - Return image as RGB pixels. - RGB colour images are passed through unchanged; - greyscales are expanded into RGB triplets - (there is a small speed overhead for doing this). - - An alpha channel in the source image will raise an exception. - - The return values are as for the :meth:`read` method except that - the *info* reflect the returned pixels, not the source image. - In particular, - for this method ``info['greyscale']`` will be ``False``. - """ - - width, height, pixels, info = self.asDirect() - if info['alpha']: - raise Error("will not convert image with alpha channel to RGB") - if not info['greyscale']: - return width, height, pixels, info - info['greyscale'] = False - info['planes'] = 3 - - if info['bitdepth'] > 8: - def newarray(): - return array('H', [0]) - else: - def newarray(): - return bytearray([0]) - - def iterrgb(): - for row in pixels: - a = newarray() * 3 * width - for i in range(3): - a[i::3] = row - yield a - return width, height, iterrgb(), info - - def asRGBA(self): - """ - Return image as RGBA pixels. - Greyscales are expanded into RGB triplets; - an alpha channel is synthesized if necessary. - The return values are as for the :meth:`read` method except that - the *info* reflect the returned pixels, not the source image. - In particular, for this method - ``info['greyscale']`` will be ``False``, and - ``info['alpha']`` will be ``True``. - """ - - width, height, pixels, info = self.asDirect() - if info['alpha'] and not info['greyscale']: - return width, height, pixels, info - typecode = 'BH'[info['bitdepth'] > 8] - maxval = 2**info['bitdepth'] - 1 - maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width - - if info['bitdepth'] > 8: - def newarray(): - return array('H', maxbuffer) - else: - def newarray(): - return bytearray(maxbuffer) - - if info['alpha'] and info['greyscale']: - # LA to RGBA - def convert(): - for row in pixels: - # Create a fresh target row, then copy L channel - # into first three target channels, and A channel - # into fourth channel. - a = newarray() - convert_la_to_rgba(row, a) - yield a - elif info['greyscale']: - # L to RGBA - def convert(): - for row in pixels: - a = newarray() - convert_l_to_rgba(row, a) - yield a - else: - assert not info['alpha'] and not info['greyscale'] - # RGB to RGBA - - def convert(): - for row in pixels: - a = newarray() - convert_rgb_to_rgba(row, a) - yield a - info['alpha'] = True - info['greyscale'] = False - info['planes'] = 4 - return width, height, convert(), info - - -def decompress(data_blocks): - """ - `data_blocks` should be an iterable that - yields the compressed data (from the ``IDAT`` chunks). - This yields decompressed byte strings. - """ - - # Currently, with no max_length parameter to decompress, - # this routine will do one yield per IDAT chunk: Not very - # incremental. - d = zlib.decompressobj() - # Each IDAT chunk is passed to the decompressor, then any - # remaining state is decompressed out. - for data in data_blocks: - # :todo: add a max_length argument here to limit output size. - yield bytearray(d.decompress(data)) - yield bytearray(d.flush()) - - -def check_bitdepth_colortype(bitdepth, colortype): - """ - Check that `bitdepth` and `colortype` are both valid, - and specified in a valid combination. - Returns (None) if valid, raise an Exception if not valid. - """ - - if bitdepth not in (1, 2, 4, 8, 16): - raise FormatError("invalid bit depth %d" % bitdepth) - if colortype not in (0, 2, 3, 4, 6): - raise FormatError("invalid colour type %d" % colortype) - # Check indexed (palettized) images have 8 or fewer bits - # per pixel; check only indexed or greyscale images have - # fewer than 8 bits per pixel. - if colortype & 1 and bitdepth > 8: - raise FormatError( - "Indexed images (colour type %d) cannot" - " have bitdepth > 8 (bit depth %d)." - " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." - % (bitdepth, colortype)) - if bitdepth < 8 and colortype not in (0, 3): - raise FormatError( - "Illegal combination of bit depth (%d)" - " and colour type (%d)." - " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." - % (bitdepth, colortype)) - - -def is_natural(x): - """A non-negative integer.""" - try: - is_integer = int(x) == x - except (TypeError, ValueError): - return False - return is_integer and x >= 0 - - -def undo_filter_sub(filter_unit, scanline, previous, result): - """Undo sub filter.""" - - ai = 0 - # Loops starts at index fu. Observe that the initial part - # of the result is already filled in correctly with - # scanline. - for i in range(filter_unit, len(result)): - x = scanline[i] - a = result[ai] - result[i] = (x + a) & 0xff - ai += 1 - - -def undo_filter_up(filter_unit, scanline, previous, result): - """Undo up filter.""" - - for i in range(len(result)): - x = scanline[i] - b = previous[i] - result[i] = (x + b) & 0xff - - -def undo_filter_average(filter_unit, scanline, previous, result): - """Undo up filter.""" - - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = 0 - else: - a = result[ai] - b = previous[i] - result[i] = (x + ((a + b) >> 1)) & 0xff - ai += 1 - - -def undo_filter_paeth(filter_unit, scanline, previous, result): - """Undo Paeth filter.""" - - # Also used for ci. - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = c = 0 - else: - a = result[ai] - c = previous[ai] - b = previous[i] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - pr = a - elif pb <= pc: - pr = b - else: - pr = c - result[i] = (x + pr) & 0xff - ai += 1 - - -def convert_la_to_rgba(row, result): - for i in range(3): - result[i::4] = row[0::2] - result[3::4] = row[1::2] - - -def convert_l_to_rgba(row, result): - """ - Convert a grayscale image to RGBA. - This method assumes the alpha channel in result is - already correctly initialized. - """ - for i in range(3): - result[i::4] = row - - -def convert_rgb_to_rgba(row, result): - """ - Convert an RGB image to RGBA. - This method assumes the alpha channel in result is - already correctly initialized. - """ - for i in range(3): - result[i::4] = row[i::3] - - -# Only reason to include this in this module is that -# several utilities need it, and it is small. -def binary_stdin(): - """ - A sys.stdin that returns bytes. - """ - - return sys.stdin.buffer - - -def binary_stdout(): - """ - A sys.stdout that accepts bytes. - """ - - stdout = sys.stdout.buffer - - # On Windows the C runtime file orientation needs changing. - if sys.platform == "win32": - import msvcrt - import os - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - - return stdout - - -def cli_open(path): - if path == "-": - return binary_stdin() - return open(path, "rb") - - -def main(argv): - """ - Run command line PNG. - Which reports version. - """ - print(__version__, __file__) - - -if __name__ == '__main__': - try: - main(sys.argv) - except Error as e: - print(e, file=sys.stderr)