diff options
author | mid-kid <esteve.varela@gmail.com> | 2020-09-13 00:07:05 +0200 |
---|---|---|
committer | mid-kid <esteve.varela@gmail.com> | 2020-09-13 00:07:05 +0200 |
commit | 2e590367aa3248ae6fda00638ce09e7b052e3a75 (patch) | |
tree | 93147aca0b50b550183dc93b25be5733bc99bb61 | |
parent | c8d5687ac4fa64396104da4f3f04f0594c38dc6c (diff) | |
parent | 24d95f7de2da127a5d9ee71372c61be20a522549 (diff) |
Merge branch 'master' of github.com:mid-kid/pokepicross
94 files changed, 3304 insertions, 313 deletions
@@ -7,3 +7,7 @@ __pycache__ *.map *.sym !shim.sym +__pycache__ +*.pyc +/coverage.png +/picross.png diff --git a/doc/complete_save.txt b/doc/complete_save.txt index 15fc41c..f211f9f 100644 --- a/doc/complete_save.txt +++ b/doc/complete_save.txt @@ -1,3 +1,3 @@ D6E6-D721: Stage clear flags, set all to FF -DB19: Mew appear flag -DB1A: Mew pokedex flag +DB19: Mew appear flag, set to 01 +DB1A: Mew Pokédex flag, set to 01 diff --git a/doc/credits_bgs.png b/doc/credits_bgs.png Binary files differnew file mode 100644 index 0000000..0e84ee9 --- /dev/null +++ b/doc/credits_bgs.png diff --git a/doc/item_pics.png b/doc/item_pics.png Binary files differnew file mode 100644 index 0000000..6555c5f --- /dev/null +++ b/doc/item_pics.png diff --git a/doc/level_bgs.png b/doc/level_bgs.png Binary files differnew file mode 100644 index 0000000..a06277a --- /dev/null +++ b/doc/level_bgs.png diff --git a/doc/pokedex_pics.png b/doc/pokedex_pics.png Binary files differnew file mode 100644 index 0000000..c86d969 --- /dev/null +++ b/doc/pokedex_pics.png diff --git a/doc/safari_zone_album_pics.png b/doc/safari_zone_album_pics.png Binary files differnew file mode 100644 index 0000000..b34f684 --- /dev/null +++ b/doc/safari_zone_album_pics.png diff --git a/doc/screens.png b/doc/screens.png Binary files differnew file mode 100644 index 0000000..12b4da0 --- /dev/null +++ b/doc/screens.png diff --git a/doc/sgb_border.png b/doc/sgb_border.png Binary files differnew file mode 100644 index 0000000..3bad01d --- /dev/null +++ b/doc/sgb_border.png diff --git a/gfx/bill_walk.png b/gfx/bill_walk.png Binary files differdeleted file mode 100644 index 5eeb561..0000000 --- a/gfx/bill_walk.png +++ /dev/null diff --git a/gfx/bulbasaur_walk.png b/gfx/bulbasaur_walk.png Binary files differdeleted file mode 100644 index ff4300a..0000000 --- a/gfx/bulbasaur_walk.png +++ /dev/null diff --git a/gfx/charmander_walk.png b/gfx/charmander_walk.png Binary files differdeleted file mode 100644 index 88694f1..0000000 --- a/gfx/charmander_walk.png +++ /dev/null diff --git a/gfx/clefairy_walk.png b/gfx/clefairy_walk.png Binary files differdeleted file mode 100644 index 22d295f..0000000 --- a/gfx/clefairy_walk.png +++ /dev/null diff --git a/gfx/fonts/text_chars.png b/gfx/fonts/text_chars.png Binary files differnew file mode 100644 index 0000000..71f1b74 --- /dev/null +++ b/gfx/fonts/text_chars.png diff --git a/gfx/fonts/text_chars_2.png b/gfx/fonts/text_chars_2.png Binary files differnew file mode 100644 index 0000000..033c695 --- /dev/null +++ b/gfx/fonts/text_chars_2.png @@ -5,14 +5,4 @@ RGBGFXFLAGS := $(dir_build)/%.bin: %.png | $$(dir $$@) $(RGBGFX) $(RGBGFXFLAGS) -o $@ $< -$(dir_build)/gfx/pikachu_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/bulbasaur_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/charmander_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/squirtle_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/clefairy_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/jigglypuff_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/misty_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/mew_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/mew_silhouette_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/psyduck_walk.bin: RGBGFXFLAGS = -h -$(dir_build)/gfx/bill_walk.bin: RGBGFXFLAGS = -h +$(dir_build)/gfx/sprites/%.bin: RGBGFXFLAGS = -h diff --git a/gfx/jigglypuff_walk.png b/gfx/jigglypuff_walk.png Binary files differdeleted file mode 100644 index 75bd331..0000000 --- a/gfx/jigglypuff_walk.png +++ /dev/null diff --git a/gfx/levels/lv_0_home.png b/gfx/levels/lv_0_home.png Binary files differnew file mode 100644 index 0000000..a8fe43b --- /dev/null +++ b/gfx/levels/lv_0_home.png diff --git a/gfx/levels/lv_0_home_sgb.png b/gfx/levels/lv_0_home_sgb.png Binary files differnew file mode 100644 index 0000000..e05c0b9 --- /dev/null +++ b/gfx/levels/lv_0_home_sgb.png diff --git a/gfx/levels/lv_0_home_unused.png b/gfx/levels/lv_0_home_unused.png Binary files differnew file mode 100644 index 0000000..abb3b58 --- /dev/null +++ b/gfx/levels/lv_0_home_unused.png diff --git a/gfx/levels/lv_10_hanada_cave.png b/gfx/levels/lv_10_hanada_cave.png Binary files differnew file mode 100644 index 0000000..23ce5fd --- /dev/null +++ b/gfx/levels/lv_10_hanada_cave.png diff --git a/gfx/levels/lv_10_hanada_cave_sgb.png b/gfx/levels/lv_10_hanada_cave_sgb.png Binary files differnew file mode 100644 index 0000000..10991c7 --- /dev/null +++ b/gfx/levels/lv_10_hanada_cave_sgb.png diff --git a/gfx/levels/lv_10_hanada_cave_unused.png b/gfx/levels/lv_10_hanada_cave_unused.png Binary files differnew file mode 100644 index 0000000..89a5d92 --- /dev/null +++ b/gfx/levels/lv_10_hanada_cave_unused.png diff --git a/gfx/levels/lv_1_forest_zone.png b/gfx/levels/lv_1_forest_zone.png Binary files differnew file mode 100644 index 0000000..00e1ad1 --- /dev/null +++ b/gfx/levels/lv_1_forest_zone.png diff --git a/gfx/levels/lv_1_forest_zone_sgb.png b/gfx/levels/lv_1_forest_zone_sgb.png Binary files differnew file mode 100644 index 0000000..f0849ee --- /dev/null +++ b/gfx/levels/lv_1_forest_zone_sgb.png diff --git a/gfx/levels/lv_1_plain_zone.png b/gfx/levels/lv_1_plain_zone.png Binary files differnew file mode 100644 index 0000000..b344243 --- /dev/null +++ b/gfx/levels/lv_1_plain_zone.png diff --git a/gfx/levels/lv_1_tokiwa_forest.png b/gfx/levels/lv_1_tokiwa_forest.png Binary files differnew file mode 100644 index 0000000..f99830f --- /dev/null +++ b/gfx/levels/lv_1_tokiwa_forest.png diff --git a/gfx/levels/lv_1_tokiwa_forest_sgb.png b/gfx/levels/lv_1_tokiwa_forest_sgb.png Binary files differnew file mode 100644 index 0000000..adb9ad6 --- /dev/null +++ b/gfx/levels/lv_1_tokiwa_forest_sgb.png diff --git a/gfx/levels/lv_1_tokiwa_forest_unused.png b/gfx/levels/lv_1_tokiwa_forest_unused.png Binary files differnew file mode 100644 index 0000000..8587bce --- /dev/null +++ b/gfx/levels/lv_1_tokiwa_forest_unused.png diff --git a/gfx/levels/lv_2_lake_zone.png b/gfx/levels/lv_2_lake_zone.png Binary files differnew file mode 100644 index 0000000..2ca4f5a --- /dev/null +++ b/gfx/levels/lv_2_lake_zone.png diff --git a/gfx/levels/lv_2_lake_zone_sgb.png b/gfx/levels/lv_2_lake_zone_sgb.png Binary files differnew file mode 100644 index 0000000..acc30ef --- /dev/null +++ b/gfx/levels/lv_2_lake_zone_sgb.png diff --git a/gfx/levels/lv_2_mt_otsukimi.png b/gfx/levels/lv_2_mt_otsukimi.png Binary files differnew file mode 100644 index 0000000..3b32a9a --- /dev/null +++ b/gfx/levels/lv_2_mt_otsukimi.png diff --git a/gfx/levels/lv_2_mt_otsukimi_sgb.png b/gfx/levels/lv_2_mt_otsukimi_sgb.png Binary files differnew file mode 100644 index 0000000..7099c74 --- /dev/null +++ b/gfx/levels/lv_2_mt_otsukimi_sgb.png diff --git a/gfx/levels/lv_3_mountain_zone.png b/gfx/levels/lv_3_mountain_zone.png Binary files differnew file mode 100644 index 0000000..64fe922 --- /dev/null +++ b/gfx/levels/lv_3_mountain_zone.png diff --git a/gfx/levels/lv_3_mountain_zone_sgb.png b/gfx/levels/lv_3_mountain_zone_sgb.png Binary files differnew file mode 100644 index 0000000..4a6867e --- /dev/null +++ b/gfx/levels/lv_3_mountain_zone_sgb.png diff --git a/gfx/levels/lv_3_sea_cottage.png b/gfx/levels/lv_3_sea_cottage.png Binary files differnew file mode 100644 index 0000000..970780e --- /dev/null +++ b/gfx/levels/lv_3_sea_cottage.png diff --git a/gfx/levels/lv_3_sea_cottage_sgb.png b/gfx/levels/lv_3_sea_cottage_sgb.png Binary files differnew file mode 100644 index 0000000..6ae479e --- /dev/null +++ b/gfx/levels/lv_3_sea_cottage_sgb.png diff --git a/gfx/levels/lv_3_sea_cottage_unused.png b/gfx/levels/lv_3_sea_cottage_unused.png Binary files differnew file mode 100644 index 0000000..c874e18 --- /dev/null +++ b/gfx/levels/lv_3_sea_cottage_unused.png diff --git a/gfx/levels/lv_4_jungle_zone.png b/gfx/levels/lv_4_jungle_zone.png Binary files differnew file mode 100644 index 0000000..25ca289 --- /dev/null +++ b/gfx/levels/lv_4_jungle_zone.png diff --git a/gfx/levels/lv_4_jungle_zone_sgb.png b/gfx/levels/lv_4_jungle_zone_sgb.png Binary files differnew file mode 100644 index 0000000..aaca3b7 --- /dev/null +++ b/gfx/levels/lv_4_jungle_zone_sgb.png diff --git a/gfx/levels/lv_4_jungle_zone_unused.png b/gfx/levels/lv_4_jungle_zone_unused.png Binary files differnew file mode 100644 index 0000000..cf23786 --- /dev/null +++ b/gfx/levels/lv_4_jungle_zone_unused.png diff --git a/gfx/levels/lv_4_s_s_anne.png b/gfx/levels/lv_4_s_s_anne.png Binary files differnew file mode 100644 index 0000000..a2fc3ea --- /dev/null +++ b/gfx/levels/lv_4_s_s_anne.png diff --git a/gfx/levels/lv_4_s_s_anne_sgb.png b/gfx/levels/lv_4_s_s_anne_sgb.png Binary files differnew file mode 100644 index 0000000..6201a1c --- /dev/null +++ b/gfx/levels/lv_4_s_s_anne_sgb.png diff --git a/gfx/levels/lv_5_pokemon_tower.png b/gfx/levels/lv_5_pokemon_tower.png Binary files differnew file mode 100644 index 0000000..6ca360a --- /dev/null +++ b/gfx/levels/lv_5_pokemon_tower.png diff --git a/gfx/levels/lv_5_pokemon_tower_sgb.png b/gfx/levels/lv_5_pokemon_tower_sgb.png Binary files differnew file mode 100644 index 0000000..74e63fb --- /dev/null +++ b/gfx/levels/lv_5_pokemon_tower_sgb.png diff --git a/gfx/levels/lv_6_silph_company.png b/gfx/levels/lv_6_silph_company.png Binary files differnew file mode 100644 index 0000000..663e4df --- /dev/null +++ b/gfx/levels/lv_6_silph_company.png diff --git a/gfx/levels/lv_6_silph_company_sgb.png b/gfx/levels/lv_6_silph_company_sgb.png Binary files differnew file mode 100644 index 0000000..fc09845 --- /dev/null +++ b/gfx/levels/lv_6_silph_company_sgb.png diff --git a/gfx/levels/lv_6_silph_company_unused.png b/gfx/levels/lv_6_silph_company_unused.png Binary files differnew file mode 100644 index 0000000..7e45711 --- /dev/null +++ b/gfx/levels/lv_6_silph_company_unused.png diff --git a/gfx/levels/lv_7_cycling_road.png b/gfx/levels/lv_7_cycling_road.png Binary files differnew file mode 100644 index 0000000..69f2d12 --- /dev/null +++ b/gfx/levels/lv_7_cycling_road.png diff --git a/gfx/levels/lv_7_cycling_road_sgb.png b/gfx/levels/lv_7_cycling_road_sgb.png Binary files differnew file mode 100644 index 0000000..316e00f --- /dev/null +++ b/gfx/levels/lv_7_cycling_road_sgb.png diff --git a/gfx/levels/lv_7_cycling_road_unused.png b/gfx/levels/lv_7_cycling_road_unused.png Binary files differnew file mode 100644 index 0000000..c808429 --- /dev/null +++ b/gfx/levels/lv_7_cycling_road_unused.png diff --git a/gfx/levels/lv_8_power_plant.png b/gfx/levels/lv_8_power_plant.png Binary files differnew file mode 100644 index 0000000..8e6cd27 --- /dev/null +++ b/gfx/levels/lv_8_power_plant.png diff --git a/gfx/levels/lv_8_power_plant_sgb.png b/gfx/levels/lv_8_power_plant_sgb.png Binary files differnew file mode 100644 index 0000000..157d593 --- /dev/null +++ b/gfx/levels/lv_8_power_plant_sgb.png diff --git a/gfx/levels/lv_9_futago_island.png b/gfx/levels/lv_9_futago_island.png Binary files differnew file mode 100644 index 0000000..12ddd9f --- /dev/null +++ b/gfx/levels/lv_9_futago_island.png diff --git a/gfx/levels/lv_9_futago_island_sgb.png b/gfx/levels/lv_9_futago_island_sgb.png Binary files differnew file mode 100644 index 0000000..fe1ffda --- /dev/null +++ b/gfx/levels/lv_9_futago_island_sgb.png diff --git a/gfx/lv_2_mt_otsukimi.png b/gfx/lv_2_mt_otsukimi.png Binary files differdeleted file mode 100644 index ed81b54..0000000 --- a/gfx/lv_2_mt_otsukimi.png +++ /dev/null diff --git a/gfx/lv_2_mt_otsukimi_sgb.png b/gfx/lv_2_mt_otsukimi_sgb.png Binary files differdeleted file mode 100644 index a83f8e6..0000000 --- a/gfx/lv_2_mt_otsukimi_sgb.png +++ /dev/null diff --git a/gfx/mew_silhouette_walk.png b/gfx/mew_silhouette_walk.png Binary files differdeleted file mode 100644 index 826c485..0000000 --- a/gfx/mew_silhouette_walk.png +++ /dev/null diff --git a/gfx/mew_walk.png b/gfx/mew_walk.png Binary files differdeleted file mode 100644 index 8f07294..0000000 --- a/gfx/mew_walk.png +++ /dev/null diff --git a/gfx/misty_walk.png b/gfx/misty_walk.png Binary files differdeleted file mode 100644 index 080b5da..0000000 --- a/gfx/misty_walk.png +++ /dev/null diff --git a/gfx/pikachu_walk.png b/gfx/pikachu_walk.png Binary files differdeleted file mode 100644 index 2eb6208..0000000 --- a/gfx/pikachu_walk.png +++ /dev/null diff --git a/gfx/psyduck_walk.png b/gfx/psyduck_walk.png Binary files differdeleted file mode 100644 index 10e9011..0000000 --- a/gfx/psyduck_walk.png +++ /dev/null diff --git a/gfx/sprites/bill_walk.png b/gfx/sprites/bill_walk.png Binary files differnew file mode 100644 index 0000000..8793237 --- /dev/null +++ b/gfx/sprites/bill_walk.png diff --git a/gfx/sprites/bulbasaur_walk.png b/gfx/sprites/bulbasaur_walk.png Binary files differnew file mode 100644 index 0000000..c4dabb8 --- /dev/null +++ b/gfx/sprites/bulbasaur_walk.png diff --git a/gfx/sprites/charmander_walk.png b/gfx/sprites/charmander_walk.png Binary files differnew file mode 100644 index 0000000..0e7ba5b --- /dev/null +++ b/gfx/sprites/charmander_walk.png diff --git a/gfx/sprites/clefairy_walk.png b/gfx/sprites/clefairy_walk.png Binary files differnew file mode 100644 index 0000000..56658fa --- /dev/null +++ b/gfx/sprites/clefairy_walk.png diff --git a/gfx/sprites/jigglypuff_walk.png b/gfx/sprites/jigglypuff_walk.png Binary files differnew file mode 100644 index 0000000..fd4b6d5 --- /dev/null +++ b/gfx/sprites/jigglypuff_walk.png diff --git a/gfx/sprites/mew_silhouette_walk.png b/gfx/sprites/mew_silhouette_walk.png Binary files differnew file mode 100644 index 0000000..2ec4862 --- /dev/null +++ b/gfx/sprites/mew_silhouette_walk.png diff --git a/gfx/sprites/mew_walk.png b/gfx/sprites/mew_walk.png Binary files differnew file mode 100644 index 0000000..a44ef72 --- /dev/null +++ b/gfx/sprites/mew_walk.png diff --git a/gfx/sprites/misty_walk.png b/gfx/sprites/misty_walk.png Binary files differnew file mode 100644 index 0000000..ad30437 --- /dev/null +++ b/gfx/sprites/misty_walk.png diff --git a/gfx/sprites/pikachu_walk.png b/gfx/sprites/pikachu_walk.png Binary files differnew file mode 100644 index 0000000..a94479d --- /dev/null +++ b/gfx/sprites/pikachu_walk.png diff --git a/gfx/sprites/psyduck_walk.png b/gfx/sprites/psyduck_walk.png Binary files differnew file mode 100644 index 0000000..d0d2b50 --- /dev/null +++ b/gfx/sprites/psyduck_walk.png diff --git a/gfx/sprites/squirtle_walk.png b/gfx/sprites/squirtle_walk.png Binary files differnew file mode 100644 index 0000000..9ef4f7e --- /dev/null +++ b/gfx/sprites/squirtle_walk.png diff --git a/gfx/squirtle_walk.png b/gfx/squirtle_walk.png Binary files differdeleted file mode 100644 index eaacb77..0000000 --- a/gfx/squirtle_walk.png +++ /dev/null diff --git a/gfx/text_chars.png b/gfx/text_chars.png Binary files differdeleted file mode 100644 index 688e0d5..0000000 --- a/gfx/text_chars.png +++ /dev/null diff --git a/gfx/text_chars_2.png b/gfx/text_chars_2.png Binary files differdeleted file mode 100644 index 2fd9216..0000000 --- a/gfx/text_chars_2.png +++ /dev/null diff --git a/gfx/textbox_border.png b/gfx/textbox_border.png Binary files differindex 9177303..c0b4a41 100644 --- a/gfx/textbox_border.png +++ b/gfx/textbox_border.png diff --git a/include/macros.inc b/include/macros.inc index 2b5b6b8..86c3fb3 100644 --- a/include/macros.inc +++ b/include/macros.inc @@ -21,3 +21,64 @@ farjp: macro pop af jp _farjp endm + + +lb: macro + ld \1, ((\2) & $ff) << 8 | ((\3) & $ff) +endm + +ln: macro + ld \1, ((\2) & $f) << 4 | ((\3) & $f) +endm + +ld16: macro + ld a, LOW(\2) + ld [\1 + 0], a + ld a, HIGH(\2) + ld [\1 + 1], a +endm + + +const_def: macro +if _NARG >= 1 +const_value = \1 +else +const_value = 0 +endc +if _NARG >= 2 +const_inc = \2 +else +const_inc = 1 +endc +endm + +const: macro +\1 EQU const_value +const_value = const_value + const_inc +endm + +shift_const: macro +\1 EQU (1 << const_value) +const_value = const_value + const_inc +endm + +const_skip: macro +if _NARG >= 1 +const_value = const_value + const_inc * (\1) +else +const_value = const_value + const_inc +endc +endm + +const_next: macro +if (const_value > 0 && \1 < const_value) || (const_value < 0 && \1 > const_value) +fail "const_next cannot go backwards from {const_value} to \1" +else +const_value = \1 +endc +endm + +const_dw: macro + const \1 + dw \2 +endm diff --git a/source/bank_05.asm b/source/bank_05.asm index 15a7de6..57e91cb 100644 --- a/source/bank_05.asm +++ b/source/bank_05.asm @@ -6,28 +6,29 @@ SECTION "tutorial", ROMX[$4000], BANK[$05] tutorial:: ld a, [w_cdd2_jumptable_index] rst jumptable - dw function_05_400a - dw function_05_401b - dw function_05_4028 + const_def + const_dw TUTORIAL_0, function_05_400a + const_dw TUTORIAL_1, function_05_401b + const_dw TUTORIAL_2, function_05_4028 function_05_400a: xor a ld [w_d535], a ld a, $00 ld [w_d550], a - ld a, 2 + ld a, TUTORIAL_2 ld [w_cdd2_jumptable_index], a jp farcall_ret function_05_401b: ld a, $01 ld [w_d550], a - ld a, 2 + ld a, TUTORIAL_2 ld [w_cdd2_jumptable_index], a jp farcall_ret function_05_4028: - xor a + xor a ; TUTORIAL_0 ld [w_cdd2_jumptable_index], a ld a, [w_c357] and a @@ -52,10 +53,7 @@ function_05_4028: ld [w_textbox_width], a ld a, 141 ld [w_textbox_height], a - ld a, LOW(tutorial_message_00) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_00) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_00 ld a, $01 ld [w_d6ca], a ld a, [w_d61b] @@ -146,67 +144,69 @@ function_05_4028: tutorial_scene:: ld a, [w_tutorial_scene] rst jumptable - dw tutorial_scene_00 - dw tutorial_scene_01 - dw tutorial_scene_02 - dw tutorial_scene_03 - dw tutorial_scene_04 - dw tutorial_scene_05 - dw tutorial_scene_06 - dw tutorial_scene_07 - dw tutorial_scene_08 - dw tutorial_scene_09 - dw tutorial_scene_10 - dw tutorial_scene_11 - dw tutorial_scene_12 - dw tutorial_scene_13 - dw tutorial_scene_14 - dw tutorial_scene_15 - dw tutorial_scene_16 - dw tutorial_scene_17 - dw tutorial_scene_18 - dw tutorial_scene_19 - dw tutorial_scene_20 - dw tutorial_scene_21 - dw tutorial_scene_22 - dw tutorial_scene_23 - dw tutorial_scene_24 - dw tutorial_scene_25 - dw tutorial_scene_26 - dw tutorial_scene_27 - dw tutorial_scene_28 - dw tutorial_scene_29 - dw tutorial_scene_30 - dw tutorial_scene_31 - dw tutorial_scene_32 - dw tutorial_scene_33 - dw tutorial_scene_34 - dw tutorial_scene_35 - dw tutorial_scene_36 - dw tutorial_scene_37 - dw tutorial_scene_38 - dw tutorial_scene_39 - dw tutorial_scene_40 - dw tutorial_scene_41 - dw tutorial_scene_42 - dw tutorial_scene_43 - dw tutorial_scene_44 - dw tutorial_scene_45 - dw tutorial_scene_46 - dw tutorial_scene_47 - dw tutorial_scene_48 - dw tutorial_scene_49 - dw tutorial_scene_50 - dw tutorial_scene_51 - dw tutorial_scene_52 - dw tutorial_scene_53 - dw tutorial_scene_54 - dw tutorial_scene_55 - dw tutorial_scene_56 - dw tutorial_scene_57 - dw tutorial_scene_58 - dw tutorial_scene_59 - dw tutorial_scene_60 + const_def + const_dw TUTORIAL_SCENE_00, tutorial_scene_00 + const_dw TUTORIAL_SCENE_01, tutorial_scene_01 + const_dw TUTORIAL_SCENE_02, tutorial_scene_02 + const_dw TUTORIAL_SCENE_03, tutorial_scene_03 + const_dw TUTORIAL_SCENE_04, tutorial_scene_04 + const_dw TUTORIAL_SCENE_05, tutorial_scene_05 + const_dw TUTORIAL_SCENE_06, tutorial_scene_06 + const_dw TUTORIAL_SCENE_07, tutorial_scene_07 + const_dw TUTORIAL_SCENE_08, tutorial_scene_08 + const_dw TUTORIAL_SCENE_09, tutorial_scene_09 + const_dw TUTORIAL_SCENE_10, tutorial_scene_10 + const_dw TUTORIAL_SCENE_11, tutorial_scene_11 + const_dw TUTORIAL_SCENE_12, tutorial_scene_12 + const_dw TUTORIAL_SCENE_13, tutorial_scene_13 + const_dw TUTORIAL_SCENE_14, tutorial_scene_14 + const_dw TUTORIAL_SCENE_15, tutorial_scene_15 + const_dw TUTORIAL_SCENE_16, tutorial_scene_16 + const_dw TUTORIAL_SCENE_17, tutorial_scene_17 + const_dw TUTORIAL_SCENE_18, tutorial_scene_18 + const_dw TUTORIAL_SCENE_19, tutorial_scene_19 + const_dw TUTORIAL_SCENE_20, tutorial_scene_20 + const_dw TUTORIAL_SCENE_21, tutorial_scene_21 + const_dw TUTORIAL_SCENE_22, tutorial_scene_22 + const_dw TUTORIAL_SCENE_23, tutorial_scene_23 + const_dw TUTORIAL_SCENE_24, tutorial_scene_24 + const_dw TUTORIAL_SCENE_25, tutorial_scene_25 + const_dw TUTORIAL_SCENE_26, tutorial_scene_26 + const_dw TUTORIAL_SCENE_27, tutorial_scene_27 + const_dw TUTORIAL_SCENE_28, tutorial_scene_28 + const_dw TUTORIAL_SCENE_29, tutorial_scene_29 + const_dw TUTORIAL_SCENE_30, tutorial_scene_30 + const_dw TUTORIAL_SCENE_31, tutorial_scene_31 + const_dw TUTORIAL_SCENE_32, tutorial_scene_32 + const_dw TUTORIAL_SCENE_33, tutorial_scene_33 + const_dw TUTORIAL_SCENE_34, tutorial_scene_34 + const_dw TUTORIAL_SCENE_35, tutorial_scene_35 + const_dw TUTORIAL_SCENE_36, tutorial_scene_36 + const_dw TUTORIAL_SCENE_37, tutorial_scene_37 + const_dw TUTORIAL_SCENE_38, tutorial_scene_38 + const_dw TUTORIAL_SCENE_39, tutorial_scene_39 + const_dw TUTORIAL_SCENE_40, tutorial_scene_40 + const_dw TUTORIAL_SCENE_41, tutorial_scene_41 + const_dw TUTORIAL_SCENE_42, tutorial_scene_42 + const_dw TUTORIAL_SCENE_43, tutorial_scene_43 + const_dw TUTORIAL_SCENE_44, tutorial_scene_44 + const_dw TUTORIAL_SCENE_45, tutorial_scene_45 + const_dw TUTORIAL_SCENE_46, tutorial_scene_46 + const_dw TUTORIAL_SCENE_47, tutorial_scene_47 + const_dw TUTORIAL_SCENE_48, tutorial_scene_48 + const_dw TUTORIAL_SCENE_49, tutorial_scene_49 + const_dw TUTORIAL_SCENE_50, tutorial_scene_50 + const_dw TUTORIAL_SCENE_51, tutorial_scene_51 + const_dw TUTORIAL_SCENE_52, tutorial_scene_52 + const_dw TUTORIAL_SCENE_53, tutorial_scene_53 + const_dw TUTORIAL_SCENE_54, tutorial_scene_54 + const_dw TUTORIAL_SCENE_55, tutorial_scene_55 + const_dw TUTORIAL_SCENE_56, tutorial_scene_56 + const_dw TUTORIAL_SCENE_57, tutorial_scene_57 + const_dw TUTORIAL_SCENE_58, tutorial_scene_58 + const_dw TUTORIAL_SCENE_59, tutorial_scene_59 + const_dw TUTORIAL_SCENE_60, tutorial_scene_60 +NUM_TUTORIAL_SCENES EQU const_value tutorial_scene_00: ld a, [w_cdd2_jumptable_index] @@ -230,10 +230,7 @@ tutorial_scene_01: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_01) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_01) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_01 xor a ld [w_cdb0], a @@ -242,10 +239,7 @@ tutorial_scene_01: ld [w_cdac], a ld a, $0e ld [w_cdad], a - ld a, LOW(tutorial_data_42c8) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_42c8) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_42c8 ld a, $05 ld [w_cdb3], a ld a, 60 @@ -278,10 +272,7 @@ tutorial_scene_03: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_02) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_02) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_02 xor a ld [w_cdb0], a @@ -290,10 +281,7 @@ tutorial_scene_03: ld [w_cdac], a ld a, $31 ld [w_cdad], a - ld a, LOW(tutorial_data_42c8) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_42c8) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_42c8 ld a, $05 ld [w_cdb3], a ld a, 60 @@ -323,10 +311,7 @@ tutorial_scene_05: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_03) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_03) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_03 ld hl, w_tutorial_scene inc [hl] @@ -345,10 +330,7 @@ tutorial_scene_06: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_04) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_04) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_04 ld a, 10 ld [w_text_delay_timer], a @@ -384,10 +366,7 @@ tutorial_scene_09: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data farcall function_02_5b67 ld a, $00 ld [w_d6ca], a @@ -413,10 +392,7 @@ tutorial_scene_10: ld [w_text_delay_timer], a farcall function_02_5b67 - ld a, LOW(tutorial_message_05) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_05) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_05 ld hl, w_tutorial_scene inc [hl] @@ -437,10 +413,7 @@ tutorial_scene_11: farcall function_06_4964 farcall function_02_5b98 - ld a, LOW(tutorial_message_06) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_06) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_06 ld hl, w_tutorial_scene inc [hl] @@ -462,10 +435,7 @@ tutorial_scene_12: farcall function_02_5560 farcall function_02_5a82 - ld a, LOW(tutorial_message_07) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_07) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_07 xor a ld [w_cdb0], a @@ -474,10 +444,7 @@ tutorial_scene_12: ld [w_cdac], a ld a, $0e ld [w_cdad], a - ld a, LOW(tutorial_data_42c8) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_42c8) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_42c8 ld a, $05 ld [w_cdb3], a ld a, 60 @@ -507,10 +474,7 @@ tutorial_scene_14: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_08) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_08) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_08 ld hl, w_tutorial_scene inc [hl] @@ -529,10 +493,7 @@ tutorial_scene_15: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_09) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_09) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_09 xor a ld [w_cdb0], a @@ -541,10 +502,7 @@ tutorial_scene_15: ld [w_cdac], a ld a, $0e ld [w_cdad], a - ld a, LOW(tutorial_data_497d) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_497d) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_497d ld a, $05 ld [w_cdb3], a ld a, 60 @@ -577,10 +535,7 @@ tutorial_scene_17: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_10) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_10) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_10 ld hl, w_tutorial_scene inc [hl] @@ -595,10 +550,7 @@ tutorial_scene_18: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data ld a, $00 ld [w_cdd6], a ld a, $00 @@ -637,19 +589,13 @@ tutorial_scene_19: ld [w_cdac], a ld a, $31 ld [w_cdad], a - ld a, LOW(tutorial_data_42c8) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_42c8) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_42c8 ld a, $05 ld [w_cdb3], a ld a, 60 ld [w_text_delay_timer], a - ld a, LOW(tutorial_message_11) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_11) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_11 ld hl, w_tutorial_scene inc [hl] @@ -674,10 +620,7 @@ tutorial_scene_21: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_12) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_12) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_12 ld hl, w_tutorial_scene inc [hl] @@ -693,10 +636,7 @@ tutorial_scene_22: ld hl, function_02_5b77 farcall wait_press_a_blink - ld a, LOW(tutorial_message_13) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_13) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_13 call function_00_1085 farcall function_02_5b98 @@ -708,10 +648,7 @@ tutorial_scene_22: ld [w_cdac], a ld a, $37 ld [w_cdad], a - ld a, LOW(tutorial_data_4c91) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_4c91) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_4c91 ld a, $05 ld [w_cdb3], a ld a, 60 @@ -743,10 +680,7 @@ tutorial_scene_24: farcall text_delay jp nz, farcall_ret - ld a, LOW(tutorial_message_14) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_14) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_14 ld hl, w_tutorial_scene inc [hl] @@ -761,10 +695,7 @@ tutorial_scene_25: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data ld a, $00 ld [w_cdd6], a ld a, $01 @@ -796,10 +727,7 @@ tutorial_scene_26: ld hl, w_tutorial_scene inc [hl] - ld a, LOW(tutorial_message_15) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_15) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_15 call function_00_1085 farcall function_02_5b98 @@ -835,10 +763,7 @@ tutorial_scene_28: ld [w_ce00], a farcall function_3c_4377 - ld a, LOW(tutorial_message_16) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_16) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_16 ld hl, w_tutorial_scene inc [hl] @@ -857,10 +782,7 @@ tutorial_scene_29: ld hl, w_tutorial_scene inc [hl] - ld a, LOW(tutorial_message_17) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_17) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_17 call function_00_1085 farcall function_02_5b98 @@ -879,10 +801,7 @@ tutorial_scene_30: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data ld a, $03 ld [w_cdd6], a ld a, $01 @@ -915,10 +834,7 @@ tutorial_scene_32: call function_00_1085 - ld a, LOW(tutorial_message_18) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_18) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_18 ld a, $01 ld [w_d6ca], a @@ -940,10 +856,7 @@ tutorial_scene_33: ld hl, w_tutorial_scene inc [hl] - ld a, LOW(tutorial_message_19) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_19) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_19 call function_00_1085 farcall function_02_5b98 @@ -960,10 +873,7 @@ tutorial_scene_34: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data ld a, $03 ld [w_cdd6], a ld a, $01 @@ -998,10 +908,7 @@ tutorial_scene_35: farcall function_02_5a82 farcall function_29_7421 - ld a, LOW(tutorial_message_20) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_20) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_20 ld hl, w_tutorial_scene inc [hl] @@ -1016,15 +923,12 @@ tutorial_scene_36: ld a, $01 ld [w_d60f], a farcall function_29_5579 - ld a, $02 + ld a, TUTORIAL_2 ld [w_cdd2_jumptable_index], a xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data ld hl, w_tutorial_scene inc [hl] @@ -1056,10 +960,7 @@ tutorial_scene_37: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_21) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_21) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_21 ld a, $01 ld [w_c329], a @@ -1097,10 +998,7 @@ tutorial_scene_39: cp $3f jp c, farcall_ret - ld a, LOW(tutorial_message_22) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_22) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_22 ld hl, w_tutorial_scene inc [hl] @@ -1134,10 +1032,7 @@ tutorial_scene_41: cp $3f jp c, farcall_ret - ld a, LOW(tutorial_message_23) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_23) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_23 call function_00_1085 farcall function_02_5b98 @@ -1162,10 +1057,7 @@ tutorial_scene_42: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_24) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_24) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_24 ld hl, w_tutorial_scene inc [hl] @@ -1185,10 +1077,7 @@ tutorial_scene_43: farcall function_02_5b98 farcall function_02_5a82 - ld a, LOW(tutorial_message_25) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_25) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_25 ld hl, w_tutorial_scene inc [hl] @@ -1203,10 +1092,7 @@ tutorial_scene_44: xor a ld [w_d54a], a ld [w_d54b], a - ld a, LOW(.data) - ld [w_d54c + 0], a - ld a, HIGH(.data) - ld [w_d54c + 1], a + ld16 w_d54c, .data farcall function_02_5b67 xor a ld [w_cdd6], a @@ -1232,10 +1118,7 @@ tutorial_scene_45: ld hl, function_02_5b77 farcall wait_press_a_blink - ld a, LOW(tutorial_message_26) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_26) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_26 ld hl, w_tutorial_scene inc [hl] @@ -1259,10 +1142,7 @@ tutorial_scene_46: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_27) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_27) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_27 ld hl, w_tutorial_scene inc [hl] @@ -1281,10 +1161,7 @@ tutorial_scene_47: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_28) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_28) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_28 ld hl, w_tutorial_scene inc [hl] @@ -1317,10 +1194,7 @@ tutorial_scene_48: farcall function_02_5b98 farcall function_02_5a82 - ld a, LOW(tutorial_message_29) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_29) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_29 ld hl, w_tutorial_scene inc [hl] @@ -1339,10 +1213,7 @@ tutorial_scene_49: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_30) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_30) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_30 ld hl, w_tutorial_scene inc [hl] @@ -1361,10 +1232,7 @@ tutorial_scene_50: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_31) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_31) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_31 ld hl, w_tutorial_scene inc [hl] @@ -1383,10 +1251,7 @@ tutorial_scene_51: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_32) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_32) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_32 ld hl, w_tutorial_scene inc [hl] @@ -1411,10 +1276,7 @@ tutorial_scene_52: ld a, $27 ld [w_cdad], a - ld a, LOW(tutorial_data_5b4e) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_5b4e) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_5b4e ld a, $05 ld [w_cdb3], a jp farcall_ret @@ -1435,10 +1297,7 @@ tutorial_scene_53: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_33) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_33) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_33 ld hl, w_tutorial_scene inc [hl] @@ -1469,10 +1328,7 @@ tutorial_scene_54: ld [w_cdac], a ld a, $27 ld [w_cdad], a - ld a, LOW(tutorial_data_5b4e) - ld [w_cdb1 + 0], a - ld a, HIGH(tutorial_data_5b4e) - ld [w_cdb1 + 1], a + ld16 w_cdb1, tutorial_data_5b4e ld a, $05 ld [w_cdb3], a jp farcall_ret @@ -1490,10 +1346,7 @@ tutorial_scene_55: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_34) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_34) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_34 ld hl, w_tutorial_scene inc [hl] @@ -1512,10 +1365,7 @@ tutorial_scene_56: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_35) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_35) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_35 ld hl, w_tutorial_scene inc [hl] @@ -1534,10 +1384,7 @@ tutorial_scene_57: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_36) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_36) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_36 ld hl, w_tutorial_scene inc [hl] @@ -1556,10 +1403,7 @@ tutorial_scene_58: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_37) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_37) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_37 ld hl, w_tutorial_scene inc [hl] @@ -1578,10 +1422,7 @@ tutorial_scene_59: call function_00_1085 farcall function_02_5b98 - ld a, LOW(tutorial_message_38) - ld [w_text_cur_string + 0], a - ld a, HIGH(tutorial_message_38) - ld [w_text_cur_string + 1], a + ld16 w_text_cur_string, tutorial_message_38 ld hl, w_tutorial_scene inc [hl] diff --git a/source/bank_69.asm b/source/bank_69.asm index ccd64a6..f76b9d4 100644 --- a/source/bank_69.asm +++ b/source/bank_69.asm @@ -1,3 +1,18 @@ +SECTION "lv_1_tok", ROMX[$4800], BANK[$69] + +gfx_lv_1_tokiwa_forest:: +INCBIN "gfx/levels/lv_1_tokiwa_forest.bin" +.end:: + +gfx_lv_1_tokiwa_forest_sgb:: +INCBIN "gfx/levels/lv_1_tokiwa_forest_sgb.bin" +.end:: + +; The unused tree trunk tiles are darker. +gfx_lv_1_tokiwa_forest_duplicate:: +INCBIN "gfx/levels/lv_1_tokiwa_forest_unused.bin" +.end:: + SECTION "gfx_textbox_border", ROMX[$6000], BANK[$69] gfx_textbox_border:: INCBIN "gfx/textbox_border.bin" diff --git a/source/bank_6e.asm b/source/bank_6e.asm index 765e5b3..aad1d67 100644 --- a/source/bank_6e.asm +++ b/source/bank_6e.asm @@ -1,49 +1,49 @@ SECTION "bank6e", ROMX[$4000], BANK[$6e] gfx_text_chars_bw:: -INCBIN "gfx/text_chars_2.bin" +INCBIN "gfx/fonts/text_chars_2.bin" .end:: gfx_pikachu_walk:: -INCBIN "gfx/pikachu_walk.bin" +INCBIN "gfx/sprites/pikachu_walk.bin" .end:: gfx_bulbasaur_walk:: -INCBIN "gfx/bulbasaur_walk.bin" +INCBIN "gfx/sprites/bulbasaur_walk.bin" .end:: gfx_charmander_walk:: -INCBIN "gfx/charmander_walk.bin" +INCBIN "gfx/sprites/charmander_walk.bin" .end:: gfx_squirtle_walk:: -INCBIN "gfx/squirtle_walk.bin" +INCBIN "gfx/sprites/squirtle_walk.bin" .end:: gfx_clefairy_walk:: -INCBIN "gfx/clefairy_walk.bin" +INCBIN "gfx/sprites/clefairy_walk.bin" .end:: gfx_jigglypuff_walk:: -INCBIN "gfx/jigglypuff_walk.bin" +INCBIN "gfx/sprites/jigglypuff_walk.bin" .end:: gfx_misty_walk:: -INCBIN "gfx/misty_walk.bin" +INCBIN "gfx/sprites/misty_walk.bin" .end:: gfx_mew_walk:: -INCBIN "gfx/mew_walk.bin" +INCBIN "gfx/sprites/mew_walk.bin" .end:: gfx_mew_silhouette_walk:: -INCBIN "gfx/mew_silhouette_walk.bin" +INCBIN "gfx/sprites/mew_silhouette_walk.bin" .end:: gfx_psyduck_walk:: -INCBIN "gfx/psyduck_walk.bin" +INCBIN "gfx/sprites/psyduck_walk.bin" .end:: gfx_bill_walk:: -INCBIN "gfx/bill_walk.bin" +INCBIN "gfx/sprites/bill_walk.bin" .end:: diff --git a/source/bank_6f.asm b/source/bank_6f.asm index d4d908b..7240091 100644 --- a/source/bank_6f.asm +++ b/source/bank_6f.asm @@ -1,17 +1,17 @@ SECTION "bank6f", ROMX[$4000], BANK[$6f] gfx_text_chars:: -INCBIN "gfx/text_chars.bin" +INCBIN "gfx/fonts/text_chars.bin" .end:: gfx_lv_2_mt_otsukimi:: -INCBIN "gfx/lv_2_mt_otsukimi.bin" +INCBIN "gfx/levels/lv_2_mt_otsukimi.bin" .end:: gfx_lv_2_mt_otsukimi_sgb:: -INCBIN "gfx/lv_2_mt_otsukimi_sgb.bin" +INCBIN "gfx/levels/lv_2_mt_otsukimi_sgb.bin" .end:: gfx_lv_2_mt_otsukimi_duplicate:: -INCBIN "gfx/lv_2_mt_otsukimi.bin" +INCBIN "gfx/levels/lv_2_mt_otsukimi.bin" .end:: diff --git a/source/bank_70.asm b/source/bank_70.asm new file mode 100644 index 0000000..0d6ed32 --- /dev/null +++ b/source/bank_70.asm @@ -0,0 +1,13 @@ +SECTION "bank70", ROMX[$64c0], BANK[$70] + +gfx_lv_2_lake_zone:: +INCBIN "gfx/levels/lv_2_lake_zone.bin" +.end:: + +gfx_lv_2_lake_zone_sgb:: +INCBIN "gfx/levels/lv_2_lake_zone_sgb.bin" +.end:: + +gfx_lv_2_lake_zone_duplicate:: +INCBIN "gfx/levels/lv_2_lake_zone.bin" +.end:: diff --git a/source/bank_71.asm b/source/bank_71.asm new file mode 100644 index 0000000..430f639 --- /dev/null +++ b/source/bank_71.asm @@ -0,0 +1,13 @@ +SECTION "bank71", ROMX[$6400], BANK[$71] + +gfx_lv_3_mountain_zone:: +INCBIN "gfx/levels/lv_3_mountain_zone.bin" +.end:: + +gfx_lv_3_mountain_zone_sgb:: +INCBIN "gfx/levels/lv_3_mountain_zone_sgb.bin" +.end:: + +gfx_lv_3_mountain_zone_duplicate:: +INCBIN "gfx/levels/lv_3_mountain_zone.bin" +.end:: diff --git a/source/bank_72.asm b/source/bank_72.asm new file mode 100644 index 0000000..fd5f5f5 --- /dev/null +++ b/source/bank_72.asm @@ -0,0 +1,17 @@ +SECTION "bank72", ROMX[$4800], BANK[$72] + +; The Safari Zone level 1 was renamed from Forest Zone to Plain Zone, +; but only the CGB graphics were updated, leaving the old name in +; the SGB and duplicate CGB graphics. + +gfx_lv_1_plain_zone:: +INCBIN "gfx/levels/lv_1_plain_zone.bin" +.end:: + +gfx_lv_1_plain_zone_sgb:: +INCBIN "gfx/levels/lv_1_forest_zone_sgb.bin" +.end:: + +gfx_lv_1_plain_zone_duplicate:: +INCBIN "gfx/levels/lv_1_forest_zone.bin" +.end:: diff --git a/source/bank_73.asm b/source/bank_73.asm new file mode 100644 index 0000000..7108756 --- /dev/null +++ b/source/bank_73.asm @@ -0,0 +1,15 @@ +SECTION "bank73", ROMX[$4800], BANK[$73] + +gfx_lv_0_home:: +INCBIN "gfx/levels/lv_0_home.bin" +.end:: + +gfx_lv_0_home_sgb:: +INCBIN "gfx/levels/lv_0_home_sgb.bin" +.end:: + +; The edges of the unused house roof tiles are dark gray +; like the rest of the roof, not white. +gfx_lv_0_home_duplicate:: +INCBIN "gfx/levels/lv_0_home_unused.bin" +.end:: diff --git a/source/bank_74.asm b/source/bank_74.asm new file mode 100644 index 0000000..097f935 --- /dev/null +++ b/source/bank_74.asm @@ -0,0 +1,28 @@ +SECTION "bank74", ROMX[$4000], BANK[$74] + +gfx_lv_4_jungle_zone:: +INCBIN "gfx/levels/lv_4_jungle_zone.bin" +.end:: + +gfx_lv_4_jungle_zone_sgb:: +INCBIN "gfx/levels/lv_4_jungle_zone_sgb.bin" +.end:: + +; The edges of the unused tree trunk tiles are light and dark gray +; like the rest of the tree, not white and light gray. +gfx_lv_4_jungle_zone_duplicate:: +INCBIN "gfx/levels/lv_4_jungle_zone_unused.bin" +.end:: + +gfx_lv_3_sea_cottage:: +INCBIN "gfx/levels/lv_3_sea_cottage.bin" +.end:: + +gfx_lv_3_sea_cottage_sgb:: +INCBIN "gfx/levels/lv_3_sea_cottage_sgb.bin" +.end:: + +; The unused "3" in "3/10" is has less of a black border. +gfx_lv_3_sea_cottage_duplicate:: +INCBIN "gfx/levels/lv_3_sea_cottage_unused.bin" +.end:: diff --git a/source/bank_75.asm b/source/bank_75.asm new file mode 100644 index 0000000..0862024 --- /dev/null +++ b/source/bank_75.asm @@ -0,0 +1,25 @@ +SECTION "bank75", ROMX[$4000], BANK[$75] + +gfx_lv_4_s_s_anne:: +INCBIN "gfx/levels/lv_4_s_s_anne.bin" +.end:: + +gfx_lv_4_s_s_anne_sgb:: +INCBIN "gfx/levels/lv_4_s_s_anne_sgb.bin" +.end:: + +gfx_lv_4_s_s_anne_duplicate:: +INCBIN "gfx/levels/lv_4_s_s_anne.bin" +.end:: + +gfx_lv_5_pokemon_tower:: +INCBIN "gfx/levels/lv_5_pokemon_tower.bin" +.end:: + +gfx_lv_5_pokemon_tower_sgb:: +INCBIN "gfx/levels/lv_5_pokemon_tower_sgb.bin" +.end:: + +gfx_lv_5_pokemon_tower_duplicate:: +INCBIN "gfx/levels/lv_5_pokemon_tower.bin" +.end:: diff --git a/source/bank_76.asm b/source/bank_76.asm new file mode 100644 index 0000000..e4dbb0f --- /dev/null +++ b/source/bank_76.asm @@ -0,0 +1,27 @@ +SECTION "bank76", ROMX[$4000], BANK[$76] + +gfx_lv_6_silph_company:: +INCBIN "gfx/levels/lv_6_silph_company.bin" +.end:: + +gfx_lv_6_silph_company_sgb:: +INCBIN "gfx/levels/lv_6_silph_company_sgb.bin" +.end:: + +; The unused "6" in "6/10" is taller. +gfx_lv_6_silph_company_duplicate:: +INCBIN "gfx/levels/lv_6_silph_company_unused.bin" +.end:: + +gfx_lv_7_cycling_road:: +INCBIN "gfx/levels/lv_7_cycling_road.bin" +.end:: + +gfx_lv_7_cycling_road_sgb:: +INCBIN "gfx/levels/lv_7_cycling_road_sgb.bin" +.end:: + +; The unused "/10" in "7/10" and "LV" are taller. +gfx_lv_7_cycling_road_duplicate:: +INCBIN "gfx/levels/lv_7_cycling_road_unused.bin" +.end:: diff --git a/source/bank_77.asm b/source/bank_77.asm new file mode 100644 index 0000000..8874fb2 --- /dev/null +++ b/source/bank_77.asm @@ -0,0 +1,25 @@ +SECTION "bank77", ROMX[$4000], BANK[$77] + +gfx_lv_8_power_plant:: +INCBIN "gfx/levels/lv_8_power_plant.bin" +.end:: + +gfx_lv_8_power_plant_sgb:: +INCBIN "gfx/levels/lv_8_power_plant_sgb.bin" +.end:: + +gfx_lv_8_power_plant_duplicate:: +INCBIN "gfx/levels/lv_8_power_plant.bin" +.end:: + +gfx_lv_9_futago_island:: +INCBIN "gfx/levels/lv_9_futago_island.bin" +.end:: + +gfx_lv_9_futago_island_sgb:: +INCBIN "gfx/levels/lv_9_futago_island_sgb.bin" +.end:: + +gfx_lv_9_futago_island_duplicate:: +INCBIN "gfx/levels/lv_9_futago_island.bin" +.end:: diff --git a/source/bank_78.asm b/source/bank_78.asm new file mode 100644 index 0000000..f150592 --- /dev/null +++ b/source/bank_78.asm @@ -0,0 +1,15 @@ +SECTION "bank78", ROMX[$4000], BANK[$78] + +gfx_lv_10_hanada_cave:: +INCBIN "gfx/levels/lv_10_hanada_cave.bin" +.end:: + +gfx_lv_10_hanada_cave_sgb:: +INCBIN "gfx/levels/lv_10_hanada_cave_sgb.bin" +.end:: + +; The water behind cliff corner tiles is dark gray +; like the rest of the water, not white. +gfx_lv_10_hanada_cave_duplicate:: +INCBIN "gfx/levels/lv_10_hanada_cave_unused.bin" +.end:: diff --git a/tools/coverage.py b/tools/coverage.py new file mode 100644 index 0000000..3e57255 --- /dev/null +++ b/tools/coverage.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Usage: python3 coverage.py [picross.map] [coverage.png] + +Generate a PNG visualizing the space used by each bank in the ROM. +""" + +import sys +import png +from colorsys import hls_to_rgb + +from parsemap import MapReader + +def main(): + mapfile = sys.argv[1] if len(sys.argv) >= 2 else 'picross.map' + filename = sys.argv[2] if len(sys.argv) >= 3 else 'coverage.png' + + num_banks = 0x80 + bank_mask = 0x3FFF + bank_size = 0x4000 # bytes + + bpp = 8 # bytes per pixel + height = 256 # pixels + assert bank_size % bpp == 0 and (bank_size // bpp) % height == 0 + + pixels_per_bank = bank_size // bpp # 2048 pixels + bank_width = pixels_per_bank // height # 8 pixels + width = bank_width * num_banks # 1024 pixels + + r = MapReader() + with open(mapfile, 'r', encoding='utf-8') as f: + l = f.readlines() + r.read_map_data(l) + + hit_data = [] + default_bank_data = {'sections': [], 'used': 0, 'slack': bank_size} + for bank in range(num_banks): + hits = [0] * pixels_per_bank + data = r.bank_data['rom bank'].get(bank, default_bank_data) + for s in data['sections']: + if s['beg'] > s['end']: + continue + if s['beg'] == 0x0000 and s['end'] > 0xFFFF: + # https://github.com/rednex/rgbds/issues/515 + continue + beg = s['beg'] & bank_mask + end = s['end'] & bank_mask + for i in range(beg, end + 1): + hits[i // bpp] += 1 + hit_data.append(hits) + + pixels = [[(0xFF, 0xFF, 0xFF)] * width for _ in range(height)] + for bank, hits in enumerate(hit_data): + hue = 0 if not bank else 210 if bank % 2 else 270 + for i, h in enumerate(hits): + y = i // bank_width + x = i % bank_width + bank * bank_width + hls = (hue / 360.0, 1.0 - (h / bpp * (100 - 15)) / 100.0, 1.0) + rgb = tuple(int(c * 255) for c in hls_to_rgb(*hls)) + pixels[y][x] = rgb + + png_data = [tuple(c for pixel in row for c in pixel) for row in pixels] + with open(filename, 'wb') as f: + w = png.Writer(width, height) + w.write(f, png_data) + +if __name__ == '__main__': + main() diff --git a/tools/parsemap.py b/tools/parsemap.py new file mode 100644 index 0000000..d00023f --- /dev/null +++ b/tools/parsemap.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +import re + +class MapReader: + + # {'rom bank': { 0: { 'sections': [ { 'beg': 1234, + # 'end': 5678, + # 'name': 'Section001', + # 'symbols': [ { 'symbol': 'Function1234', + # 'address: 1234, + # }, + # ] + # }, + # ], + # 'used': 1234, + # 'slack': 4567, + # }, + # }, + # 'oam': { 'sections': [ { 'beg': 1234, + # 'end': 5678, + # 'name': 'Section002', + # 'symbols': [ { 'symbol': 'Data1234', + # 'address: 1234, + # }, + # ] + # }, + # ], + # 'used': 1234, + # 'slack': 4567, + # }, + # } + # + bank_data = {} + + bank_types = { + 'hram bank': { 'size': 0x80, 'banked': False, }, + 'oam bank' : { 'size': 0xA0, 'banked': False, }, + 'rom bank' : { 'size': 0x4000, 'banked': True, }, + 'sram bank': { 'size': 0x2000, 'banked': True, }, + 'vram bank': { 'size': 0x1000, 'banked': True, }, + 'wram bank': { 'size': 0x2000, 'banked': True, }, + } + + bank_aliases = { + 'hram': 'hram bank', + 'oam': 'oam bank', + 'rom0 bank': 'rom bank', + 'romx bank': 'rom bank', + 'wram0 bank': 'wram bank', + 'wramx bank': 'wram bank', + } + + # FSM states + INIT, BANK, SECTION = range(3) + + # $506D-$519A ($012E bytes) ["Type Matchups"] + section_header_regex = re.compile('\$([0-9A-Fa-f]{4})-\$([0-9A-Fa-f]{4,}) \(.*\)(?: \["(.*)"\])?') + # $506D = TypeMatchups + section_data_regex = re.compile('\$([0-9A-Fa-f]{4,}) = (.*)') + # $3ED2 bytes + slack_regex = re.compile('\$([0-9A-Fa-f]{4,}) bytes?') + + def __init__(self, *args, **kwargs): + self.__dict__.update(kwargs) + + def _parse_init(self, line): + + line = line.split(':', 1)[0] + parts = line.split(' #', 1) + + bank_type = parts[0].lower() + bank_type = self.bank_aliases.get(bank_type, bank_type) + + if (bank_type in self.bank_types): + self._cur_bank_name = bank_type + self._cur_bank_type = self.bank_types[self._cur_bank_name] + if (self._cur_bank_type['banked'] and len(parts) > 1): + parts[1] = parts[1].split(':', 1)[0] + parts[1] = parts[1].split(' ', 1)[0] + self._cur_bank = int(parts[1], 10) + if self._cur_bank_name not in self.bank_data: + self.bank_data[self._cur_bank_name] = {} + if self._cur_bank_type['banked']: + if self._cur_bank not in self.bank_data[self._cur_bank_name]: + self.bank_data[self._cur_bank_name][self._cur_bank] = {} + self._cur_data = self.bank_data[self._cur_bank_name][self._cur_bank] + else: + self._cur_data = self.bank_data[self._cur_bank_name] + + if ({} == self._cur_data): + self._cur_data['sections'] = [] + self._cur_data['used'] = 0 + self._cur_data['slack'] = self._cur_bank_type['size'] + return True + + return False + + def _parse_section_header(self, header): + + section_data = self.section_header_regex.match(header) + if section_data is not None: + beg = int(section_data.group(1), 16) + end = int(section_data.group(2), 16) + name = section_data.group(3) + self._cur_section = {'beg': beg, 'end': end, 'name': name, 'symbols': []} + self._cur_data['sections'].append(self._cur_section) + return True + return False + + def _parse_slack(self, data): + + slack_data = self.slack_regex.match(data) + slack_bytes = int(slack_data.group(1), 16) + self._cur_data['slack'] = slack_bytes + + used_bytes = 0 + + for s in self._cur_data['sections']: + used_bytes += s['end'] - s['beg'] + 1 + + self._cur_data['used'] = used_bytes + + def read_map_data(self, map): + + if type(map) is str: + map = map.split('\n') + + self._state = MapReader.INIT + self._cur_bank_name = '' + self._cur_bank_type = {} + self._cur_bank = 0 + self._cur_data = {} + + for line in map: + + line = line.rstrip() + if (MapReader.INIT == self._state): + + if (self._parse_init(line)): + self._state = MapReader.BANK + + elif (MapReader.BANK == self._state or MapReader.SECTION == self._state): + + if ('' == line): + self._state = MapReader.INIT + else: + + line = line.lstrip() + parts = line.split(': ', 1) + + if (MapReader.SECTION == self._state): + section_data = self.section_data_regex.match(parts[0]) + if section_data is not None: + address = int(section_data.group(1), 16) + name = section_data.group(2) + self._cur_section['symbols'].append({'name': name, 'address': address}) + continue + + if ('SECTION' == parts[0]): + if (self._parse_section_header(parts[1])): + self._state = MapReader.SECTION + elif ('SLACK' == parts[0]): + self._parse_slack(parts[1]) + self._state = MapReader.INIT + elif ('EMPTY' == parts[0]): + self._cur_data = {'sections': [], 'used': 0, 'slack': self._cur_bank_type['size']} + self._state = MapReader.INIT + + else: + pass + + for k, v in self.bank_data.items(): + if (self.bank_types[k]['banked']): + for _, vv in v.items(): + vv['sections'].sort(key=lambda x: x['beg']) + for vvv in vv['sections']: + vvv['symbols'].sort(key=lambda x: x['address']) + else: + v['sections'].sort(key=lambda x: x['beg']) + for vv in v['sections']: + vv['symbols'].sort(key=lambda x: x['address']) diff --git a/tools/png.py b/tools/png.py new file mode 100644 index 0000000..db6da12 --- /dev/null +++ b/tools/png.py @@ -0,0 +1,2650 @@ +#!/usr/bin/env python + +from __future__ import print_function + +# png.py - PNG encoder/decoder in pure Python +# +# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org> +# Portions Copyright (C) 2009 David Jones <drj@pobox.com> +# And probably portions Copyright (C) 2006 Nicko van Someren <nicko@nicko.org> +# +# 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. + +""" +Pure Python PNG Reader/Writer + +This Python module implements support for PNG images (see PNG +specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads +and writes PNG files with all allowable bit depths +(1/2/4/8/16/24/32/48/64 bits per pixel) and colour combinations: +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). +Adam7 interlacing is supported for reading and +writing. A number of optional chunks can be specified (when writing) +and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. + +For help, type ``import png; help(png)`` in your python interpreter. + +A good place to start is the :class:`Reader` and :class:`Writer` +classes. + +Requires Python 2.3. Limited support is available for Python 2.2, but +not everything works. Best with Python 2.4 and higher. Installation is +trivial, but see the ``README.txt`` file (with the source distribution) +for details. + +This file can also be used as a command-line utility to convert +`Netpbm <http://netpbm.sourceforge.net/>`_ PNM files to PNG, and the +reverse conversion from PNG to PNM. The interface is similar to that +of the ``pnmtopng`` program from Netpbm. Type ``python png.py --help`` +at the shell prompt for usage and a list of options. + +A note on 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. + +The major colour models supported by PNG (and hence by PyPNG) are: +greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes +referred to using the abbreviations: L, RGB, LA, RGBA. In this case +each letter abbreviates a single channel: *L* is for Luminance or Luma +or Lightness which is the channel used in greyscale images; *R*, *G*, +*B* stand for Red, Green, Blue, the components of a colour image; *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). + +A note on formats +----------------- + +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. Generally this module uses one of +three formats called "flat row flat pixel", "boxed row flat pixel", and +"boxed row boxed pixel". Basically the concern is whether each pixel +and each row comes in its own little tuple (box), or not. + +Consider an image that is 3 pixels wide by 2 pixels high, and each pixel +has RGB components: + +Boxed row flat pixel:: + + 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 most common format used, 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 (from +the array module), and the outer list is sometimes an iterator rather +than an explicit list (so that streaming is possible). + +Flat row flat pixel:: + + [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. + +Boxed row boxed pixel:: + + list([ (R,G,B), (R,G,B), (R,G,B) ], + [ (R,G,B), (R,G,B), (R,G,B) ]) + +Each row appears in its own list, but each pixel also appears in its own +tuple. A serious memory burn in Python. + +In all cases the top row comes first, and for 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 a fourth format, 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 essentially the same as boxed row flat pixel; when the +bit depth is less than 8, several pixels are packed into each byte; +when the bit depth is 16 (the only value more than 8 that is supported +by the PNG image format) 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). + +And now, my famous members +-------------------------- +""" + +__version__ = "0.0.18" + +import itertools +import math +# http://www.python.org/doc/2.4.4/lib/module-operator.html +import operator +import struct +import sys +# http://www.python.org/doc/2.4.4/lib/module-warnings.html +import warnings +import zlib + +from array import array +from functools import reduce + +try: + # `cpngfilters` is a Cython module: it must be compiled by + # Cython for this import to work. + # If this import does work, then it overrides pure-python + # filtering functions defined later in this file (see `class + # pngfilters`). + import cpngfilters as pngfilters +except ImportError: + pass + + +__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) + +_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 group(s, n): + # See http://www.python.org/doc/2.6/library/functions.html#zip + return list(zip(*[iter(s)]*n)) + +def isarray(x): + return isinstance(x, array) + +def tostring(row): + return row.tostring() + +def interleave_planes(ipixels, apixels, ipsize, apsize): + """ + Interleave (colour) planes, e.g. RGB + A = RGBA. + + Return an array of pixels consisting of the `ipsize` elements of + data from each pixel in `ipixels` followed by the `apsize` elements + of data from each pixel in `apixels`. Conventionally `ipixels` + and `apixels` are byte arrays so the sizes are bytes, but it + actually works with any arrays of the same type. The returned + array is the same type as the input arrays which should be the + same type as each other. + """ + + itotal = len(ipixels) + atotal = len(apixels) + newtotal = itotal + atotal + newpsize = ipsize + apsize + # Set up the output buffer + # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 + out = array(ipixels.typecode) + # It's annoying that there is no cheap way to set the array size :-( + out.extend(ipixels) + out.extend(apixels) + # Interleave in the pixel data + for i in range(ipsize): + out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] + for i in range(apsize): + out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] + return out + +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 ValueError("a palette must have between 1 and 256 entries") + seen_triple = False + for i,t in enumerate(p): + if len(t) not in (3,4): + raise ValueError( + "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 ValueError( + "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 ValueError( + "palette entry %d: values must be integer: 0 <= x <= 255" % i) + return p + +def check_sizes(size, width, height): + """Check that these arguments, in supplied, are consistent. + Return a (width, height) pair. + """ + + if not size: + return width, height + + if len(size) != 2: + raise ValueError( + "size argument should be a pair (width, height)") + if width is not None and width != size[0]: + raise ValueError( + "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 ValueError( + "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 bar 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 ValueError("%s for greyscale must be 1-tuple" % + which) + if not isinteger(c[0]): + raise ValueError( + "%s colour for greyscale must be integer" % which) + else: + if not (len(c) == 3 and + isinteger(c[0]) and + isinteger(c[1]) and + isinteger(c[2])): + raise ValueError( + "%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 ChunkError(FormatError): + pass + + +class Writer: + """ + PNG encoder in pure Python. + """ + + def __init__(self, width=None, height=None, + size=None, + greyscale=False, + alpha=False, + bitdepth=8, + palette=None, + transparent=None, + background=None, + gamma=None, + compression=None, + interlace=False, + bytes_per_sample=None, # deprecated + 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 + Input data is greyscale, not RGB. + alpha + Input data has alpha channel (RGBA or LA). + bitdepth + Bit depth: from 1 to 16. + 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*). + + `greyscale` and `alpha` are booleans that specify whether + an image is greyscale (or colour), and whether it has an + alpha channel (or not). + + `bitdepth` specifies the bit depth of the source pixel values. + Each source pixel value must be an integer between 0 and + ``2**bitdepth-1``. 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. When `bitdepth` is not one of these values, + 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 details of which bit depth / colour model combinations the + PNG file format supports directly, are somewhat arcane + (refer to the PNG specification for full details). Briefly: + "small" bit depths (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 press the + `bitdepth` argument into service 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 set; `alpha` must not be set; + `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. If both 4-tuples and 3-tuples + appear in the sequence then all the 4-tuples 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 three integer values for red, green, blue, or + a simple integer (or singleton tuple) 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 specify + compression, with 9 being "more compressed" (usually smaller + and slower, but it doesn't always work out that way). 0 means + no compression. -1 and ``None`` both mean that the default + level of compession will be picked by the ``zlib`` module + (which is generally acceptable). + + If `interlace` is true then an interlaced image is created + (using PNG's so far only interace method, *Adam7*). This does + not affect how the pixels should be presented to the encoder, + 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 width <= 0 or height <= 0: + raise ValueError("width and height must be greater than zero") + if not isinteger(width) or not isinteger(height): + raise ValueError("width and height must be integers") + # http://www.w3.org/TR/PNG/#7Integers-and-byte-order + if width > 2**32-1 or height > 2**32-1: + raise ValueError("width and height cannot exceed 2**32-1") + + if alpha and transparent is not None: + raise ValueError( + "transparent colour not allowed with alpha channel") + + if bytes_per_sample is not None: + warnings.warn('please use bitdepth instead of bytes_per_sample', + DeprecationWarning) + if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): + raise ValueError( + "bytes per sample must be .125, .25, .5, 1, or 2") + bitdepth = int(8*bytes_per_sample) + del bytes_per_sample + if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: + raise ValueError("bitdepth (%r) must be a positive integer <= 16" % + bitdepth) + + self.rescale = None + palette = check_palette(palette) + if palette: + if bitdepth not in (1,2,4,8): + raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") + if transparent is not None: + raise ValueError("transparent and palette not compatible") + if alpha: + raise ValueError("alpha and palette not compatible") + if greyscale: + raise ValueError("greyscale and palette not compatible") + else: + # No palette, check for sBIT chunk generation. + if alpha or not greyscale: + if bitdepth not in (8,16): + targetbitdepth = (8,16)[bitdepth > 8] + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + else: + assert greyscale + assert not alpha + if bitdepth not in (1,2,4,8,16): + if bitdepth > 8: + targetbitdepth = 16 + elif bitdepth == 3: + targetbitdepth = 4 + else: + assert bitdepth in (5,6,7) + targetbitdepth = 8 + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + + if bitdepth < 8 and (alpha or not greyscale and not palette): + raise ValueError( + "bitdepth < 8 only permitted with greyscale or palette") + if bitdepth > 8 and palette: + raise ValueError( + "bit depth must be 8 or less for images with 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 = bool(greyscale) + self.alpha = bool(alpha) + self.colormap = bool(palette) + 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 = (3,1)[self.greyscale or self.colormap] + self.planes = self.color_planes + self.alpha + # :todo: fix for bitdepth < 8 + self.psize = (self.bitdepth/8) * self.planes + + def make_palette(self): + """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 = array('B') + t = array('B') + + for x in self.palette: + p.extend(x[0:3]) + if len(x) > 3: + t.append(x[3]) + p = tostring(p) + t = tostring(t) + if t: + return p,t + return p,None + + def write(self, outfile, rows): + """Write a PNG image to the output file. `rows` should be + an iterable that yields each row in boxed row flat pixel + format. 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 will require the entire image to be in working + memory. + """ + + if self.interlace: + fmt = 'BH'[self.bitdepth > 8] + a = array(fmt, itertools.chain(*rows)) + return self.write_array(outfile, a) + + nrows = self.write_passes(outfile, rows) + if nrows != self.height: + raise ValueError( + "rows supplied (%d) does not match height (%d)" % + (nrows, self.height)) + + def write_passes(self, outfile, rows, packed=False): + """ + 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, but for interlaced + images the rows should have already been interlaced before + passing them to this function. + + `rows` should be an iterable that yields each row. When + `packed` is ``False`` the rows should be in boxed row flat pixel + format; when `packed` is ``True`` each row should be a packed + sequence of bytes. + """ + + # 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, + *[self.rescale[0]]*self.planes)) + + # :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 = self.make_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: + write_chunk(outfile, b'tRNS', + struct.pack("!1H", *self.transparent)) + else: + write_chunk(outfile, b'tRNS', + struct.pack("!3H", *self.transparent)) + + # http://www.w3.org/TR/PNG/#11bKGD + if self.background is not None: + if self.greyscale: + write_chunk(outfile, b'bKGD', + struct.pack("!1H", *self.background)) + else: + write_chunk(outfile, b'bKGD', + struct.pack("!3H", *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)) + + # http://www.w3.org/TR/PNG/#11IDAT + if self.compression is not None: + compressor = zlib.compressobj(self.compression) + else: + compressor = zlib.compressobj() + + # Choose an extend function based on the bitdepth. The extend + # function packs/decomposes the pixel values into bytes and + # stuffs them onto the data array. + data = array('B') + if self.bitdepth == 8 or packed: + extend = data.extend + elif self.bitdepth == 16: + # Decompose into bytes + def extend(sl): + fmt = '!%dH' % len(sl) + data.extend(array('B', struct.pack(fmt, *sl))) + else: + # Pack into bytes + assert self.bitdepth < 8 + # samples per byte + spb = int(8/self.bitdepth) + def extend(sl): + a = array('B', sl) + # Adding padding bytes so we can group into a whole + # number of spb-tuples. + l = float(len(a)) + extra = math.ceil(l / float(spb))*spb - l + a.extend([0]*int(extra)) + # Pack into bytes + l = group(a, spb) + l = [reduce(lambda x,y: + (x << self.bitdepth) + y, e) for e in l] + data.extend(l) + if self.rescale: + oldextend = extend + factor = \ + float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1) + def extend(sl): + oldextend([int(round(factor*x)) for x in sl]) + + # Build the first row, testing mostly to see if we need to + # changed the extend function to cope with NumPy integer types + # (they cause our ordinary definition of extend to fail, so we + # wrap it). See + # http://code.google.com/p/pypng/issues/detail?id=44 + enumrows = enumerate(rows) + del rows + + # First row's filter type. + data.append(0) + # :todo: Certain exceptions in the call to ``.next()`` or the + # following try would indicate no row data supplied. + # Should catch. + i,row = next(enumrows) + try: + # If this fails... + extend(row) + except: + # ... try a version that converts the values to int first. + # Not only does this work for the (slightly broken) NumPy + # types, there are probably lots of other, unknown, "nearly" + # int types it works for. + def wrapmapint(f): + return lambda sl: f([int(x) for x in sl]) + extend = wrapmapint(extend) + del wrapmapint + extend(row) + + for i,row in enumrows: + # 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) + extend(row) + if len(data) > self.chunk_limit: + compressed = compressor.compress(tostring(data)) + if len(compressed): + write_chunk(outfile, b'IDAT', compressed) + # Because of our very witty definition of ``extend``, + # above, we must re-use the same ``data`` object. Hence + # we use ``del`` to empty this one, rather than create a + # fresh one (which would be my natural FP instinct). + del data[:] + if len(data): + compressed = compressor.compress(tostring(data)) + else: + compressed = b'' + 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_array(self, outfile, pixels): + """ + Write an array in flat row flat pixel format as a PNG file on + the output file. See also :meth:`write` method. + """ + + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def write_packed(self, outfile, rows): + """ + Write PNG file to `outfile`. The pixel data comes from `rows` + which should be in boxed row packed format. Each row should be + a sequence of packed bytes. + + Technically, 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. + + This method should not be used when the source image bit depth + is not one naturally supported by PNG; the bit depth should be + 1, 2, 4, 8, or 16. + """ + + if self.rescale: + raise Error("write_packed method not suitable for bit depth %d" % + self.rescale[0]) + return self.write_passes(outfile, rows, packed=True) + + def convert_pnm(self, infile, outfile): + """ + Convert a PNM file containing raw pixel data into a PNG file + with the parameters set in the writer object. Works for + (binary) PGM, PPM, and PAM formats. + """ + + if self.interlace: + pixels = array('B') + pixels.fromfile(infile, + (self.bitdepth/8) * self.color_planes * + self.width * self.height) + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.file_scanlines(infile)) + + def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): + """ + Convert a PPM and PGM file containing raw pixel data into a + PNG outfile with the parameters set in the writer object. + """ + pixels = array('B') + pixels.fromfile(ppmfile, + (self.bitdepth/8) * self.color_planes * + self.width * self.height) + apixels = array('B') + apixels.fromfile(pgmfile, + (self.bitdepth/8) * + self.width * self.height) + pixels = interleave_planes(pixels, apixels, + (self.bitdepth/8) * self.color_planes, + (self.bitdepth/8)) + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def file_scanlines(self, infile): + """ + Generates boxed rows in flat pixel format, from the input file + `infile`. It assumes that the input file is in a "Netpbm-like" + binary format, and is positioned at the beginning of the first + pixel. The number of pixels to read is taken from the image + dimensions (`width`, `height`, `planes`) and the number of bytes + per value is implied by the image `bitdepth`. + """ + + # Values per row + vpr = self.width * self.planes + row_bytes = vpr + if self.bitdepth > 8: + assert self.bitdepth == 16 + row_bytes *= 2 + fmt = '>%dH' % vpr + def line(): + return array('H', struct.unpack(fmt, infile.read(row_bytes))) + else: + def line(): + scanline = array('B', infile.read(row_bytes)) + return scanline + for y in range(self.height): + yield line() + + def array_scanlines(self, pixels): + """ + Generates boxed rows (flat pixels) from flat rows (flat pixels) + in an array. + """ + + # 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 in flat row flat pixel format. The + generator yields each scanline of the reduced passes in turn, in + boxed row flat pixel format. + """ + + # http://www.w3.org/TR/PNG/#8InterlaceMethods + # Array type. + fmt = 'BH'[self.bitdepth > 8] + # Value per row + vpr = self.width * self.planes + for xstart, ystart, xstep, ystep in _adam7: + if xstart >= self.width: + continue + # Pixels per row (of reduced image) + ppr = int(math.ceil((self.width-xstart)/float(xstep))) + # number of values in reduced image row. + row_len = ppr*self.planes + for y in range(ystart, self.height, ystep): + if xstep == 1: + offset = y * vpr + yield pixels[offset:offset+vpr] + else: + row = array(fmt) + # There's no easier way to set the length of an array + row.extend(pixels[0:row_len]) + offset = y * vpr + xstart * 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. + """ + + # 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 filter_scanline(type, line, fo, prev=None): + """Apply a scanline filter to a scanline. `type` specifies the + filter type (0 to 4); `line` specifies the current (unfiltered) + scanline as a sequence of bytes; `prev` specifies the previous + (unfiltered) scanline as a sequence of bytes. `fo` specifies the + filter offset; normally this is size of a pixel in bytes (the number + of bytes per sample times the number of channels), but when this is + < 1 (for bit depths < 8) then the filter offset is 1. + """ + + assert 0 <= type < 5 + + # The output array. Which, pathetically, we extend one-byte at a + # time (fortunately this is linear). + out = array('B', [type]) + + def sub(): + ai = -fo + for x in line: + if ai >= 0: + x = (x - line[ai]) & 0xff + out.append(x) + ai += 1 + def up(): + for i,x in enumerate(line): + x = (x - prev[i]) & 0xff + out.append(x) + def average(): + ai = -fo + for i,x in enumerate(line): + if ai >= 0: + x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff + else: + x = (x - (prev[i] >> 1)) & 0xff + out.append(x) + ai += 1 + def paeth(): + # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth + ai = -fo # also used for ci + for i,x in enumerate(line): + a = 0 + b = prev[i] + c = 0 + + if ai >= 0: + a = line[ai] + c = prev[ai] + 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 + + x = (x - Pr) & 0xff + out.append(x) + ai += 1 + + if not prev: + # We're on the first line. Some of the filters can be reduced + # to simpler cases which makes handling the line "off the top" + # of the image simpler. "up" becomes "none"; "paeth" becomes + # "left" (non-trivial, but true). "average" needs to be handled + # specially. + if type == 2: # "up" + type = 0 + elif type == 3: + prev = [0]*len(line) + elif type == 4: # "paeth" + type = 1 + if type == 0: + out.extend(line) + elif type == 1: + sub() + elif type == 2: + up() + elif type == 3: + average() + else: # type == 4 + paeth() + return out + + +def from_array(a, mode=None, info={}): + """Create a PNG :class:`Image` object from a 2- or 3-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. For a 3 dimensional + array the first axis is the height; the second axis is the width; + and the third axis is the channel number. Thus an RGB image that is + 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 + dimensional arrays the first axis is the height, but the second axis + is ``width*channels``, so an RGB image that is 16 pixels high and 8 + wide will use 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; ``len(a[0][0])`` gives the + third dimension, unless an exception is raised in which case a + 2-dimensional array is assumed. 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, for example it is a plain Python + list of lists, as above, 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. + bitdepth = None + try: + # Assign the 'L' or 'RGBA' part to `gotmode`. + if mode.startswith('L'): + gotmode = 'L' + mode = mode[1:] + elif mode.startswith('RGB'): + gotmode = 'RGB' + mode = mode[3:] + else: + raise Error() + if mode.startswith('A'): + gotmode += 'A' + mode = mode[1:] + + # Skip any optional ';' + while mode.startswith(';'): + mode = mode[1:] + + # Parse optional bitdepth + if mode: + try: + bitdepth = int(mode) + except (TypeError, ValueError): + raise Error() + except Error: + raise Error("mode string should be 'RGB' or 'L;16' or similar.") + mode = gotmode + + # Get bitdepth from *mode* if possible. + if bitdepth: + if info.get('bitdepth') and bitdepth != info['bitdepth']: + raise Error("mode bitdepth (%d) should match info bitdepth (%d)." % + (bitdepth, info['bitdepth'])) + info['bitdepth'] = bitdepth + + # Fill in and/or check entries in *info*. + # Dimensions. + if 'size' in info: + # Check width, height, size all match where used. + for dimension,axis in [('width', 0), ('height', 1)]: + if dimension in info: + if info[dimension] != info['size'][axis]: + raise Error( + "info[%r] should match info['size'][%r]." % + (dimension, axis)) + info['width'],info['height'] = info['size'] + if 'height' not in info: + try: + l = len(a) + except TypeError: + raise Error( + "len(a) does not work, supply info['height'] instead.") + info['height'] = l + # Colour format. + if 'greyscale' in info: + if bool(info['greyscale']) != ('L' in mode): + raise Error("info['greyscale'] should match mode.") + info['greyscale'] = 'L' in mode + if 'alpha' in info: + if bool(info['alpha']) != ('A' in mode): + raise Error("info['alpha'] should match mode.") + info['alpha'] = 'A' in mode + + 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 + try: + row[0][0] + threed = True + testelement = row[0] + except (IndexError, TypeError): + threed = False + testelement = row + if 'width' not in info: + if threed: + width = len(row) + else: + width = len(row) // planes + info['width'] = width + + if threed: + # Flatten the threed rows + a = (itertools.chain.from_iterable(x) for x in a) + + 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'.split(): + 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 *file*. If *file* looks like an open file + descriptor then it is used, otherwise it is treated as a + filename and a fresh file is opened. + + In general, you can only call this method once; after it has + been called the first time and the PNG image has been saved, the + source data will have been streamed, and cannot be streamed + again. + """ + + w = Writer(**self.info) + + try: + file.write + def close(): pass + except AttributeError: + file = open(file, 'wb') + def close(): file.close() + + try: + w.write(file, self.rows) + finally: + close() + +class _readable: + """ + A simple file-like interface for strings and arrays. + """ + + def __init__(self, buf): + self.buf = buf + self.offset = 0 + + def read(self, n): + r = self.buf[self.offset:self.offset+n] + if isarray(r): + r = r.tostring() + self.offset += n + return r + +try: + str(b'dummy', 'ascii') +except TypeError: + as_str = str +else: + def as_str(x): + return str(x, 'ascii') + +class Reader: + """ + PNG decoder in pure Python. + """ + + def __init__(self, _guess=None, **kw): + """ + Create a PNG decoder object. + + The constructor expects exactly one keyword argument. If you + supply a positional argument instead, it will guess the input + type. You can choose among the following keyword arguments: + + filename + Name of input file (a PNG file). + file + A file-like object (object with a read() method). + bytes + ``array`` or ``string`` with PNG data. + + """ + if ((_guess is not None and len(kw) != 0) or + (_guess is None and len(kw) != 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): + kw["bytes"] = _guess + elif isinstance(_guess, str): + kw["filename"] = _guess + elif hasattr(_guess, 'read'): + kw["file"] = _guess + + if "filename" in kw: + self.file = open(kw["filename"], "rb") + elif "file" in kw: + self.file = kw["file"] + elif "bytes" in kw: + self.file = _readable(kw["bytes"]) + else: + raise TypeError("expecting filename, file or bytes array") + + + def chunk(self, seek=None, 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 `seek` argument is + specified then it will keep reading chunks until it either runs + out of file or finds the type specified by the argument. Note + that in general the order of chunks in PNGs is unspecified, so + using `seek` can cause you to miss chunks. + + If the optional `lenient` argument evaluates to `True`, + checksum failures will raise warnings rather than exceptions. + """ + + self.validate_signature() + + while True: + # http://www.w3.org/TR/PNG/#5Chunk-layout + if not self.atchunk: + self.atchunk = self.chunklentype() + 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) + if seek and type != seek: + continue + verify = zlib.crc32(type) + verify = zlib.crc32(data, verify) + # Whether the output from zlib.crc32 is signed or not varies + # according to hideous implementation details, see + # http://bugs.python.org/issue1202 . + # We coerce it to be positive here (in a way which works on + # Python 2.3 and older). + verify &= 2**32 - 1 + 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, 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, and the + result will be returned as a fresh sequence of bytes. + """ + + # :todo: Would it be better to update scanline in place? + # Yes, with the Cython extension making the undo_filter fast, + # updating scanline inplace makes the code 3 times faster + # (reading 50 images of 800x800 went from 40s to 16s) + 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 = array('B', [0]*len(scanline)) + + def sub(): + """Undo sub filter.""" + + ai = 0 + # Loop starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(fu, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xff + ai += 1 + + def up(): + """Undo up filter.""" + + for i in range(len(result)): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xff + + def average(): + """Undo average filter.""" + + ai = -fu + 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 paeth(): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -fu + 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 + + # Call appropriate filter algorithm. Note that 0 has already + # been dealt with. + (None, + pngfilters.undo_filter_sub, + pngfilters.undo_filter_up, + pngfilters.undo_filter_average, + pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result) + return result + + def deinterlace(self, raw): + """ + Read raw pixel data, undo filters, deinterlace, and flatten. + Return in flat row flat pixel format. + """ + + # Values per row (of the target image) + vpr = self.width * self.planes + + # Make a result array, and make it big enough. Interleaving + # writes to the output array randomly (well, not quite), so the + # entire output array must be in memory. + fmt = 'BH'[self.bitdepth > 8] + a = array(fmt, [0]*vpr*self.height) + source_offset = 0 + + for xstart, ystart, xstep, ystep in _adam7: + if xstart >= self.width: + continue + # The previous (reconstructed) scanline. None at the + # beginning of a pass to indicate that there is no previous + # line. + recon = None + # Pixels per row (reduced pass image) + ppr = int(math.ceil((self.width-xstart)/float(xstep))) + # Row size in bytes for this pass. + row_size = int(math.ceil(self.psize * ppr)) + for y in range(ystart, self.height, ystep): + 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.serialtoflat(recon, ppr) + if xstep == 1: + assert xstart == 0 + offset = y * vpr + a[offset:offset+vpr] = flat + else: + offset = y * vpr + xstart * 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 iterboxed(self, rows): + """Iterator that yields each scanline in boxed row flat pixel + format. `rows` should be an iterator that yields the bytes of + each row in turn. + """ + + def asvalues(raw): + """Convert a row of raw bytes into a flat row. Result will + be a freshly allocated object, not shared with + argument. + """ + + if self.bitdepth == 8: + return array('B', raw) + if self.bitdepth == 16: + raw = tostring(raw) + return array('H', struct.unpack('!%dH' % (len(raw)//2), raw)) + assert self.bitdepth < 8 + width = self.width + # Samples per byte + spb = 8//self.bitdepth + out = array('B') + mask = 2**self.bitdepth - 1 + shifts = [self.bitdepth * i + for i in reversed(list(range(spb)))] + for o in raw: + out.extend([mask&(o>>i) for i in shifts]) + return out[:width] + + return map(asvalues, rows) + + def serialtoflat(self, bytes, width=None): + """Convert serial format (byte stream) pixel data to flat row + flat pixel. + """ + + if self.bitdepth == 8: + return bytes + if self.bitdepth == 16: + bytes = tostring(bytes) + return array('H', + struct.unpack('!%dH' % (len(bytes)//2), bytes)) + assert self.bitdepth < 8 + if width is None: + width = self.width + # Samples per byte + spb = 8//self.bitdepth + out = array('B') + mask = 2**self.bitdepth - 1 + shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb))))) + l = width + for o in bytes: + out.extend([(mask&(o>>s)) for s in shifts][:l]) + l -= spb + if l <= 0: + l = width + return out + + def iterstraight(self, raw): + """Iterator that undoes the effect of filtering, and yields + each row in serialised format (as a sequence of bytes). + Assumes input is straightlaced. `raw` should be an iterable + that yields the raw bytes in chunks of arbitrary size. + """ + + # length of row, in bytes + rb = self.row_bytes + a = array('B') + # The previous (reconstructed) scanline. None indicates first + # line of image. + recon = None + for some in raw: + a.extend(some) + 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. + """ + + if self.signature: + return + self.signature = self.file.read(8) + 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.chunklentype() + 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 chunklentype(self): + """Reads just enough of the input to determine the next + chunk's length and type, returned as a (*length*, *type*) pair + where *type* is a string. 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)) + return length,type + + def process_chunk(self, lenient=False): + """Process the next chunk and its data. This only processes the + following chunk types, all others are ignored: ``IHDR``, + ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``. + + If the optional `lenient` argument evaluates to `True`, + checksum failures will raise warnings rather than exceptions. + """ + + type, data = self.chunk(lenient=lenient) + method = '_process_' + as_str(type) + 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 Error("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`, + `pixels`, `metadata`). + + May use excessive memory. + + `pixels` are returned in boxed row flat pixel format. + + 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: + try: + type, data = self.chunk(lenient=lenient) + except ValueError as e: + raise ChunkError(e.args[0]) + 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 + + def iterdecomp(idat): + """Iterator that yields decompressed strings. `idat` should + be an iterator that yields the ``IDAT`` chunk data. + """ + + # 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 idat: + # :todo: add a max_length argument here to limit output + # size. + yield array('B', d.decompress(data)) + yield array('B', d.flush()) + + self.preamble(lenient=lenient) + raw = iterdecomp(iteridat()) + + if self.interlace: + raw = array('B', itertools.chain(*raw)) + arraycode = 'BH'[self.bitdepth>8] + # Like :meth:`group` but producing an array.array object for + # each row. + pixels = map(lambda *row: array(arraycode, row), + *[iter(self.deinterlace(raw))]*self.width*self.planes) + else: + pixels = self.iterboxed(self.iterstraight(raw)) + meta = dict() + for attr in 'greyscale alpha planes bitdepth interlace'.split(): + meta[attr] = getattr(self, attr) + meta['size'] = (self.width, self.height) + for attr in 'gamma transparent background'.split(): + a = getattr(self, attr, None) + if a is not None: + meta[attr] = a + if self.plte: + meta['palette'] = self.palette() + return self.width, self.height, pixels, meta + + + def read_flat(self): + """ + Read a PNG file and decode it into flat row flat pixel format. + Returns (*width*, *height*, *pixels*, *metadata*). + + May use excessive memory. + + `pixels` are returned in flat row flat pixel format. + + See also the :meth:`read` method which returns pixels in the + more stream-friendly boxed row flat pixel format. + """ + + x, y, pixel, meta = self.read() + arraycode = 'BH'[meta['bitdepth']>8] + pixel = array(arraycode, itertools.chain(*pixel)) + return x, y, pixel, meta + + 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 method is intended to remove 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*, *pixels*, *meta*) + + 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 *meta* 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 have ``planes=3`` and ``alpha=False`` for the + source image, but the *meta* dictionary returned by this method + will have ``planes=4`` and ``alpha=True`` because an alpha + channel is synthesized and added. + + *pixels* is the pixel data in boxed row flat pixel format (just + 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,meta = self.read() + + if self.colormap: + meta['colormap'] = False + meta['alpha'] = bool(self.trns) + meta['bitdepth'] = 8 + meta['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**meta['bitdepth']-1 + planes = meta['planes'] + meta['alpha'] = True + meta['planes'] += 1 + typecode = 'BH'[meta['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 > meta['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 == meta['bitdepth']: + targetbitdepth = None + if targetbitdepth: + shift = meta['bitdepth'] - targetbitdepth + meta['bitdepth'] = targetbitdepth + def itershift(pixels): + for row in pixels: + yield [p >> shift for p in row] + pixels = itershift(pixels) + return x,y,pixels,meta + + def asFloat(self, maxval=1.0): + """Return image pixels as per :meth:`asDirect` method, but scale + all pixel values to be floating point values between 0.0 and + *maxval*. + """ + + x,y,pixels,info = self.asDirect() + sourcemaxval = 2**info['bitdepth']-1 + del info['bitdepth'] + info['maxval'] = float(maxval) + factor = float(maxval)/float(sourcemaxval) + def iterfloat(): + for row in pixels: + yield [factor * p for p in row] + return x,y,iterfloat(),info + + def _as_rescale(self, get, targetbitdepth): + """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" + + width,height,pixels,meta = get() + maxval = 2**meta['bitdepth'] - 1 + targetmaxval = 2**targetbitdepth - 1 + factor = float(targetmaxval) / float(maxval) + meta['bitdepth'] = targetbitdepth + def iterscale(): + for row in pixels: + yield [int(round(x*factor)) for x in row] + if maxval == targetmaxval: + return width, height, pixels, meta + else: + return width, height, iterscale(), meta + + 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*, *pixels*, *metadata*). + *width*, *height*, *metadata* are as per the + :meth:`read` method. + + *pixels* is the pixel data in boxed row flat pixel format. + """ + + 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 *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``. + """ + + width,height,pixels,meta = self.asDirect() + if meta['alpha']: + raise Error("will not convert image with alpha channel to RGB") + if not meta['greyscale']: + return width,height,pixels,meta + meta['greyscale'] = False + typecode = 'BH'[meta['bitdepth'] > 8] + def iterrgb(): + for row in pixels: + a = array(typecode, [0]) * 3 * width + for i in range(3): + a[i::3] = row + yield a + return width,height,iterrgb(),meta + + 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 *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``, and + ``metadata['alpha']`` will be ``True``. + """ + + width,height,pixels,meta = self.asDirect() + if meta['alpha'] and not meta['greyscale']: + return width,height,pixels,meta + typecode = 'BH'[meta['bitdepth'] > 8] + maxval = 2**meta['bitdepth'] - 1 + maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width + def newarray(): + return array(typecode, maxbuffer) + + if meta['alpha'] and meta['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() + pngfilters.convert_la_to_rgba(row, a) + yield a + elif meta['greyscale']: + # L to RGBA + def convert(): + for row in pixels: + a = newarray() + pngfilters.convert_l_to_rgba(row, a) + yield a + else: + assert not meta['alpha'] and not meta['greyscale'] + # RGB to RGBA + def convert(): + for row in pixels: + a = newarray() + pngfilters.convert_rgb_to_rgba(row, a) + yield a + meta['alpha'] = True + meta['greyscale'] = False + return width,height,convert(),meta + +def check_bitdepth_colortype(bitdepth, colortype): + """Check that `bitdepth` and `colortype` are both valid, + and specified in a valid combination. Returns 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 isinteger(x): + try: + return int(x) == x + except (TypeError, ValueError): + return False + + +# === Support for users without Cython === + +try: + pngfilters +except NameError: + class pngfilters(object): + 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 + undo_filter_sub = staticmethod(undo_filter_sub) + + 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 + undo_filter_up = staticmethod(undo_filter_up) + + 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 + undo_filter_average = staticmethod(undo_filter_average) + + 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 + undo_filter_paeth = staticmethod(undo_filter_paeth) + + def convert_la_to_rgba(row, result): + for i in range(3): + result[i::4] = row[0::2] + result[3::4] = row[1::2] + convert_la_to_rgba = staticmethod(convert_la_to_rgba) + + 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 + convert_l_to_rgba = staticmethod(convert_l_to_rgba) + + 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] + convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba) + + +# === Command Line Support === + +def read_pam_header(infile): + """ + Read (the rest of a) PAM header. `infile` should be positioned + immediately after the initial 'P7' line (at the beginning of the + second line). Returns are as for `read_pnm_header`. + """ + + # Unlike PBM, PGM, and PPM, we can read the header a line at a time. + header = dict() + while True: + l = infile.readline().strip() + if l == b'ENDHDR': + break + if not l: + raise EOFError('PAM ended prematurely') + if l[0] == b'#': + continue + l = l.split(None, 1) + if l[0] not in header: + header[l[0]] = l[1] + else: + header[l[0]] += b' ' + l[1] + + required = [b'WIDTH', b'HEIGHT', b'DEPTH', b'MAXVAL'] + WIDTH,HEIGHT,DEPTH,MAXVAL = required + present = [x for x in required if x in header] + if len(present) != len(required): + raise Error('PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL') + width = int(header[WIDTH]) + height = int(header[HEIGHT]) + depth = int(header[DEPTH]) + maxval = int(header[MAXVAL]) + if (width <= 0 or + height <= 0 or + depth <= 0 or + maxval <= 0): + raise Error( + 'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers') + return 'P7', width, height, depth, maxval + +def read_pnm_header(infile, supported=(b'P5', b'P6')): + """ + Read a PNM header, returning (format,width,height,depth,maxval). + `width` and `height` are in pixels. `depth` is the number of + channels in the image; for PBM and PGM it is synthesized as 1, for + PPM as 3; for PAM images it is read from the header. `maxval` is + synthesized (as 1) for PBM images. + """ + + # Generally, see http://netpbm.sourceforge.net/doc/ppm.html + # and http://netpbm.sourceforge.net/doc/pam.html + + # Technically 'P7' must be followed by a newline, so by using + # rstrip() we are being liberal in what we accept. I think this + # is acceptable. + type = infile.read(3).rstrip() + if type not in supported: + raise NotImplementedError('file format %s not supported' % type) + if type == b'P7': + # PAM header parsing is completely different. + return read_pam_header(infile) + # Expected number of tokens in header (3 for P4, 4 for P6) + expected = 4 + pbm = (b'P1', b'P4') + if type in pbm: + expected = 3 + header = [type] + + # We have to read the rest of the header byte by byte because the + # final whitespace character (immediately following the MAXVAL in + # the case of P6) may not be a newline. Of course all PNM files in + # the wild use a newline at this point, so it's tempting to use + # readline; but it would be wrong. + def getc(): + c = infile.read(1) + if not c: + raise Error('premature EOF reading PNM header') + return c + + c = getc() + while True: + # Skip whitespace that precedes a token. + while c.isspace(): + c = getc() + # Skip comments. + while c == '#': + while c not in b'\n\r': + c = getc() + if not c.isdigit(): + raise Error('unexpected character %s found in header' % c) + # According to the specification it is legal to have comments + # that appear in the middle of a token. + # This is bonkers; I've never seen it; and it's a bit awkward to + # code good lexers in Python (no goto). So we break on such + # cases. + token = b'' + while c.isdigit(): + token += c + c = getc() + # Slight hack. All "tokens" are decimal integers, so convert + # them here. + header.append(int(token)) + if len(header) == expected: + break + # Skip comments (again) + while c == '#': + while c not in '\n\r': + c = getc() + if not c.isspace(): + raise Error('expected header to end with whitespace, not %s' % c) + + if type in pbm: + # synthesize a MAXVAL + header.append(1) + depth = (1,3)[type == b'P6'] + return header[0], header[1], header[2], depth, header[3] + +def write_pnm(file, width, height, pixels, meta): + """Write a Netpbm PNM/PAM file. + """ + + bitdepth = meta['bitdepth'] + maxval = 2**bitdepth - 1 + # Rudely, the number of image planes can be used to determine + # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). + planes = meta['planes'] + # Can be an assert as long as we assume that pixels and meta came + # from a PNG file. + assert planes in (1,2,3,4) + if planes in (1,3): + if 1 == planes: + # PGM + # Could generate PBM if maxval is 1, but we don't (for one + # thing, we'd have to convert the data, not just blat it + # out). + fmt = 'P5' + else: + # PPM + fmt = 'P6' + header = '%s %d %d %d\n' % (fmt, width, height, maxval) + if planes in (2,4): + # PAM + # See http://netpbm.sourceforge.net/doc/pam.html + if 2 == planes: + tupltype = 'GRAYSCALE_ALPHA' + else: + tupltype = 'RGB_ALPHA' + header = ('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n' + 'TUPLTYPE %s\nENDHDR\n' % + (width, height, planes, maxval, tupltype)) + file.write(header.encode('ascii')) + # Values per row + vpr = planes * width + # struct format + fmt = '>%d' % vpr + if maxval > 0xff: + fmt = fmt + 'H' + else: + fmt = fmt + 'B' + for row in pixels: + file.write(struct.pack(fmt, *row)) + file.flush() + +def color_triple(color): + """ + Convert a command line colour value to a RGB triple of integers. + FIXME: Somewhere we need support for greyscale backgrounds etc. + """ + if color.startswith('#') and len(color) == 4: + return (int(color[1], 16), + int(color[2], 16), + int(color[3], 16)) + if color.startswith('#') and len(color) == 7: + return (int(color[1:3], 16), + int(color[3:5], 16), + int(color[5:7], 16)) + elif color.startswith('#') and len(color) == 13: + return (int(color[1:5], 16), + int(color[5:9], 16), + int(color[9:13], 16)) + +def _add_common_options(parser): + """Call *parser.add_option* for each of the options that are + common between this PNG--PNM conversion tool and the gen + tool. + """ + parser.add_option("-i", "--interlace", + default=False, action="store_true", + help="create an interlaced PNG file (Adam7)") + parser.add_option("-t", "--transparent", + action="store", type="string", metavar="#RRGGBB", + help="mark the specified colour as transparent") + parser.add_option("-b", "--background", + action="store", type="string", metavar="#RRGGBB", + help="save the specified background colour") + parser.add_option("-g", "--gamma", + action="store", type="float", metavar="value", + help="save the specified gamma value") + parser.add_option("-c", "--compression", + action="store", type="int", metavar="level", + help="zlib compression level (0-9)") + return parser + +def _main(argv): + """ + Run the PNG encoder with options from the command line. + """ + + # Parse command line arguments + from optparse import OptionParser + version = '%prog ' + __version__ + parser = OptionParser(version=version) + parser.set_usage("%prog [options] [imagefile]") + parser.add_option('-r', '--read-png', default=False, + action='store_true', + help='Read PNG, write PNM') + parser.add_option("-a", "--alpha", + action="store", type="string", metavar="pgmfile", + help="alpha channel transparency (RGBA)") + _add_common_options(parser) + + (options, args) = parser.parse_args(args=argv[1:]) + + # Convert options + if options.transparent is not None: + options.transparent = color_triple(options.transparent) + if options.background is not None: + options.background = color_triple(options.background) + + # Prepare input and output files + if len(args) == 0: + infilename = '-' + infile = sys.stdin + elif len(args) == 1: + infilename = args[0] + infile = open(infilename, 'rb') + else: + parser.error("more than one input file") + outfile = sys.stdout + if sys.platform == "win32": + import msvcrt, os + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + + if options.read_png: + # Encode PNG to PPM + png = Reader(file=infile) + width,height,pixels,meta = png.asDirect() + write_pnm(outfile, width, height, pixels, meta) + else: + # Encode PNM to PNG + format, width, height, depth, maxval = \ + read_pnm_header(infile, (b'P5',b'P6',b'P7')) + # When it comes to the variety of input formats, we do something + # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour + # types supported by PNG and that they correspond to 1, 2, 3, 4 + # channels respectively. So we use the number of channels in + # the source image to determine which one we have. We do not + # care about TUPLTYPE. + greyscale = depth <= 2 + pamalpha = depth in (2,4) + supported = [2**x-1 for x in range(1,17)] + try: + mi = supported.index(maxval) + except ValueError: + raise NotImplementedError( + 'your maxval (%s) not in supported list %s' % + (maxval, str(supported))) + bitdepth = mi+1 + writer = Writer(width, height, + greyscale=greyscale, + bitdepth=bitdepth, + interlace=options.interlace, + transparent=options.transparent, + background=options.background, + alpha=bool(pamalpha or options.alpha), + gamma=options.gamma, + compression=options.compression) + if options.alpha: + pgmfile = open(options.alpha, 'rb') + format, awidth, aheight, adepth, amaxval = \ + read_pnm_header(pgmfile, 'P5') + if amaxval != '255': + raise NotImplementedError( + 'maxval %s not supported for alpha channel' % amaxval) + if (awidth, aheight) != (width, height): + raise ValueError("alpha channel image size mismatch" + " (%s has %sx%s but %s has %sx%s)" + % (infilename, width, height, + options.alpha, awidth, aheight)) + writer.convert_ppm_and_pgm(infile, pgmfile, outfile) + else: + writer.convert_pnm(infile, outfile) + + +if __name__ == '__main__': + try: + _main(sys.argv) + except Error as e: + print(e, file=sys.stderr) |