aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordannye <33dannye@gmail.com>2024-10-09 21:56:57 -0500
committerdannye <33dannye@gmail.com>2024-10-09 21:56:57 -0500
commit942c5f13eadb828dbf9dd6f75dfece007b53f353 (patch)
tree0013406b83f352bed3f9b8247e7a73d11a150828
parentRename off-by-one wram labels (diff)
parentRenamed pokemon-asm-tools to gb-asm-tools (diff)
downloadpokeyellow-942c5f13eadb828dbf9dd6f75dfece007b53f353.tar.gz
pokeyellow-942c5f13eadb828dbf9dd6f75dfece007b53f353.tar.xz
pokeyellow-942c5f13eadb828dbf9dd6f75dfece007b53f353.zip
Merge branch 'master' of https://github.com/pret/pokered
-rw-r--r--README.md2
-rw-r--r--audio/wave_samples.asm6
-rw-r--r--engine/movie/oak_speech/init_player_data.asm2
-rw-r--r--engine/movie/trade.asm64
-rw-r--r--engine/overworld/cut.asm11
-rw-r--r--engine/overworld/emotion_bubbles.asm11
-rw-r--r--engine/overworld/npc_movement_2.asm6
-rw-r--r--home/map_objects.asm4
-rw-r--r--home/trainers.asm2
-rw-r--r--ram/wram.asm11
-rw-r--r--scripts/VermilionDock.asm2
-rw-r--r--tools/Makefile5
-rwxr-xr-xtools/consts.py56
-rwxr-xr-xtools/free_space.awk78
-rw-r--r--tools/gfx.py267
-rwxr-xr-xtools/palfix.py63
-rwxr-xr-xtools/pic.py491
-rw-r--r--tools/pkmncompress.c313
-rw-r--r--tools/png.py2357
-rw-r--r--tools/pokemontools/__init__.py1
-rw-r--r--tools/pokemontools/gfx.py938
-rw-r--r--tools/pokemontools/lz.py580
-rw-r--r--tools/pokemontools/png.py2650
-rwxr-xr-xtools/rgb555.py38
-rwxr-xr-xtools/sym_comments.py52
-rwxr-xr-xtools/toc.py99
-rwxr-xr-xtools/unique.py106
27 files changed, 297 insertions, 7918 deletions
diff --git a/README.md b/README.md
index 1cab08ce..dcc01df6 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ To set up the repository, see [**INSTALL.md**](INSTALL.md).
- [**Wiki**][wiki] (includes [tutorials][tutorials])
- [**Symbols**][symbols]
+- [**Tools**][tools]
You can find us on [Discord (pret, #pokered)](https://discord.gg/d5dubZ3).
@@ -23,5 +24,6 @@ For other pret projects, see [pret.github.io](https://pret.github.io/).
[wiki]: https://github.com/pret/pokeyellow/wiki
[tutorials]: https://github.com/pret/pokeyellow/wiki/Tutorials
[symbols]: https://github.com/pret/pokeyellow/tree/symbols
+[tools]: https://github.com/pret/gb-asm-tools
[ci]: https://github.com/pret/pokeyellow/actions
[ci-badge]: https://github.com/pret/pokeyellow/actions/workflows/main.yml/badge.svg
diff --git a/audio/wave_samples.asm b/audio/wave_samples.asm
index 842fafd9..cd789611 100644
--- a/audio/wave_samples.asm
+++ b/audio/wave_samples.asm
@@ -30,3 +30,9 @@
; duty 5 reads from sfx data
.wave5
+; in audio 1: (used by audio/music/lavender.asm)
+; dn 2, 1, 14, 2, 3, 3, 2, 8, 14, 1, 2, 2, 15, 15, 14, 10, 1, 0, 1, 4, 13, 12, 1, 0, 14, 3, 4, 1, 5, 1, 7, 3
+; in audio 2:
+; dn 14, 12, 0, 2, 2, 0, 9, 1, 0, 7, 12, 0, 2, 0, 8, 1, 0, 7, 13, 0, 2, 0, 9, 1, 0, 7, 12, 0, 2, 12, 10, 1
+; in audio 3: (used by audio/music/pokemontower.asm)
+; dn 2, 1, 14, 2, 3, 3, 2, 8, 14, 1, 2, 2, 15, 15, 2, 2, 15, 7, 2, 4, 2, 2, 15, 7, 3, 4, 2, 4, 15, 7, 4, 4
diff --git a/engine/movie/oak_speech/init_player_data.asm b/engine/movie/oak_speech/init_player_data.asm
index 25b6067d..d393b333 100644
--- a/engine/movie/oak_speech/init_player_data.asm
+++ b/engine/movie/oak_speech/init_player_data.asm
@@ -39,7 +39,7 @@ DEF START_MONEY EQU $3000
ld hl, wObtainedBadges
ld [hli], a
-
+ assert wObtainedBadges + 1 == wUnusedObtainedBadges
ld [hl], a
ld hl, wPlayerCoins
diff --git a/engine/movie/trade.asm b/engine/movie/trade.asm
index 990f1788..be826996 100644
--- a/engine/movie/trade.asm
+++ b/engine/movie/trade.asm
@@ -312,7 +312,7 @@ Trade_AnimateBallEnteringLinkCable:
.moveBallInsideLinkCableLoop
push bc
xor a
- ld de, Trade_BallInsideLinkCableOAM
+ ld de, Trade_BallInsideLinkCableOAMBlock
call WriteOAMBlock
ld a, [wLinkCableAnimBulgeToggle]
xor $1
@@ -348,9 +348,11 @@ Trade_AnimateBallEnteringLinkCable:
ldh [hAutoBGTransferEnabled], a
ret
-Trade_BallInsideLinkCableOAM:
- dbsprite 0, 15, 0, 6, $7e, OAM_HFLIP
- dbsprite 8, 15, 0, 6, $7e, OAM_HFLIP | OAM_VFLIP
+Trade_BallInsideLinkCableOAMBlock:
+ db $7e, 0
+ db $7e, OAM_HFLIP
+ db $7e, OAM_VFLIP
+ db $7e, OAM_HFLIP | OAM_VFLIP
Trade_ShowEnemyMon:
ld a, TRADE_BALL_TILT_ANIM
@@ -620,7 +622,7 @@ Trade_AnimCircledMon:
Trade_WriteCircledMonOAM:
farcall WriteMonPartySpriteOAMBySpecies
- call Trade_WriteCircleOAM
+ call Trade_WriteCircleOAMBlock
Trade_AddOffsetsToOAMCoords:
ld hl, wShadowOAM
@@ -674,11 +676,11 @@ Trade_AnimMonMoveVertical:
jr nz, .loop
ret
-Trade_WriteCircleOAM:
+Trade_WriteCircleOAMBlock:
; Writes the OAM blocks for the circle around the traded mon as it passes
; the link cable.
- ld hl, Trade_CircleOAMPointers
- ld c, $4
+ ld hl, Trade_CircleOAMBlocks
+ ld c, 4
xor a
.loop
push bc
@@ -701,33 +703,41 @@ Trade_WriteCircleOAM:
jr nz, .loop
ret
-MACRO trade_circle_oam
+MACRO trade_circle_oam_block
+ ; oam block pointer, upper-left x coord, upper-left y coord
dw \1
db \2, \3
ENDM
-Trade_CircleOAMPointers:
- ; oam pointer, upper-left x coord, upper-left y coord
- trade_circle_oam Trade_CircleOAM0, $08, $08
- trade_circle_oam Trade_CircleOAM1, $18, $08
- trade_circle_oam Trade_CircleOAM2, $08, $18
- trade_circle_oam Trade_CircleOAM3, $18, $18
+Trade_CircleOAMBlocks:
+ trade_circle_oam_block .OAMBlock0, 8, 8
+ trade_circle_oam_block .OAMBlock1, 24, 8
+ trade_circle_oam_block .OAMBlock2, 8, 24
+ trade_circle_oam_block .OAMBlock3, 24, 24
-Trade_CircleOAM0:
- dbsprite 2, 7, 0, 0, ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1
- dbsprite 2, 7, 0, 2, ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1
+.OAMBlock0:
+ db ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1
+ db ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1
+ db ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1
+ db ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1
-Trade_CircleOAM1:
- dbsprite 6, 7, 0, 1, ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1 | OAM_HFLIP
- dbsprite 6, 7, 0, 3, ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1 | OAM_HFLIP
+.OAMBlock1:
+ db ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1 | OAM_HFLIP
+ db ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1 | OAM_HFLIP
+ db ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1 | OAM_HFLIP
+ db ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1 | OAM_HFLIP
-Trade_CircleOAM2:
- dbsprite 10, 7, 0, 2, ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1 | OAM_VFLIP
- dbsprite 10, 7, 0, 0, ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1 | OAM_VFLIP
+.OAMBlock2:
+ db ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1 | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1 | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1 | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1 | OAM_VFLIP
-Trade_CircleOAM3:
- dbsprite 14, 7, 0, 3, ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
- dbsprite 14, 7, 0, 1, ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
+.OAMBlock3:
+ db ICON_TRADEBUBBLE << 2 + 3, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 2, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 1, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
+ db ICON_TRADEBUBBLE << 2 + 0, OAM_OBP1 | OAM_HFLIP | OAM_VFLIP
; a = species
Trade_LoadMonSprite:
diff --git a/engine/overworld/cut.asm b/engine/overworld/cut.asm
index 8cc76104..d26832ec 100644
--- a/engine/overworld/cut.asm
+++ b/engine/overworld/cut.asm
@@ -120,12 +120,15 @@ LoadCutGrassAnimationTilePattern:
WriteCutOrBoulderDustAnimationOAMBlock:
call GetCutOrBoulderDustAnimationOffsets
ld a, $9
- ld de, CutOrBoulderDustAnimationTilesAndAttributes
+ ld de, .OAMBlock
jp WriteOAMBlock
-CutOrBoulderDustAnimationTilesAndAttributes:
- dbsprite 2, -1, 4, 4, $fd, OAM_OBP1 | OAM_HIGH_PALS
- dbsprite 2, -1, 4, 6, $ff, OAM_OBP1 | OAM_HIGH_PALS
+.OAMBlock:
+; tile ID, attributes
+ db $fc, OAM_OBP1 | OAM_HIGH_PALS
+ db $fd, OAM_OBP1 | OAM_HIGH_PALS
+ db $fe, OAM_OBP1 | OAM_HIGH_PALS
+ db $ff, OAM_OBP1 | OAM_HIGH_PALS
GetCutOrBoulderDustAnimationOffsets:
ld hl, wSpritePlayerStateData1YPixels
diff --git a/engine/overworld/emotion_bubbles.asm b/engine/overworld/emotion_bubbles.asm
index 61bbc4f5..da9502b0 100644
--- a/engine/overworld/emotion_bubbles.asm
+++ b/engine/overworld/emotion_bubbles.asm
@@ -54,7 +54,7 @@ EmotionBubble:
add $8
ld c, a
- ld de, EmotionBubblesOAM
+ ld de, EmotionBubblesOAMBlock
xor a
call WriteOAMBlock
ld c, 60
@@ -66,9 +66,12 @@ EmotionBubble:
ret
-EmotionBubblesOAM:
- dbsprite 0, -1, 0, 0, $f9, 0
- dbsprite 0, -1, 0, 2, $fb, 0
+EmotionBubblesOAMBlock:
+; tile ID, attributes
+ db $f8, 0
+ db $f9, 0
+ db $fa, 0
+ db $fb, 0
EmotionBubbles:
ShockEmote: INCBIN "gfx/emotes/shock.2bpp"
diff --git a/engine/overworld/npc_movement_2.asm b/engine/overworld/npc_movement_2.asm
index 2ced9da6..bca9bf52 100644
--- a/engine/overworld/npc_movement_2.asm
+++ b/engine/overworld/npc_movement_2.asm
@@ -1,7 +1,7 @@
-FreezeEnemyTrainerSprite::
+SetEnemyTrainerToStayAndFaceAnyDirection::
ld a, [wCurMap]
cp POKEMON_TOWER_7F
- ret z ; the Rockets on Pokemon Tower 7F leave after battling, so don't freeze them
+ ret z ; the Rockets on Pokemon Tower 7F leave after battling, so don't set them
ld hl, RivalIDs
ld a, [wEngagedTrainerClass]
ld b, a
@@ -10,7 +10,7 @@ FreezeEnemyTrainerSprite::
cp -1
jr z, .notRival
cp b
- ret z ; the rival leaves after battling, so don't freeze him
+ ret z ; the rival leaves after battling, so don't set him
jr .loop
.notRival
ld a, [wSpriteIndex]
diff --git a/home/map_objects.asm b/home/map_objects.asm
index c959f2db..59140505 100644
--- a/home/map_objects.asm
+++ b/home/map_objects.asm
@@ -293,9 +293,9 @@ SetSpriteMovementBytesToFE::
SetSpriteMovementBytesToFF::
push hl
call GetSpriteMovementByte1Pointer
- ld [hl], $FF
+ ld [hl], STAY
call GetSpriteMovementByte2Pointer
- ld [hl], $FF ; prevent person from walking?
+ ld [hl], NONE
pop hl
ret
diff --git a/home/trainers.asm b/home/trainers.asm
index 09a69ae1..5048a8c5 100644
--- a/home/trainers.asm
+++ b/home/trainers.asm
@@ -358,7 +358,7 @@ PrintEndBattleText::
pop af
ldh [hLoadedROMBank], a
ld [MBC1RomBank], a
- farcall FreezeEnemyTrainerSprite
+ farcall SetEnemyTrainerToStayAndFaceAnyDirection
jp WaitForSoundToFinish
GetSavedEndBattleTextPointer::
diff --git a/ram/wram.asm b/ram/wram.asm
index c72329fb..8d983753 100644
--- a/ram/wram.asm
+++ b/ram/wram.asm
@@ -639,10 +639,13 @@ wPlayerMonMinimized:: db
ds 13
-; number of hits by enemy in attacks like Double Slap, etc.
-wEnemyNumHits:: ; db
+UNION
; the amount of damage accumulated by the enemy while biding
wEnemyBideAccumulatedDamage:: dw
+NEXTU
+; number of hits by enemy in attacks like Double Slap, etc.
+wEnemyNumHits:: db
+ENDU
ds 8
wMiscBattleDataEnd::
@@ -1919,7 +1922,7 @@ wOptions:: db
wObtainedBadges:: flag_array NUM_BADGES
- ds 1
+wUnusedObtainedBadges:: db
wLetterPrintingDelayFlags:: db
@@ -2395,7 +2398,7 @@ wSerialEnemyDataBlock:: ; ds $1a8
ds 9
-wEnemyPartyCount:: ds 1
+wEnemyPartyCount:: db
wEnemyPartySpecies:: ds PARTY_LENGTH + 1
wEnemyMons::
diff --git a/scripts/VermilionDock.asm b/scripts/VermilionDock.asm
index ce4e5f1e..22312510 100644
--- a/scripts/VermilionDock.asm
+++ b/scripts/VermilionDock.asm
@@ -155,7 +155,7 @@ VermilionDock_EmitSmokePuff:
ret
VermilionDockOAMBlock:
- ; tile id, attribute
+; tile ID, attributes
db $fc, $10
db $fd, $10
db $fe, $10
diff --git a/tools/Makefile b/tools/Makefile
index ed748baf..8bc6ff71 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -16,8 +16,5 @@ all: $(tools)
clean:
$(RM) $(tools)
-gfx: common.h
-scan_includes: common.h
-
-%: %.c
+%: %.c common.h
$(CC) $(CFLAGS) -o $@ $<
diff --git a/tools/consts.py b/tools/consts.py
deleted file mode 100755
index 37c85951..00000000
--- a/tools/consts.py
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python consts.py constants/some_constants.asm
-
-View numeric values of `const`ants.
-"""
-
-import sys
-import re
-
-const_value = 0
-const_inc = 1
-
-def asm_int(s):
- base = {'$': 16, '&': 8, '%': 2}.get(s[0], 10)
- return int(s if base == 10 else s[1:], base)
-
-def print_const(s, v):
- print(f'{s} == {v} == ${v:x}')
-
-def parse_for_constants(line):
- global const_value, const_inc
- if not (m := re.match(r'^\s+([A-Za-z_][A-Za-z0-9_@#]*)(?:\s+([^;\\n]+))?', line)):
- return
- macro, rest = m.groups()
- args = [arg.strip() for arg in rest.split(',')] if rest else []
- if args and not args[-1]:
- args = args[:-1]
- if macro == 'const_def':
- const_value = asm_int(args[0]) if len(args) >= 1 else 0
- const_inc = asm_int(args[1]) if len(args) >= 2 else 1
- elif macro == 'const':
- print_const(args[0], const_value)
- const_value += const_inc
- elif macro == 'shift_const':
- print_const(args[0], 1 << const_value)
- print_const(args[0] + '_F', const_value)
- const_value += const_inc
- elif macro == 'const_skip':
- const_value += const_inc * (asm_int(args[0]) if len(args) >= 1 else 1)
- elif macro == 'const_next':
- const_value = asm_int(args[0])
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} constants/some_constants.asm', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- with open(filename, 'r', encoding='utf-8') as file:
- for line in file:
- parse_for_constants(line)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/free_space.awk b/tools/free_space.awk
deleted file mode 100755
index b0e46dc7..00000000
--- a/tools/free_space.awk
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/gawk -f
-
-# Usage: tools/free_space.awk [BANK=<bank_spec>] pokeyellow.map
-
-# The BANK argument allows printing free space in one, all, or none of the ROM's banks.
-# Valid arguments are numbers (in decimal "42" or hexadecimal "0x2a"), "all" or "none".
-# If not specified, defaults to "none".
-# The `BANK` argument MUST be before the map file name, otherwise it has no effect!
-# Yes: tools/free_space.awk BANK=all pokeyellow.map
-# No: tools/free_space.awk pokeyellow.map BANK=42
-
-# Copyright (c) 2020, Eldred Habert.
-# SPDX-License-Identifier: MIT
-
-BEGIN {
- nb_banks = 0
- free = 0
- rom_bank = 0 # Safety net for malformed files
-
- # Default settings
- # Variables assigned via the command-line (except through `-v`) are *after* `BEGIN`
- BANK="none"
-}
-
-# Only accept ROM banks, ignore everything else
-toupper($0) ~ /^[ \t]*ROM[0X][ \t]+BANK[ \t]+#/ {
- nb_banks++
- rom_bank = 1
- split($0, fields, /[ \t]/)
- bank_num = strtonum(substr(fields[3], 2))
-}
-
-function register_bank(amount) {
- free += amount
- rom_bank = 0 # Reject upcoming banks by default
-
- if (BANK ~ /all/ || BANK == bank_num) {
- printf "Bank %3d: %5d/16384 (%.2f%%)\n", bank_num, amount, amount * 100 / 16384
- }
-}
-function register_bank_str(str) {
- if (str ~ /\$[0-9A-F]+/) {
- register_bank(strtonum("0x" substr(str, 2)))
- } else {
- printf "Malformed number? \"%s\" does not start with '$'\n", str
- }
-}
-
-rom_bank && toupper($0) ~ /^[ \t]*EMPTY$/ {
- # Empty bank
- register_bank(16384)
-}
-rom_bank && toupper($0) ~ /^[ \t]*SLACK:[ \t]/ {
- # Old (rgbds <=0.6.0) end-of-bank free space
- register_bank_str($2)
-}
-rom_bank && toupper($0) ~ /^[ \t]*TOTAL EMPTY:[ \t]/ {
- # New (rgbds >=0.6.1) total free space
- register_bank_str($3)
-}
-
-END {
- # Determine number of banks, by rounding to upper power of 2
- total_banks = 2 # Smallest size is 2 banks
- while (total_banks < nb_banks) {
- total_banks *= 2
- }
-
- # RGBLINK omits "trailing" ROM banks, so fake them
- bank_num = nb_banks
- while (bank_num < total_banks) {
- register_bank(16384)
- bank_num++
- }
-
- total = total_banks * 16384
- printf "Free space: %5d/%5d (%.2f%%)\n", free, total, free * 100 / total
-}
diff --git a/tools/gfx.py b/tools/gfx.py
deleted file mode 100644
index a3d95562..00000000
--- a/tools/gfx.py
+++ /dev/null
@@ -1,267 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-
-"""Supplementary scripts for graphics conversion."""
-
-import os
-import argparse
-
-from pokemontools import gfx, lz
-
-
-# Graphics with inverted tilemaps that aren't covered by filepath_rules.
-pics = [
- 'gfx/shrink1',
- 'gfx/shrink2',
-]
-
-def recursive_read(filename):
- def recurse(filename_):
- lines = []
- for line in open(filename_):
- if 'include "' in line.lower():
- lines += recurse(line.split('"')[1])
- else:
- lines += [line]
- return lines
- lines = recurse(filename)
- return ''.join(lines)
-
-base_stats = None
-def get_base_stats():
- global base_stats
- if not base_stats:
- base_stats = recursive_read('data/base_stats.asm')
- return base_stats
-
-def get_pokemon_dimensions(path):
- try:
- byte = bytearray(open(path, 'rb').read())[0]
- width = byte & 0xf
- height = (byte >> 8) & 0xf
- return width, height
- except:
- return None
-
-
-def get_animation_frames(path=None, w=7, h=7, bitmask_path=None, frame_path=None):
- """Retrieve animation frame tilemaps from generated frame/bitmask data."""
- if not path:
- path = bitmask_path
- if not path:
- path = frame_path
- if not path:
- raise Exception("need at least one of path, bitmask_path or frame_path")
-
- if not bitmask_path:
- bitmask_path = os.path.join(os.path.split(path)[0], 'bitmask.asm')
- if not frame_path:
- frame_path = os.path.join(os.path.split(path)[0], 'frames.asm')
- bitmask_lines = open(bitmask_path).readlines()
- frame_lines = open(frame_path).readlines()
-
- bitmask_length = w * h
-
- bitmasks = []
- bitmask = []
- for line in bitmask_lines:
- if '\tdb ' in line:
- value = line.split('\tdb ')[1].strip().replace('%', '0b')
- value = int(value, 0)
- #print line.strip(), value, len(bitmasks), len(bitmask)
- for bit in xrange(8):
- bitmask += [(value >> bit) & 1]
- if len(bitmask) >= bitmask_length:
- bitmasks += [bitmask]
- bitmask = []
- break
- if bitmask:
- bitmasks += [bitmask]
-
- frames = []
- frame_labels = []
- i = 0
- for line in frame_lines:
- if '\tdw ' in line:
- frame_labels += [line.split('\tdw ')[1].strip()]
- else:
- for part in line.split():
- part = part.strip()
- if part in frame_labels:
- frames += [(part, i)]
- i += 1
-
- results = []
-
- for label, i in frames:
- result = []
-
- # get the bitmask and tile ids for each frame
- # don't care if we read past bounds, so just read the rest of the file
- values = []
- for line in frame_lines[i:]:
- if '\tdb ' in line:
- values += line.split('\tdb ')[1].split(';')[0].split(',')
-
- #print bitmasks
- #print values[0]
- #print int(values[0].replace('$', '0x'), 0)
- bitmask = bitmasks[int(values[0].replace('$', '0x'), 0)]
- tiles = values[1:]
- k = 0
- j = 0
- for bit in bitmask:
- if bit:
- result += [int(tiles[k].replace('$', '0x'), 0)]
- k += 1
- else:
- result += [j]
- j += 1
-
- results += [result]
-
- return results
-
-def get_animated_graphics(path, w=7, h=7, bitmask_path=None, frame_path=None):
- frames = get_animation_frames(path, w, h, bitmask_path, frame_path)
- new_path = path.replace('.animated.2bpp', '.2bpp')
- tiles = gfx.get_tiles(bytearray(open(path, 'rb').read()))
- new_tiles = tiles[:w * h]
- for frame in frames:
- for tile in frame:
- new_tiles += [tiles[tile]]
- new_graphic = gfx.connect(new_tiles)
- print new_path, list(new_graphic)
- open(new_path, 'wb').write(bytearray(new_graphic))
- return new_path
-
-def filepath_rules(filepath):
- """Infer attributes of certain graphics by their location in the filesystem."""
- args = {}
-
- filedir, filename = os.path.split(filepath)
- if filedir.startswith('./'):
- filedir = filedir[2:]
-
- name, ext = os.path.splitext(filename)
- if ext == '.lz':
- name, ext = os.path.splitext(name)
-
- pokemon_name = ''
-
- if 'gfx/pokemon/' in filedir:
- pokemon_name = filedir.split('/')[-1]
- if pokemon_name.startswith('unown_'):
- index = filedir.find(pokemon_name)
- if index != -1:
- filedir = filedir[:index + len('unown')] + filedir[index + len('unown_a'):]
- if name == 'front' or name == 'front.animated':
- args['pal_file'] = os.path.join(filedir, 'normal.pal')
- args['pic'] = True
- args['animate'] = True
- elif name == 'back':
- args['pal_file'] = os.path.join(filedir, 'normal.pal')
- args['pic'] = True
-
- elif 'gfx/trainers' in filedir:
- args['pic'] = True
-
- elif os.path.join(filedir, name) in pics:
- args['pic'] = True
-
- elif filedir == 'gfx/tilesets':
- args['tileset'] = True
-
- if args.get('pal_file'):
- if os.path.exists(args['pal_file']):
- args['palout'] = args['pal_file']
- else:
- del args['pal_file']
-
- if args.get('pic'):
- if ext == '.png':
- w, h = gfx.png.Reader(filepath).asRGBA8()[:2]
- w = min(w/8, h/8)
- args['pic_dimensions'] = w, w
- elif ext == '.2bpp':
- if pokemon_name and name == 'front' or name == 'front.animated':
- w, h = get_pokemon_dimensions(filepath.replace(ext, '.dimensions')) or (7, 7)
- args['pic_dimensions'] = w, w
- elif pokemon_name and name == 'back':
- args['pic_dimensions'] = 6, 6
- else:
- args['pic_dimensions'] = 7, 7
-
- if args.get('tileset'):
- args['width'] = 128
- return args
-
-
-def to_1bpp(filename, **kwargs):
- name, ext = os.path.splitext(filename)
- if ext == '.1bpp': pass
- elif ext == '.2bpp': gfx.export_2bpp_to_1bpp(filename, **kwargs)
- elif ext == '.png': gfx.export_png_to_1bpp(filename, **kwargs)
- elif ext == '.lz':
- decompress(filename, **kwargs)
- to_1bpp(name, **kwargs)
-
-def to_2bpp(filename, **kwargs):
- name, ext = os.path.splitext(filename)
- if ext == '.1bpp': gfx.export_1bpp_to_2bpp(filename, **kwargs)
- elif ext == '.2bpp': pass
- elif ext == '.png': gfx.export_png_to_2bpp(filename, **kwargs)
- elif ext == '.lz':
- decompress(filename, **kwargs)
- to_2bpp(name, **kwargs)
-
-def to_png(filename, **kwargs):
- name, ext = os.path.splitext(filename)
- if ext == '.1bpp': gfx.export_1bpp_to_png(filename, **kwargs)
- elif ext == '.2bpp' and name.endswith('.animated'):
- w, h = kwargs.get('pic_dimensions') or (7, 7)
- new_path = get_animated_graphics(filename, w=w, h=h)
- return to_png(new_path, **kwargs)
- elif ext == '.2bpp': gfx.export_2bpp_to_png(filename, **kwargs)
- elif ext == '.png': pass
- elif ext == '.lz':
- decompress(filename, **kwargs)
- to_png(name, **kwargs)
-
-def compress(filename, **kwargs):
- data = open(filename, 'rb').read()
- lz_data = lz.Compressed(data).output
- open(filename + '.lz', 'wb').write(bytearray(lz_data))
-
-def decompress(filename, **kwargs):
- lz_data = open(filename, 'rb').read()
- data = lz.Decompressed(lz_data).output
- name, ext = os.path.splitext(filename)
- open(name, 'wb').write(bytearray(data))
-
-
-methods = {
- '2bpp': to_2bpp,
- '1bpp': to_1bpp,
- 'png': to_png,
- 'lz': compress,
- 'unlz': decompress,
-}
-
-def main(method_name, filenames=None):
- if filenames is None: filenames = []
- for filename in filenames:
- args = filepath_rules(filename)
- method = methods.get(method_name)
- if method:
- method(filename, **args)
-
-def get_args():
- ap = argparse.ArgumentParser()
- ap.add_argument('method_name')
- ap.add_argument('filenames', nargs='*')
- args = ap.parse_args()
- return args
-
-if __name__ == '__main__':
- main(**get_args().__dict__)
diff --git a/tools/palfix.py b/tools/palfix.py
deleted file mode 100755
index 3a997d54..00000000
--- a/tools/palfix.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python palfix.py image.png
-
-Fix the palette format of the input image to two-bit grayscale.
-Colored images will have their palette sorted {white, light
-color, dark color, black}.
-"""
-
-import sys
-
-import png
-
-def rgb8_to_rgb5(c):
- r, g, b = c
- return (r // 8, g // 8, b // 8)
-
-def invert(c):
- r, g, b = c
- return (31 - r, 31 - g, 31 - b)
-
-def luminance(c):
- r, g, b = c
- return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2
-
-def rgb5_pixels(row):
- yield from (rgb8_to_rgb5(row[x:x+3]) for x in range(0, len(row), 4))
-
-def fix_pal(filename):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = list(rows)
- b_and_w = {(0, 0, 0), (31, 31, 31)}
- colors = {c for row in rows for c in rgb5_pixels(row)} - b_and_w
- if not colors:
- colors = {(21, 21, 21), (10, 10, 10)}
- elif len(colors) == 1:
- c = colors.pop()
- colors = {c, invert(c)}
- elif len(colors) != 2:
- return False
- palette = tuple(sorted(colors | b_and_w, key=luminance, reverse=True))
- assert len(palette) == 4
- rows = [[3 - palette.index(c) for c in rgb5_pixels(row)] for row in rows]
- writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
- return True
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- elif not fix_pal(filename):
- print(f'{filename} has too many colors!', file=sys.stderr)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/pic.py b/tools/pic.py
deleted file mode 100755
index 7a663832..00000000
--- a/tools/pic.py
+++ /dev/null
@@ -1,491 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-
-"""
-A library for use with compressed monster and trainer pics in pokeyellow.
-"""
-from __future__ import absolute_import
-from __future__ import division
-
-import os
-import sys
-import argparse
-from math import sqrt
-
-from pokemontools import gfx
-
-
-def bitflip(x, n):
- r = 0
- while n:
- r = (r << 1) | (x & 1)
- x >>= 1
- n -= 1
- return r
-
-
-class Decompressor:
- """
- pokeyellow pic decompression.
-
- Ported to python 2.7 from the python 3 code at https://github.com/magical/pokemon-sprites-rby.
- """
-
- table1 = [(2 << i) - 1 for i in range(16)]
- table2 = [
- [0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5, 0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa],
- [0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa, 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5], # prev ^ 0xf
- [0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa, 0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5],
- [0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5, 0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa], # prev ^ 0xf
- ]
- table3 = [bitflip(i, 4) for i in range(16)]
-
- tilesize = 8
-
-
- def __init__(self, f, mirror=False, planar=True):
- self.bs = fbitstream(f)
- self.mirror = mirror
- self.planar = planar
- self.data = None
-
- def decompress(self):
- rams = [[], []]
-
- self.sizex = self._readint(4) * self.tilesize
- self.sizey = self._readint(4)
-
- self.size = self.sizex * self.sizey
-
- self.ramorder = self._readbit()
-
- r1 = self.ramorder
- r2 = self.ramorder ^ 1
-
- self._fillram(rams[r1])
- mode = self._readbit()
- if mode:
- mode += self._readbit()
- self._fillram(rams[r2])
-
- rams[0] = bytearray(bitgroups_to_bytes(rams[0]))
- rams[1] = bytearray(bitgroups_to_bytes(rams[1]))
-
- if mode == 0:
- self._decode(rams[0])
- self._decode(rams[1])
- elif mode == 1:
- self._decode(rams[r1])
- self._xor(rams[r1], rams[r2])
- elif mode == 2:
- self._decode(rams[r2], mirror=False)
- self._decode(rams[r1])
- self._xor(rams[r1], rams[r2])
- else:
- raise Exception("Invalid deinterlace mode!")
-
- data = []
- if self.planar:
- for a, b in zip(rams[0], rams[1]):
- data += [a, b]
- self.data = bytearray(data)
- else:
- for a, b in zip(bitstream(rams[0]), bitstream(rams[1])):
- data.append(a | (b << 1))
- self.data = bitgroups_to_bytes(data)
-
- def _fillram(self, ram):
- mode = ['rle', 'data'][self._readbit()]
- size = self.size * 4
- while len(ram) < size:
- if mode == 'rle':
- self._read_rle_chunk(ram)
- mode = 'data'
- elif mode == 'data':
- self._read_data_chunk(ram, size)
- mode = 'rle'
- if len(ram) > size:
- #ram = ram[:size]
- raise ValueError(size, len(ram))
-
- ram[:] = self._deinterlace_bitgroups(ram)
-
- def _read_rle_chunk(self, ram):
-
- i = 0
- while self._readbit():
- i += 1
-
- n = self.table1[i]
- a = self._readint(i + 1)
- n += a
-
- for i in range(n):
- ram.append(0)
-
- def _read_data_chunk(self, ram, size):
- while 1:
- bitgroup = self._readint(2)
- if bitgroup == 0:
- break
- ram.append(bitgroup)
-
- if size <= len(ram):
- break
-
- def _decode(self, ram, mirror=None):
- if mirror is None:
- mirror = self.mirror
-
- for x in range(self.sizex):
- bit = 0
- for y in range(self.sizey):
- i = y * self.sizex + x
- a = (ram[i] >> 4) & 0xf
- b = ram[i] & 0xf
-
- a = self.table2[bit][a]
- bit = a & 1
- if mirror:
- a = self.table3[a]
-
- b = self.table2[bit][b]
- bit = b & 1
- if mirror:
- b = self.table3[b]
-
- ram[i] = (a << 4) | b
-
- def _xor(self, ram1, ram2, mirror=None):
- if mirror is None:
- mirror = self.mirror
-
- for i in range(len(ram2)):
- if mirror:
- a = (ram2[i] >> 4) & 0xf
- b = ram2[i] & 0xf
- a = self.table3[a]
- b = self.table3[b]
- ram2[i] = (a << 4) | b
-
- ram2[i] ^= ram1[i]
-
- def _deinterlace_bitgroups(self, bits):
- l = []
- for y in range(self.sizey):
- for x in range(self.sizex):
- i = 4 * y * self.sizex + x
- for j in range(4):
- l.append(bits[i])
- i += self.sizex
- return l
-
-
- def _readbit(self):
- return next(self.bs)
-
- def _readint(self, count):
- return readint(self.bs, count)
-
-
-def fbitstream(f):
- while 1:
- char = f.read(1)
- if not char:
- break
- byte = ord(char)
-
- for i in range(7, -1, -1):
- yield (byte >> i) & 1
-
-def bitstream(b):
- for byte in b:
- for i in range(7, -1, -1):
- yield (byte >> i) & 1
-
-def readint(bs, count):
- n = 0
- while count:
- n <<= 1
- n |= next(bs)
- count -= 1
- return n
-
-def bitgroups_to_bytes(bits):
- l = []
- for i in range(0, len(bits) - 3, 4):
- n = ((bits[i + 0] << 6)
- | (bits[i + 1] << 4)
- | (bits[i + 2] << 2)
- | (bits[i + 3] << 0))
- l.append(n)
- return bytearray(l)
-
-
-def bytes_to_bits(bytelist):
- return list(bitstream(bytelist))
-
-
-class Compressor:
- """
- pokeyellow pic compression.
-
- Adapted from stag019's C compressor.
- """
-
- table1 = [(2 << i) - 1 for i in range(16)]
- table2 = [
- [0x0, 0x1, 0x3, 0x2, 0x6, 0x7, 0x5, 0x4, 0xc, 0xd, 0xf, 0xe, 0xa, 0xb, 0x9, 0x8],
- [0x8, 0x9, 0xb, 0xa, 0xe, 0xf, 0xd, 0xc, 0x4, 0x5, 0x7, 0x6, 0x2, 0x3, 0x1, 0x0], # reverse
- ]
- table3 = [bitflip(i, 4) for i in range(16)]
-
- def __init__(self, image, width=None, height=None):
- self.image = bytearray(image)
- self.size = len(self.image)
-
- planar_tile = 8 * 8 // 4
- tile_size = self.size // planar_tile
- if height and not width: width = tile_size // height
- elif width and not height: height = tile_size // width
- elif not width and not height: width = height = int(sqrt(tile_size))
- self.width, self.height = width, height
-
- def compress(self):
- """
- Compress the image five times (twice for each mode, except 0)
- and use the smallest one (in bits).
- """
- rams = [[],[]]
- datas = []
-
- for mode in range(3):
-
- # Order is redundant for mode 0.
-
- # While this seems like an optimization,
- # it's actually required for 1:1 compression
- # to the original compressed pics.
-
- # This appears to be the algorithm
- # that Game Freak's compressor used.
-
- # Using order 0 instead of 1 breaks this feature.
-
- for order in range(2):
- if mode == 0 and order == 0:
- continue
- for i in range(2):
- rams[i] = self.image[i::2]
- self._interpret_compress(rams, mode, order)
- datas += [(self.data[:], int(self.which_bit))]
-
- # Pick the smallest pic, measured in bits.
- datas = sorted(datas, key=lambda data_bit: (len(data_bit[0]), -data_bit[1]))
- self.data, self.which_bit = datas[0]
-
- def _interpret_compress(self, rams, mode, order):
- self.data = []
- self.which_bit = 0
-
- r1 = order
- r2 = order ^ 1
-
- if mode == 0:
- self._encode(rams[1])
- self._encode(rams[0])
- elif mode == 1:
- self._xor(rams[r1], rams[r2])
- self._encode(rams[r1])
- elif mode == 2:
- self._xor(rams[r1], rams[r2])
- self._encode(rams[r1])
- self._encode(rams[r2], mirror=False)
- else:
- raise Exception('invalid interlace mode!')
-
- self._writeint(self.height, 4)
- self._writeint(self.width, 4)
-
- self._writebit(order)
-
- self._fillram(rams[r1])
- if mode == 0:
- self._writebit(0)
- else:
- self._writebit(1)
- self._writebit(mode - 1)
- self._fillram(rams[r2])
-
- def _fillram(self, ram):
- rle = 0
- nums = 0
- bitgroups = []
-
- for x in range(self.width):
- for bit in range(0, 8, 2):
- byte = x * self.height * 8
- for y in range(self.height * 8):
- bitgroup = (ram[byte] >> (6 - bit)) & 3
- if bitgroup == 0:
- if rle == 0:
- self._writebit(0)
- elif rle == 1:
- nums += 1
- else:
- self._data_packet(bitgroups)
- self._writebit(0)
- self._writebit(0)
- rle = 1
- bitgroups = []
- else:
- if rle == 0:
- self._writebit(1)
- elif rle == 1:
- self._rle(nums)
- rle = -1
- bitgroups += [bitgroup]
- nums = 0
- byte += 1
-
- if rle == 1:
- self._rle(nums)
- else:
- self._data_packet(bitgroups)
-
- def _data_packet(self, bitgroups):
- for bitgroup in bitgroups:
- self._writebit((bitgroup >> 1) & 1)
- self._writebit((bitgroup >> 0) & 1)
-
- def _rle(self, nums):
- nums += 1
-
- # Get the previous power of 2.
- # Deriving the bitcount from that seems to be
- # faster on average than using the lookup table.
- v = nums
- v += 1
- v |= v >> 1
- v |= v >> 2
- v |= v >> 4
- v |= v >> 8
- v |= v >> 16
- v -= v >> 1
- v -= 1
- number = nums - v
-
- bitcount = -1
- while v:
- v >>= 1
- bitcount += 1
-
- for j in range(bitcount):
- self._writebit(1)
- self._writebit(0)
- for j in range(bitcount, -1, -1):
- self._writebit((number >> j) & 1)
-
- def _encode(self, ram, mirror=None):
- a = b = 0
- for i in range(len(ram)):
- j = i // self.height
- j += i % self.height * self.width * 8
- if i % self.height == 0:
- b = 0
-
- a = (ram[j] >> 4) & 0xf
- table = b & 1
- code_1 = self.table2[table][a]
-
- b = ram[j] & 0xf
- table = a & 1
- code_2 = self.table2[table][b]
-
- ram[j] = (code_1 << 4) | code_2
-
- def _xor(self, ram1, ram2):
- for i in range(len(ram2)):
- ram2[i] ^= ram1[i]
-
- def _writebit(self, bit):
- self.which_bit -= 1
- if self.which_bit == -1:
- self.which_bit = 7
- self.data += [0]
- if bit: self.data[-1] |= bit << self.which_bit
-
- def _writeint(self, num, size=None):
- bits = []
- if size:
- for i in range(size):
- bits += [num & 1]
- num >>= 1
- else:
- while num > 0:
- bits += [num & 1]
- num >>= 1
- for bit in reversed(bits):
- self._writebit(bit)
-
-
-def decompress(f, offset=None, mirror=False):
- """
- Decompress a pic given a file object. Return a planar 2bpp image.
-
- Optional: offset (for roms).
- """
- if offset is not None:
- f.seek(offset)
- dcmp = Decompressor(f, mirror=mirror)
- dcmp.decompress()
- return dcmp.data
-
-
-def compress(f):
- """
- Compress a planar 2bpp into a pic.
- """
- comp = Compressor(f)
- comp.compress()
- return comp.data
-
-
-def decompress_file(filename):
- """
- Decompress a pic given a filename.
- Export the resulting planar 2bpp image to
- """
- pic = open(filename, 'rb')
- image = decompress(pic)
- image = gfx.transpose_tiles(image)
- image = bytearray(image)
- output_filename = os.path.splitext(filename)[0] + '.2bpp'
- with open(output_filename, 'wb') as out:
- out.write(image)
-
-def compress_file(filename):
- image = open(filename, 'rb').read()
- image = gfx.transpose_tiles(image)
- pic = compress(image)
- pic = bytearray(pic)
- output_filename = os.path.splitext(filename)[0] + '.pic'
- with open(output_filename, 'wb') as out:
- out.write(pic)
-
-
-def main():
- ap = argparse.ArgumentParser()
- ap.add_argument('mode')
- ap.add_argument('filenames', nargs='*')
- args = ap.parse_args()
-
- for filename in args.filenames:
- if args.mode == 'decompress':
- decompress_file(filename)
- elif args.mode == 'compress':
- compress_file(filename)
-
-if __name__ == '__main__':
- main()
-
diff --git a/tools/pkmncompress.c b/tools/pkmncompress.c
index 0e20df4f..60d6fd0f 100644
--- a/tools/pkmncompress.c
+++ b/tools/pkmncompress.c
@@ -1,25 +1,29 @@
-/*
- * Copyright © 2013 stag019 <stag019@gmail.com>
- *
- * Permission to use, copy, modify, and distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
#define PROGRAM_NAME "pkmncompress"
-#define USAGE_OPTS "infile.2bpp outfile.pic"
+#define USAGE_OPTS "[-h|--help] [-u|--uncompress] infile.2bpp outfile.pic"
#include "common.h"
-uint8_t compressed[15 * 15 * 0x10];
+void parse_args(int argc, char *argv[], bool *uncomp) {
+ struct option long_options[] = {
+ {"uncompress", no_argument, 0, 'u'},
+ {"help", no_argument, 0, 'h'},
+ {0}
+ };
+ for (int opt; (opt = getopt_long(argc, argv, "uh", long_options)) != -1;) {
+ switch (opt) {
+ case 'u':
+ *uncomp = true;
+ break;
+ case 'h':
+ usage_exit(0);
+ break;
+ default:
+ usage_exit(1);
+ }
+ }
+}
+
+uint8_t output[15 * 15 * 0x10];
int cur_bit;
int cur_byte;
@@ -28,7 +32,30 @@ void write_bit(int bit) {
cur_byte++;
cur_bit = 0;
}
- compressed[cur_byte] |= bit << (7 - cur_bit);
+ output[cur_byte] |= bit << (7 - cur_bit);
+}
+
+int read_bit(uint8_t *data) {
+ if (cur_bit == -1) {
+ cur_byte++;
+ cur_bit = 7;
+ }
+ return (data[cur_byte] >> cur_bit--) & 1;
+}
+
+void transpose_tiles(uint8_t *data, int width) {
+ int size = width * width;
+ for (int i = 0; i < size; i++) {
+ int j = (i * width + i / width) % size;
+ if (i < j) {
+ uint8_t tmp[0x10];
+ uint8_t *p = data + i * COUNTOF(tmp);
+ uint8_t *q = data + j * COUNTOF(tmp);
+ memcpy(tmp, p, COUNTOF(tmp));
+ memcpy(p, q, COUNTOF(tmp));
+ memcpy(q, tmp, COUNTOF(tmp));
+ }
+ }
}
void compress_plane(uint8_t *plane, int width) {
@@ -83,32 +110,26 @@ void write_data_packet(uint8_t *bit_groups, int n) {
}
}
-int interpret_compress(uint8_t *plane1, uint8_t *plane2, int mode, int order, int width) {
+int interpret_compress(uint8_t *planes[2], int mode, int order, int width) {
int ram_size = width * width * 8;
- uint8_t *_plane1 = xmalloc(ram_size);
- uint8_t *_plane2 = xmalloc(ram_size);
- if (order) {
- memcpy(_plane1, plane2, ram_size);
- memcpy(_plane2, plane1, ram_size);
- } else {
- memcpy(_plane1, plane1, ram_size);
- memcpy(_plane2, plane2, ram_size);
- }
- if (mode != 1) {
+ uint8_t *rams[2] = {xmalloc(ram_size), xmalloc(ram_size)};
+ memcpy(rams[0], planes[order], ram_size);
+ memcpy(rams[1], planes[order ^ 1], ram_size);
+ if (mode != 0) {
for (int i = 0; i < ram_size; i++) {
- _plane2[i] ^= _plane1[i];
+ rams[1][i] ^= rams[0][i];
}
}
- compress_plane(_plane1, width);
- if (mode != 2) {
- compress_plane(_plane2, width);
+ compress_plane(rams[0], width);
+ if (mode != 1) {
+ compress_plane(rams[1], width);
}
cur_bit = 7;
cur_byte = 0;
- memset(compressed, 0, COUNTOF(compressed));
- compressed[0] = (width << 4) | width;
+ memset(output, 0, COUNTOF(output));
+ output[0] = (width << 4) | width;
write_bit(order);
- uint8_t bit_groups[0x1000] = {0};
+ uint8_t bit_groups[15 * 4 * 15 * 8] = {0};
int index = 0;
for (int plane = 0; plane < 2; plane++) {
int type = 0;
@@ -117,9 +138,18 @@ int interpret_compress(uint8_t *plane1, uint8_t *plane2, int mode, int order, in
for (int x = 0; x < width; x++) {
for (int bit = 0; bit < 8; bit += 2) {
for (int y = 0, byte = x * width * 8; y < width * 8; y++, byte++) {
- int bit_group = ((plane ? _plane2 : _plane1)[byte] >> (6 - bit)) & 3;
- if (!bit_group) {
- if (!type) {
+ int bit_group = (rams[plane][byte] >> (6 - bit)) & 3;
+ if (bit_group) {
+ if (type == 0) {
+ write_bit(1);
+ } else if (type == 1) {
+ rle_encode_number(nums);
+ }
+ type = 2;
+ bit_groups[index++] = bit_group;
+ nums = 0;
+ } else {
+ if (type == 0) {
write_bit(0);
} else if (type == 1) {
nums++;
@@ -131,15 +161,6 @@ int interpret_compress(uint8_t *plane1, uint8_t *plane2, int mode, int order, in
type = 1;
memset(bit_groups, 0, COUNTOF(bit_groups));
index = 0;
- } else {
- if (!type) {
- write_bit(1);
- } else if (type == 1) {
- rle_encode_number(nums);
- }
- type = 2;
- bit_groups[index++] = bit_group;
- nums = 0;
}
}
}
@@ -150,83 +171,193 @@ int interpret_compress(uint8_t *plane1, uint8_t *plane2, int mode, int order, in
write_data_packet(bit_groups, index);
}
if (!plane) {
- if (mode < 2) {
+ if (mode == 0) {
write_bit(0);
} else {
write_bit(1);
- write_bit(mode - 2);
+ write_bit(mode - 1);
}
}
}
- free(_plane1);
- free(_plane2);
+ free(rams[0]);
+ free(rams[1]);
return (cur_byte + 1) * 8 + cur_bit;
}
-int compress(uint8_t *data, int width) {
+int get_width(long filesize) {
+ int width = 0;
+ for (int w = 1; w < 16; w++) {
+ if (filesize == w * w * 0x10) {
+ width = w;
+ break;
+ }
+ }
+ if (!width) {
+ error_exit("Image is not a square, or is larger than 15x15 tiles");
+ }
+ return width;
+}
+
+int compress(uint8_t *data, long filesize) {
+ int width = get_width(filesize);
int ram_size = width * width * 8;
- uint8_t *plane1 = xmalloc(ram_size);
- uint8_t *plane2 = xmalloc(ram_size);
+ uint8_t *planes[2] = {xmalloc(ram_size), xmalloc(ram_size)};
+ transpose_tiles(data, width);
for (int i = 0; i < ram_size; i++) {
- plane1[i] = data[i * 2];
- plane2[i] = data[i * 2 + 1];
+ planes[0][i] = data[i * 2];
+ planes[1][i] = data[i * 2 + 1];
}
- uint8_t current[COUNTOF(compressed)] = {0};
+ uint8_t current[COUNTOF(output)] = {0};
int compressed_size = -1;
- for (int mode = 1; mode < 4; mode++) {
+ for (int mode = 0; mode < 3; mode++) {
for (int order = 0; order < 2; order++) {
- if (mode == 1 && order == 0) {
+ if (mode == 0 && order == 0) {
continue;
}
- int new_size = interpret_compress(plane1, plane2, mode, order, width);
+ int new_size = interpret_compress(planes, mode, order, width);
if (compressed_size == -1 || new_size < compressed_size) {
compressed_size = new_size;
memset(current, 0, COUNTOF(current));
- memcpy(current, compressed, compressed_size / 8);
+ memcpy(current, output, compressed_size / 8);
}
}
}
- memset(compressed, 0, COUNTOF(compressed));
- memcpy(compressed, current, compressed_size / 8);
- free(plane1);
- free(plane2);
+ memset(output, 0, COUNTOF(output));
+ memcpy(output, current, compressed_size / 8);
+ free(planes[0]);
+ free(planes[1]);
return compressed_size / 8;
}
-uint8_t *transpose_tiles(uint8_t *data, int width) {
- int size = width * width * 0x10;
- uint8_t *transposed = xmalloc(size);
- for (int i = 0; i < size; i++) {
- int j = (i / 0x10) * width * 0x10;
- j = (j % size) + 0x10 * (j / size) + (i % 0x10);
- transposed[j] = data[i];
+int read_int(uint8_t *data, int count) {
+ int n = 0;
+ while (count--) {
+ n = (n << 1) | read_bit(data);
}
- free(data);
- return transposed;
+ return n;
}
-int main(int argc, char *argv[]) {
- if (argc != 3) {
- usage_exit(1);
+uint8_t *fill_plane(uint8_t *data, int width) {
+ static int table[0x10] = {
+ 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
+ 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF,
+ };
+ int mode = read_bit(data);
+ int size = width * width * 0x20;
+ uint8_t *plane = xmalloc(size);
+ int len = 0;
+ while (len < size) {
+ if (mode) {
+ while (len < size) {
+ int bit_group = read_int(data, 2);
+ if (!bit_group) {
+ break;
+ }
+ plane[len++] = bit_group;
+ }
+ } else {
+ size_t w = 0;
+ while (read_bit(data)) {
+ w++;
+ }
+ if (w >= COUNTOF(table)) {
+ error_exit("Invalid compressed data");
+ }
+ int n = table[w] + read_int(data, w + 1);
+ while (len < size && n--) {
+ plane[len++] = 0;
+ }
+ }
+ mode ^= 1;
+ }
+ if (len > size) {
+ error_exit("Invalid compressed data");
+ }
+ uint8_t *ram = xmalloc(size);
+ len = 0;
+ for (int y = 0; y < width; y++) {
+ for (int x = 0; x < width * 8; x++) {
+ for (int i = 0; i < 4; i++) {
+ ram[len++] = plane[(y * 4 + i) * width * 8 + x];
+ }
+ }
}
+ for (int i = 0; i < size - 3; i += 4) {
+ ram[i / 4] = (ram[i] << 6) | (ram[i + 1] << 4) | (ram[i + 2] << 2) | ram[i + 3];
+ }
+ free(plane);
+ return ram;
+}
- long filesize;
- uint8_t *data = read_u8(argv[1], &filesize);
+void uncompress_plane(uint8_t *plane, int width) {
+ static int codes[2][0x10] = {
+ {0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5, 0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA},
+ {0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA, 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5},
+ };
+ for (int x = 0; x < width * 8; x++) {
+ int bit = 0;
+ for (int y = 0; y < width; y++) {
+ int i = y * width * 8 + x;
+ int nybble_hi = (plane[i] >> 4) & 0xF;
+ int code_hi = codes[bit][nybble_hi];
+ bit = code_hi & 1;
+ int nybble_lo = plane[i] & 0xF;
+ int code_lo = codes[bit][nybble_lo];
+ bit = code_lo & 1;
+ plane[i] = (code_hi << 4) | code_lo;
+ }
+ }
+}
- int width = 0;
- for (int w = 1; w < 16; w++) {
- if (filesize == w * w * 0x10) {
- width = w;
- break;
+int uncompress(uint8_t *data) {
+ cur_bit = 7;
+ int width = read_int(data, 4);
+ if (read_int(data, 4) != width) {
+ error_exit("Image is not a square");
+ }
+ int size = width * width * 8;
+ uint8_t *rams[2];
+ int order = read_bit(data);
+ rams[order] = fill_plane(data, width);
+ int mode = read_bit(data);
+ if (mode) {
+ mode += read_bit(data);
+ }
+ rams[order ^ 1] = fill_plane(data, width);
+ uncompress_plane(rams[order], width);
+ if (mode != 1) {
+ uncompress_plane(rams[order ^ 1], width);
+ }
+ if (mode != 0) {
+ for (int i = 0; i < size; i++) {
+ rams[order ^ 1][i] ^= rams[order][i];
}
}
- if (!width) {
- error_exit("Image is not a square, or is larger than 15x15 tiles");
+ for (int i = 0; i < size; i++) {
+ output[i * 2] = rams[0][i];
+ output[i * 2 + 1] = rams[1][i];
}
+ transpose_tiles(output, width);
+ free(rams[0]);
+ free(rams[1]);
+ return size * 2;
+}
+
+int main(int argc, char *argv[]) {
+ bool uncomp = false;
+ parse_args(argc, argv, &uncomp);
+
+ argc -= optind;
+ argv += optind;
+ if (argc < 1) {
+ usage_exit(1);
+ }
+
+ long filesize;
+ uint8_t *data = read_u8(argv[0], &filesize);
- data = transpose_tiles(data, width);
- int compressed_size = compress(data, width);
- write_u8(argv[2], compressed, compressed_size);
+ int output_size = uncomp ? uncompress(data) : compress(data, filesize);
+ write_u8(argv[1], output, output_size);
free(data);
return 0;
diff --git a/tools/png.py b/tools/png.py
deleted file mode 100644
index dd9746bf..00000000
--- a/tools/png.py
+++ /dev/null
@@ -1,2357 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# png.py - PNG encoder/decoder in pure Python
-#
-# Copyright (C) 2006 Johann C. Rocholl <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.
-
-"""
-The ``png`` module can read and write PNG files.
-
-Installation and Overview
--------------------------
-
-``pip install pypng``
-
-For help, type ``import png; help(png)`` in your python interpreter.
-
-A good place to start is the :class:`Reader` and :class:`Writer` classes.
-
-Coverage of PNG formats is fairly complete;
-all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and
-colour combinations are supported:
-
-- greyscale (1/2/4/8/16 bit);
-- RGB, RGBA, LA (greyscale with alpha) with 8/16 bits per channel;
-- colour mapped images (1/2/4/8 bit).
-
-Interlaced images,
-which support a progressive display when downloading,
-are supported for both reading and writing.
-
-A number of optional chunks can be specified (when writing)
-and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
-
-The ``sBIT`` chunk can be used to specify precision for
-non-native bit depths.
-
-Requires Python 3.5 or higher.
-Installation is trivial,
-but see the ``README.txt`` file (with the source distribution) for details.
-
-Full use of all features will need some reading of the PNG specification
-http://www.w3.org/TR/2003/REC-PNG-20031110/.
-
-The package also comes with command line utilities.
-
-- ``pripamtopng`` converts
- `Netpbm <http://netpbm.sourceforge.net/>`_ PAM/PNM files to PNG;
-- ``pripngtopam`` converts PNG to file PAM/PNM.
-
-There are a few more for simple PNG manipulations.
-
-Spelling and Terminology
-------------------------
-
-Generally British English spelling is used in the documentation.
-So that's "greyscale" and "colour".
-This not only matches the author's native language,
-it's also used by the PNG specification.
-
-Colour Models
--------------
-
-The major colour models supported by PNG (and hence by PyPNG) are:
-
-- greyscale;
-- greyscale--alpha;
-- RGB;
-- RGB--alpha.
-
-Also referred to using the abbreviations: L, LA, RGB, RGBA.
-Each letter codes a single channel:
-*L* is for Luminance or Luma or Lightness (greyscale images);
-*A* stands for Alpha, the opacity channel
-(used for transparency effects, but higher values are more opaque,
-so it makes sense to call it opacity);
-*R*, *G*, *B* stand for Red, Green, Blue (colour image).
-
-Lists, arrays, sequences, and so on
------------------------------------
-
-When getting pixel data out of this module (reading) and
-presenting data to this module (writing) there are
-a number of ways the data could be represented as a Python value.
-
-The preferred format is a sequence of *rows*,
-which each row being a sequence of *values*.
-In this format, the values are in pixel order,
-with all the values from all the pixels in a row
-being concatenated into a single sequence for that row.
-
-Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
-has RGB components:
-
-Sequence of rows::
-
- list([R,G,B, R,G,B, R,G,B],
- [R,G,B, R,G,B, R,G,B])
-
-Each row appears as its own list,
-but the pixels are flattened so that three values for one pixel
-simply follow the three values for the previous pixel.
-
-This is the preferred because
-it provides a good compromise between space and convenience.
-PyPNG regards itself as at liberty to replace any sequence type with
-any sufficiently compatible other sequence type;
-in practice each row is an array (``bytearray`` or ``array.array``).
-
-To allow streaming the outer list is sometimes
-an iterator rather than an explicit list.
-
-An alternative format is a single array holding all the values.
-
-Array of values::
-
- [R,G,B, R,G,B, R,G,B,
- R,G,B, R,G,B, R,G,B]
-
-The entire image is one single giant sequence of colour values.
-Generally an array will be used (to save space), not a list.
-
-The top row comes first,
-and within each row the pixels are ordered from left-to-right.
-Within a pixel the values appear in the order R-G-B-A
-(or L-A for greyscale--alpha).
-
-There is another format, which should only be used with caution.
-It is mentioned because it is used internally,
-is close to what lies inside a PNG file itself,
-and has some support from the public API.
-This format is called *packed*.
-When packed, each row is a sequence of bytes (integers from 0 to 255),
-just as it is before PNG scanline filtering is applied.
-When the bit depth is 8 this is the same as a sequence of rows;
-when the bit depth is less than 8 (1, 2 and 4),
-several pixels are packed into each byte;
-when the bit depth is 16 each pixel value is decomposed into 2 bytes
-(and `packed` is a misnomer).
-This format is used by the :meth:`Writer.write_packed` method.
-It isn't usually a convenient format,
-but may be just right if the source data for
-the PNG image comes from something that uses a similar format
-(for example, 1-bit BMPs, or another PNG file).
-"""
-
-__version__ = "0.0.21"
-
-import collections
-import io # For io.BytesIO
-import itertools
-import math
-# http://www.python.org/doc/2.4.4/lib/module-operator.html
-import operator
-import re
-import struct
-import sys
-# http://www.python.org/doc/2.4.4/lib/module-warnings.html
-import warnings
-import zlib
-
-from array import array
-
-
-__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
-
-
-# The PNG signature.
-# http://www.w3.org/TR/PNG/#5PNG-file-signature
-signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
-
-# The xstart, ystart, xstep, ystep for the Adam7 interlace passes.
-adam7 = ((0, 0, 8, 8),
- (4, 0, 8, 8),
- (0, 4, 4, 8),
- (2, 0, 4, 4),
- (0, 2, 2, 4),
- (1, 0, 2, 2),
- (0, 1, 1, 2))
-
-
-def adam7_generate(width, height):
- """
- Generate the coordinates for the reduced scanlines
- of an Adam7 interlaced image
- of size `width` by `height` pixels.
-
- Yields a generator for each pass,
- and each pass generator yields a series of (x, y, xstep) triples,
- each one identifying a reduced scanline consisting of
- pixels starting at (x, y) and taking every xstep pixel to the right.
- """
-
- for xstart, ystart, xstep, ystep in adam7:
- if xstart >= width:
- continue
- yield ((xstart, y, xstep) for y in range(ystart, height, ystep))
-
-
-# Models the 'pHYs' chunk (used by the Reader)
-Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter')
-
-
-def group(s, n):
- return list(zip(* [iter(s)] * n))
-
-
-def isarray(x):
- return isinstance(x, array)
-
-
-def check_palette(palette):
- """
- Check a palette argument (to the :class:`Writer` class) for validity.
- Returns the palette as a list if okay;
- raises an exception otherwise.
- """
-
- # None is the default and is allowed.
- if palette is None:
- return None
-
- p = list(palette)
- if not (0 < len(p) <= 256):
- raise ProtocolError(
- "a palette must have between 1 and 256 entries,"
- " see https://www.w3.org/TR/PNG/#11PLTE")
- seen_triple = False
- for i, t in enumerate(p):
- if len(t) not in (3, 4):
- raise ProtocolError(
- "palette entry %d: entries must be 3- or 4-tuples." % i)
- if len(t) == 3:
- seen_triple = True
- if seen_triple and len(t) == 4:
- raise ProtocolError(
- "palette entry %d: all 4-tuples must precede all 3-tuples" % i)
- for x in t:
- if int(x) != x or not(0 <= x <= 255):
- raise ProtocolError(
- "palette entry %d: "
- "values must be integer: 0 <= x <= 255" % i)
- return p
-
-
-def check_sizes(size, width, height):
- """
- Check that these arguments, if supplied, are consistent.
- Return a (width, height) pair.
- """
-
- if not size:
- return width, height
-
- if len(size) != 2:
- raise ProtocolError(
- "size argument should be a pair (width, height)")
- if width is not None and width != size[0]:
- raise ProtocolError(
- "size[0] (%r) and width (%r) should match when both are used."
- % (size[0], width))
- if height is not None and height != size[1]:
- raise ProtocolError(
- "size[1] (%r) and height (%r) should match when both are used."
- % (size[1], height))
- return size
-
-
-def check_color(c, greyscale, which):
- """
- Checks that a colour argument for transparent or background options
- is the right form.
- Returns the colour
- (which, if it's a bare integer, is "corrected" to a 1-tuple).
- """
-
- if c is None:
- return c
- if greyscale:
- try:
- len(c)
- except TypeError:
- c = (c,)
- if len(c) != 1:
- raise ProtocolError("%s for greyscale must be 1-tuple" % which)
- if not is_natural(c[0]):
- raise ProtocolError(
- "%s colour for greyscale must be integer" % which)
- else:
- if not (len(c) == 3 and
- is_natural(c[0]) and
- is_natural(c[1]) and
- is_natural(c[2])):
- raise ProtocolError(
- "%s colour must be a triple of integers" % which)
- return c
-
-
-class Error(Exception):
- def __str__(self):
- return self.__class__.__name__ + ': ' + ' '.join(self.args)
-
-
-class FormatError(Error):
- """
- Problem with input file format.
- In other words, PNG file does not conform to
- the specification in some way and is invalid.
- """
-
-
-class ProtocolError(Error):
- """
- Problem with the way the programming interface has been used,
- or the data presented to it.
- """
-
-
-class ChunkError(FormatError):
- pass
-
-
-class Default:
- """The default for the greyscale parameter."""
-
-
-class Writer:
- """
- PNG encoder in pure Python.
- """
-
- def __init__(self, width=None, height=None,
- size=None,
- greyscale=Default,
- alpha=False,
- bitdepth=8,
- palette=None,
- transparent=None,
- background=None,
- gamma=None,
- compression=None,
- interlace=False,
- planes=None,
- colormap=None,
- maxval=None,
- chunk_limit=2**20,
- x_pixels_per_unit=None,
- y_pixels_per_unit=None,
- unit_is_meter=False):
- """
- Create a PNG encoder object.
-
- Arguments:
-
- width, height
- Image size in pixels, as two separate arguments.
- size
- Image size (w,h) in pixels, as single argument.
- greyscale
- Pixels are greyscale, not RGB.
- alpha
- Input data has alpha channel (RGBA or LA).
- bitdepth
- Bit depth: from 1 to 16 (for each channel).
- palette
- Create a palette for a colour mapped image (colour type 3).
- transparent
- Specify a transparent colour (create a ``tRNS`` chunk).
- background
- Specify a default background colour (create a ``bKGD`` chunk).
- gamma
- Specify a gamma value (create a ``gAMA`` chunk).
- compression
- zlib compression level: 0 (none) to 9 (more compressed);
- default: -1 or None.
- interlace
- Create an interlaced image.
- chunk_limit
- Write multiple ``IDAT`` chunks to save memory.
- x_pixels_per_unit
- Number of pixels a unit along the x axis (write a
- `pHYs` chunk).
- y_pixels_per_unit
- Number of pixels a unit along the y axis (write a
- `pHYs` chunk). Along with `x_pixel_unit`, this gives
- the pixel size ratio.
- unit_is_meter
- `True` to indicate that the unit (for the `pHYs`
- chunk) is metre.
-
- The image size (in pixels) can be specified either by using the
- `width` and `height` arguments, or with the single `size`
- argument.
- If `size` is used it should be a pair (*width*, *height*).
-
- The `greyscale` argument indicates whether input pixels
- are greyscale (when true), or colour (when false).
- The default is true unless `palette=` is used.
-
- The `alpha` argument (a boolean) specifies
- whether input pixels have an alpha channel (or not).
-
- `bitdepth` specifies the bit depth of the source pixel values.
- Each channel may have a different bit depth.
- Each source pixel must have values that are
- an integer between 0 and ``2**bitdepth-1``, where
- `bitdepth` is the bit depth for the corresponding channel.
- For example, 8-bit images have values between 0 and 255.
- PNG only stores images with bit depths of
- 1,2,4,8, or 16 (the same for all channels).
- When `bitdepth` is not one of these values or where
- channels have different bit depths,
- the next highest valid bit depth is selected,
- and an ``sBIT`` (significant bits) chunk is generated
- that specifies the original precision of the source image.
- In this case the supplied pixel values will be rescaled to
- fit the range of the selected bit depth.
-
- The PNG file format supports many bit depth / colour model
- combinations, but not all.
- The details are somewhat arcane
- (refer to the PNG specification for full details).
- Briefly:
- Bit depths < 8 (1,2,4) are only allowed with greyscale and
- colour mapped images;
- colour mapped images cannot have bit depth 16.
-
- For colour mapped images
- (in other words, when the `palette` argument is specified)
- the `bitdepth` argument must match one of
- the valid PNG bit depths: 1, 2, 4, or 8.
- (It is valid to have a PNG image with a palette and
- an ``sBIT`` chunk, but the meaning is slightly different;
- it would be awkward to use the `bitdepth` argument for this.)
-
- The `palette` option, when specified,
- causes a colour mapped image to be created:
- the PNG colour type is set to 3;
- `greyscale` must not be true; `alpha` must not be true;
- `transparent` must not be set.
- The bit depth must be 1,2,4, or 8.
- When a colour mapped image is created,
- the pixel values are palette indexes and
- the `bitdepth` argument specifies the size of these indexes
- (not the size of the colour values in the palette).
-
- The palette argument value should be a sequence of 3- or
- 4-tuples.
- 3-tuples specify RGB palette entries;
- 4-tuples specify RGBA palette entries.
- All the 4-tuples (if present) must come before all the 3-tuples.
- A ``PLTE`` chunk is created;
- if there are 4-tuples then a ``tRNS`` chunk is created as well.
- The ``PLTE`` chunk will contain all the RGB triples in the same
- sequence;
- the ``tRNS`` chunk will contain the alpha channel for
- all the 4-tuples, in the same sequence.
- Palette entries are always 8-bit.
-
- If specified, the `transparent` and `background` parameters must be
- a tuple with one element for each channel in the image.
- Either a 3-tuple of integer (RGB) values for a colour image, or
- a 1-tuple of a single integer for a greyscale image.
-
- If specified, the `gamma` parameter must be a positive number
- (generally, a `float`).
- A ``gAMA`` chunk will be created.
- Note that this will not change the values of the pixels as
- they appear in the PNG file,
- they are assumed to have already
- been converted appropriately for the gamma specified.
-
- The `compression` argument specifies the compression level to
- be used by the ``zlib`` module.
- Values from 1 to 9 (highest) specify compression.
- 0 means no compression.
- -1 and ``None`` both mean that the ``zlib`` module uses
- the default level of compression (which is generally acceptable).
-
- If `interlace` is true then an interlaced image is created
- (using PNG's so far only interlace method, *Adam7*).
- This does not affect how the pixels should be passed in,
- rather it changes how they are arranged into the PNG file.
- On slow connexions interlaced images can be
- partially decoded by the browser to give
- a rough view of the image that is
- successively refined as more image data appears.
-
- .. note ::
-
- Enabling the `interlace` option requires the entire image
- to be processed in working memory.
-
- `chunk_limit` is used to limit the amount of memory used whilst
- compressing the image.
- In order to avoid using large amounts of memory,
- multiple ``IDAT`` chunks may be created.
- """
-
- # At the moment the `planes` argument is ignored;
- # its purpose is to act as a dummy so that
- # ``Writer(x, y, **info)`` works, where `info` is a dictionary
- # returned by Reader.read and friends.
- # Ditto for `colormap`.
-
- width, height = check_sizes(size, width, height)
- del size
-
- if not is_natural(width) or not is_natural(height):
- raise ProtocolError("width and height must be integers")
- if width <= 0 or height <= 0:
- raise ProtocolError("width and height must be greater than zero")
- # http://www.w3.org/TR/PNG/#7Integers-and-byte-order
- if width > 2 ** 31 - 1 or height > 2 ** 31 - 1:
- raise ProtocolError("width and height cannot exceed 2**31-1")
-
- if alpha and transparent is not None:
- raise ProtocolError(
- "transparent colour not allowed with alpha channel")
-
- # bitdepth is either single integer, or tuple of integers.
- # Convert to tuple.
- try:
- len(bitdepth)
- except TypeError:
- bitdepth = (bitdepth, )
- for b in bitdepth:
- valid = is_natural(b) and 1 <= b <= 16
- if not valid:
- raise ProtocolError(
- "each bitdepth %r must be a positive integer <= 16" %
- (bitdepth,))
-
- # Calculate channels, and
- # expand bitdepth to be one element per channel.
- palette = check_palette(palette)
- alpha = bool(alpha)
- colormap = bool(palette)
- if greyscale is Default and palette:
- greyscale = False
- greyscale = bool(greyscale)
- if colormap:
- color_planes = 1
- planes = 1
- else:
- color_planes = (3, 1)[greyscale]
- planes = color_planes + alpha
- if len(bitdepth) == 1:
- bitdepth *= planes
-
- bitdepth, self.rescale = check_bitdepth_rescale(
- palette,
- bitdepth,
- transparent, alpha, greyscale)
-
- # These are assertions, because above logic should have
- # corrected or raised all problematic cases.
- if bitdepth < 8:
- assert greyscale or palette
- assert not alpha
- if bitdepth > 8:
- assert not palette
-
- transparent = check_color(transparent, greyscale, 'transparent')
- background = check_color(background, greyscale, 'background')
-
- # It's important that the true boolean values
- # (greyscale, alpha, colormap, interlace) are converted
- # to bool because Iverson's convention is relied upon later on.
- self.width = width
- self.height = height
- self.transparent = transparent
- self.background = background
- self.gamma = gamma
- self.greyscale = greyscale
- self.alpha = alpha
- self.colormap = colormap
- self.bitdepth = int(bitdepth)
- self.compression = compression
- self.chunk_limit = chunk_limit
- self.interlace = bool(interlace)
- self.palette = palette
- self.x_pixels_per_unit = x_pixels_per_unit
- self.y_pixels_per_unit = y_pixels_per_unit
- self.unit_is_meter = bool(unit_is_meter)
-
- self.color_type = (4 * self.alpha +
- 2 * (not greyscale) +
- 1 * self.colormap)
- assert self.color_type in (0, 2, 3, 4, 6)
-
- self.color_planes = color_planes
- self.planes = planes
- # :todo: fix for bitdepth < 8
- self.psize = (self.bitdepth / 8) * self.planes
-
- def write(self, outfile, rows):
- """
- Write a PNG image to the output file.
- `rows` should be an iterable that yields each row
- (each row is a sequence of values).
- The rows should be the rows of the original image,
- so there should be ``self.height`` rows of
- ``self.width * self.planes`` values.
- If `interlace` is specified (when creating the instance),
- then an interlaced PNG file will be written.
- Supply the rows in the normal image order;
- the interlacing is carried out internally.
-
- .. note ::
-
- Interlacing requires the entire image to be in working memory.
- """
-
- # Values per row
- vpr = self.width * self.planes
-
- def check_rows(rows):
- """
- Yield each row in rows,
- but check each row first (for correct width).
- """
- for i, row in enumerate(rows):
- try:
- wrong_length = len(row) != vpr
- except TypeError:
- # When using an itertools.ichain object or
- # other generator not supporting __len__,
- # we set this to False to skip the check.
- wrong_length = False
- if wrong_length:
- # Note: row numbers start at 0.
- raise ProtocolError(
- "Expected %d values but got %d values, in row %d" %
- (vpr, len(row), i))
- yield row
-
- if self.interlace:
- fmt = 'BH'[self.bitdepth > 8]
- a = array(fmt, itertools.chain(*check_rows(rows)))
- return self.write_array(outfile, a)
-
- nrows = self.write_passes(outfile, check_rows(rows))
- if nrows != self.height:
- raise ProtocolError(
- "rows supplied (%d) does not match height (%d)" %
- (nrows, self.height))
- return nrows
-
- def write_passes(self, outfile, rows):
- """
- Write a PNG image to the output file.
-
- Most users are expected to find the :meth:`write` or
- :meth:`write_array` method more convenient.
-
- The rows should be given to this method in the order that
- they appear in the output file.
- For straightlaced images, this is the usual top to bottom ordering.
- For interlaced images the rows should have been interlaced before
- passing them to this function.
-
- `rows` should be an iterable that yields each row
- (each row being a sequence of values).
- """
-
- # Ensure rows are scaled (to 4-/8-/16-bit),
- # and packed into bytes.
-
- if self.rescale:
- rows = rescale_rows(rows, self.rescale)
-
- if self.bitdepth < 8:
- rows = pack_rows(rows, self.bitdepth)
- elif self.bitdepth == 16:
- rows = unpack_rows(rows)
-
- return self.write_packed(outfile, rows)
-
- def write_packed(self, outfile, rows):
- """
- Write PNG file to `outfile`.
- `rows` should be an iterator that yields each packed row;
- a packed row being a sequence of packed bytes.
-
- The rows have a filter byte prefixed and
- are then compressed into one or more IDAT chunks.
- They are not processed any further,
- so if bitdepth is other than 1, 2, 4, 8, 16,
- the pixel values should have been scaled
- before passing them to this method.
-
- This method does work for interlaced images but it is best avoided.
- For interlaced images, the rows should be
- presented in the order that they appear in the file.
- """
-
- self.write_preamble(outfile)
-
- # http://www.w3.org/TR/PNG/#11IDAT
- if self.compression is not None:
- compressor = zlib.compressobj(self.compression)
- else:
- compressor = zlib.compressobj()
-
- # data accumulates bytes to be compressed for the IDAT chunk;
- # it's compressed when sufficiently large.
- data = bytearray()
-
- # raise i scope out of the for loop. set to -1, because the for loop
- # sets i to 0 on the first pass
- i = -1
- for i, row in enumerate(rows):
- # Add "None" filter type.
- # Currently, it's essential that this filter type be used
- # for every scanline as
- # we do not mark the first row of a reduced pass image;
- # that means we could accidentally compute
- # the wrong filtered scanline if we used
- # "up", "average", or "paeth" on such a line.
- data.append(0)
- data.extend(row)
- if len(data) > self.chunk_limit:
- compressed = compressor.compress(data)
- if len(compressed):
- write_chunk(outfile, b'IDAT', compressed)
- data = bytearray()
-
- compressed = compressor.compress(bytes(data))
- flushed = compressor.flush()
- if len(compressed) or len(flushed):
- write_chunk(outfile, b'IDAT', compressed + flushed)
- # http://www.w3.org/TR/PNG/#11IEND
- write_chunk(outfile, b'IEND')
- return i + 1
-
- def write_preamble(self, outfile):
- # http://www.w3.org/TR/PNG/#5PNG-file-signature
- outfile.write(signature)
-
- # http://www.w3.org/TR/PNG/#11IHDR
- write_chunk(outfile, b'IHDR',
- struct.pack("!2I5B", self.width, self.height,
- self.bitdepth, self.color_type,
- 0, 0, self.interlace))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11gAMA
- if self.gamma is not None:
- write_chunk(outfile, b'gAMA',
- struct.pack("!L", int(round(self.gamma * 1e5))))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11sBIT
- if self.rescale:
- write_chunk(
- outfile, b'sBIT',
- struct.pack('%dB' % self.planes,
- * [s[0] for s in self.rescale]))
-
- # :chunk:order: Without a palette (PLTE chunk),
- # ordering is relatively relaxed.
- # With one, gAMA chunk must precede PLTE chunk
- # which must precede tRNS and bKGD.
- # See http://www.w3.org/TR/PNG/#5ChunkOrdering
- if self.palette:
- p, t = make_palette_chunks(self.palette)
- write_chunk(outfile, b'PLTE', p)
- if t:
- # tRNS chunk is optional;
- # Only needed if palette entries have alpha.
- write_chunk(outfile, b'tRNS', t)
-
- # http://www.w3.org/TR/PNG/#11tRNS
- if self.transparent is not None:
- if self.greyscale:
- fmt = "!1H"
- else:
- fmt = "!3H"
- write_chunk(outfile, b'tRNS',
- struct.pack(fmt, *self.transparent))
-
- # http://www.w3.org/TR/PNG/#11bKGD
- if self.background is not None:
- if self.greyscale:
- fmt = "!1H"
- else:
- fmt = "!3H"
- write_chunk(outfile, b'bKGD',
- struct.pack(fmt, *self.background))
-
- # http://www.w3.org/TR/PNG/#11pHYs
- if (self.x_pixels_per_unit is not None and
- self.y_pixels_per_unit is not None):
- tup = (self.x_pixels_per_unit,
- self.y_pixels_per_unit,
- int(self.unit_is_meter))
- write_chunk(outfile, b'pHYs', struct.pack("!LLB", *tup))
-
- def write_array(self, outfile, pixels):
- """
- Write an array that holds all the image values
- as a PNG file on the output file.
- See also :meth:`write` method.
- """
-
- if self.interlace:
- if type(pixels) != array:
- # Coerce to array type
- fmt = 'BH'[self.bitdepth > 8]
- pixels = array(fmt, pixels)
- return self.write_passes(
- outfile,
- self.array_scanlines_interlace(pixels)
- )
- else:
- return self.write_passes(
- outfile,
- self.array_scanlines(pixels)
- )
-
- def array_scanlines(self, pixels):
- """
- Generates rows (each a sequence of values) from
- a single array of values.
- """
-
- # Values per row
- vpr = self.width * self.planes
- stop = 0
- for y in range(self.height):
- start = stop
- stop = start + vpr
- yield pixels[start:stop]
-
- def array_scanlines_interlace(self, pixels):
- """
- Generator for interlaced scanlines from an array.
- `pixels` is the full source image as a single array of values.
- The generator yields each scanline of the reduced passes in turn,
- each scanline being a sequence of values.
- """
-
- # http://www.w3.org/TR/PNG/#8InterlaceMethods
- # Array type.
- fmt = 'BH'[self.bitdepth > 8]
- # Value per row
- vpr = self.width * self.planes
-
- # Each iteration generates a scanline starting at (x, y)
- # and consisting of every xstep pixels.
- for lines in adam7_generate(self.width, self.height):
- for x, y, xstep in lines:
- # Pixels per row (of reduced image)
- ppr = int(math.ceil((self.width - x) / float(xstep)))
- # Values per row (of reduced image)
- reduced_row_len = ppr * self.planes
- if xstep == 1:
- # Easy case: line is a simple slice.
- offset = y * vpr
- yield pixels[offset: offset + vpr]
- continue
- # We have to step by xstep,
- # which we can do one plane at a time
- # using the step in Python slices.
- row = array(fmt)
- # There's no easier way to set the length of an array
- row.extend(pixels[0:reduced_row_len])
- offset = y * vpr + x * self.planes
- end_offset = (y + 1) * vpr
- skip = self.planes * xstep
- for i in range(self.planes):
- row[i::self.planes] = \
- pixels[offset + i: end_offset: skip]
- yield row
-
-
-def write_chunk(outfile, tag, data=b''):
- """
- Write a PNG chunk to the output file, including length and
- checksum.
- """
-
- data = bytes(data)
- # http://www.w3.org/TR/PNG/#5Chunk-layout
- outfile.write(struct.pack("!I", len(data)))
- outfile.write(tag)
- outfile.write(data)
- checksum = zlib.crc32(tag)
- checksum = zlib.crc32(data, checksum)
- checksum &= 2 ** 32 - 1
- outfile.write(struct.pack("!I", checksum))
-
-
-def write_chunks(out, chunks):
- """Create a PNG file by writing out the chunks."""
-
- out.write(signature)
- for chunk in chunks:
- write_chunk(out, *chunk)
-
-
-def rescale_rows(rows, rescale):
- """
- Take each row in rows (an iterator) and yield
- a fresh row with the pixels scaled according to
- the rescale parameters in the list `rescale`.
- Each element of `rescale` is a tuple of
- (source_bitdepth, target_bitdepth),
- with one element per channel.
- """
-
- # One factor for each channel
- fs = [float(2 ** s[1] - 1)/float(2 ** s[0] - 1)
- for s in rescale]
-
- # Assume all target_bitdepths are the same
- target_bitdepths = set(s[1] for s in rescale)
- assert len(target_bitdepths) == 1
- (target_bitdepth, ) = target_bitdepths
- typecode = 'BH'[target_bitdepth > 8]
-
- # Number of channels
- n_chans = len(rescale)
-
- for row in rows:
- rescaled_row = array(typecode, iter(row))
- for i in range(n_chans):
- channel = array(
- typecode,
- (int(round(fs[i] * x)) for x in row[i::n_chans]))
- rescaled_row[i::n_chans] = channel
- yield rescaled_row
-
-
-def pack_rows(rows, bitdepth):
- """Yield packed rows that are a byte array.
- Each byte is packed with the values from several pixels.
- """
-
- assert bitdepth < 8
- assert 8 % bitdepth == 0
-
- # samples per byte
- spb = int(8 / bitdepth)
-
- def make_byte(block):
- """Take a block of (2, 4, or 8) values,
- and pack them into a single byte.
- """
-
- res = 0
- for v in block:
- res = (res << bitdepth) + v
- return res
-
- for row in rows:
- a = bytearray(row)
- # Adding padding bytes so we can group into a whole
- # number of spb-tuples.
- n = float(len(a))
- extra = math.ceil(n / spb) * spb - n
- a.extend([0] * int(extra))
- # Pack into bytes.
- # Each block is the samples for one byte.
- blocks = group(a, spb)
- yield bytearray(make_byte(block) for block in blocks)
-
-
-def unpack_rows(rows):
- """Unpack each row from being 16-bits per value,
- to being a sequence of bytes.
- """
- for row in rows:
- fmt = '!%dH' % len(row)
- yield bytearray(struct.pack(fmt, *row))
-
-
-def make_palette_chunks(palette):
- """
- Create the byte sequences for a ``PLTE`` and
- if necessary a ``tRNS`` chunk.
- Returned as a pair (*p*, *t*).
- *t* will be ``None`` if no ``tRNS`` chunk is necessary.
- """
-
- p = bytearray()
- t = bytearray()
-
- for x in palette:
- p.extend(x[0:3])
- if len(x) > 3:
- t.append(x[3])
- if t:
- return p, t
- return p, None
-
-
-def check_bitdepth_rescale(
- palette, bitdepth, transparent, alpha, greyscale):
- """
- Returns (bitdepth, rescale) pair.
- """
-
- if palette:
- if len(bitdepth) != 1:
- raise ProtocolError(
- "with palette, only a single bitdepth may be used")
- (bitdepth, ) = bitdepth
- if bitdepth not in (1, 2, 4, 8):
- raise ProtocolError(
- "with palette, bitdepth must be 1, 2, 4, or 8")
- if transparent is not None:
- raise ProtocolError("transparent and palette not compatible")
- if alpha:
- raise ProtocolError("alpha and palette not compatible")
- if greyscale:
- raise ProtocolError("greyscale and palette not compatible")
- return bitdepth, None
-
- # No palette, check for sBIT chunk generation.
-
- if greyscale and not alpha:
- # Single channel, L.
- (bitdepth,) = bitdepth
- if bitdepth in (1, 2, 4, 8, 16):
- return bitdepth, None
- if bitdepth > 8:
- targetbitdepth = 16
- elif bitdepth == 3:
- targetbitdepth = 4
- else:
- assert bitdepth in (5, 6, 7)
- targetbitdepth = 8
- return targetbitdepth, [(bitdepth, targetbitdepth)]
-
- assert alpha or not greyscale
-
- depth_set = tuple(set(bitdepth))
- if depth_set in [(8,), (16,)]:
- # No sBIT required.
- (bitdepth, ) = depth_set
- return bitdepth, None
-
- targetbitdepth = (8, 16)[max(bitdepth) > 8]
- return targetbitdepth, [(b, targetbitdepth) for b in bitdepth]
-
-
-# Regex for decoding mode string
-RegexModeDecode = re.compile("(LA?|RGBA?);?([0-9]*)", flags=re.IGNORECASE)
-
-
-def from_array(a, mode=None, info={}):
- """
- Create a PNG :class:`Image` object from a 2-dimensional array.
- One application of this function is easy PIL-style saving:
- ``png.from_array(pixels, 'L').save('foo.png')``.
-
- Unless they are specified using the *info* parameter,
- the PNG's height and width are taken from the array size.
- The first axis is the height; the second axis is the
- ravelled width and channel index.
- The array is treated is a sequence of rows,
- each row being a sequence of values (``width*channels`` in number).
- So an RGB image that is 16 pixels high and 8 wide will
- occupy a 2-dimensional array that is 16x24
- (each row will be 8*3 = 24 sample values).
-
- *mode* is a string that specifies the image colour format in a
- PIL-style mode. It can be:
-
- ``'L'``
- greyscale (1 channel)
- ``'LA'``
- greyscale with alpha (2 channel)
- ``'RGB'``
- colour image (3 channel)
- ``'RGBA'``
- colour image with alpha (4 channel)
-
- The mode string can also specify the bit depth
- (overriding how this function normally derives the bit depth,
- see below).
- Appending ``';16'`` to the mode will cause the PNG to be
- 16 bits per channel;
- any decimal from 1 to 16 can be used to specify the bit depth.
-
- When a 2-dimensional array is used *mode* determines how many
- channels the image has, and so allows the width to be derived from
- the second array dimension.
-
- The array is expected to be a ``numpy`` array,
- but it can be any suitable Python sequence.
- For example, a list of lists can be used:
- ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``.
- The exact rules are: ``len(a)`` gives the first dimension, height;
- ``len(a[0])`` gives the second dimension.
- It's slightly more complicated than that because
- an iterator of rows can be used, and it all still works.
- Using an iterator allows data to be streamed efficiently.
-
- The bit depth of the PNG is normally taken from
- the array element's datatype
- (but if *mode* specifies a bitdepth then that is used instead).
- The array element's datatype is determined in a way which
- is supposed to work both for ``numpy`` arrays and for Python
- ``array.array`` objects.
- A 1 byte datatype will give a bit depth of 8,
- a 2 byte datatype will give a bit depth of 16.
- If the datatype does not have an implicit size,
- like the above example where it is a plain Python list of lists,
- then a default of 8 is used.
-
- The *info* parameter is a dictionary that can
- be used to specify metadata (in the same style as
- the arguments to the :class:`png.Writer` class).
- For this function the keys that are useful are:
-
- height
- overrides the height derived from the array dimensions and
- allows *a* to be an iterable.
- width
- overrides the width derived from the array dimensions.
- bitdepth
- overrides the bit depth derived from the element datatype
- (but must match *mode* if that also specifies a bit depth).
-
- Generally anything specified in the *info* dictionary will
- override any implicit choices that this function would otherwise make,
- but must match any explicit ones.
- For example, if the *info* dictionary has a ``greyscale`` key then
- this must be true when mode is ``'L'`` or ``'LA'`` and
- false when mode is ``'RGB'`` or ``'RGBA'``.
- """
-
- # We abuse the *info* parameter by modifying it. Take a copy here.
- # (Also typechecks *info* to some extent).
- info = dict(info)
-
- # Syntax check mode string.
- match = RegexModeDecode.match(mode)
- if not match:
- raise Error("mode string should be 'RGB' or 'L;16' or similar.")
-
- mode, bitdepth = match.groups()
- if bitdepth:
- bitdepth = int(bitdepth)
-
- # Colour format.
- if 'greyscale' in info:
- if bool(info['greyscale']) != ('L' in mode):
- raise ProtocolError("info['greyscale'] should match mode.")
- info['greyscale'] = 'L' in mode
-
- alpha = 'A' in mode
- if 'alpha' in info:
- if bool(info['alpha']) != alpha:
- raise ProtocolError("info['alpha'] should match mode.")
- info['alpha'] = alpha
-
- # Get bitdepth from *mode* if possible.
- if bitdepth:
- if info.get("bitdepth") and bitdepth != info['bitdepth']:
- raise ProtocolError(
- "bitdepth (%d) should match bitdepth of info (%d)." %
- (bitdepth, info['bitdepth']))
- info['bitdepth'] = bitdepth
-
- # Fill in and/or check entries in *info*.
- # Dimensions.
- width, height = check_sizes(
- info.get("size"),
- info.get("width"),
- info.get("height"))
- if width:
- info["width"] = width
- if height:
- info["height"] = height
-
- if "height" not in info:
- try:
- info['height'] = len(a)
- except TypeError:
- raise ProtocolError(
- "len(a) does not work, supply info['height'] instead.")
-
- planes = len(mode)
- if 'planes' in info:
- if info['planes'] != planes:
- raise Error("info['planes'] should match mode.")
-
- # In order to work out whether we the array is 2D or 3D we need its
- # first row, which requires that we take a copy of its iterator.
- # We may also need the first row to derive width and bitdepth.
- a, t = itertools.tee(a)
- row = next(t)
- del t
-
- testelement = row
- if 'width' not in info:
- width = len(row) // planes
- info['width'] = width
-
- if 'bitdepth' not in info:
- try:
- dtype = testelement.dtype
- # goto the "else:" clause. Sorry.
- except AttributeError:
- try:
- # Try a Python array.array.
- bitdepth = 8 * testelement.itemsize
- except AttributeError:
- # We can't determine it from the array element's datatype,
- # use a default of 8.
- bitdepth = 8
- else:
- # If we got here without exception,
- # we now assume that the array is a numpy array.
- if dtype.kind == 'b':
- bitdepth = 1
- else:
- bitdepth = 8 * dtype.itemsize
- info['bitdepth'] = bitdepth
-
- for thing in ["width", "height", "bitdepth", "greyscale", "alpha"]:
- assert thing in info
-
- return Image(a, info)
-
-
-# So that refugee's from PIL feel more at home. Not documented.
-fromarray = from_array
-
-
-class Image:
- """A PNG image. You can create an :class:`Image` object from
- an array of pixels by calling :meth:`png.from_array`. It can be
- saved to disk with the :meth:`save` method.
- """
-
- def __init__(self, rows, info):
- """
- .. note ::
-
- The constructor is not public. Please do not call it.
- """
-
- self.rows = rows
- self.info = info
-
- def save(self, file):
- """Save the image to the named *file*.
-
- See `.write()` if you already have an open file object.
-
- In general, you can only call this method once;
- after it has been called the first time the PNG image is written,
- the source data will have been streamed, and
- cannot be streamed again.
- """
-
- w = Writer(**self.info)
-
- with open(file, 'wb') as fd:
- w.write(fd, self.rows)
-
- def write(self, file):
- """Write the image to the open file object.
-
- See `.save()` if you have a filename.
-
- In general, you can only call this method once;
- after it has been called the first time the PNG image is written,
- the source data will have been streamed, and
- cannot be streamed again.
- """
-
- w = Writer(**self.info)
- w.write(file, self.rows)
-
-
-class Reader:
- """
- Pure Python PNG decoder in pure Python.
- """
-
- def __init__(self, _guess=None, filename=None, file=None, bytes=None):
- """
- The constructor expects exactly one keyword argument.
- If you supply a positional argument instead,
- it will guess the input type.
- Choose from the following keyword arguments:
-
- filename
- Name of input file (a PNG file).
- file
- A file-like object (object with a read() method).
- bytes
- ``bytes`` or ``bytearray`` with PNG data.
-
- """
- keywords_supplied = (
- (_guess is not None) +
- (filename is not None) +
- (file is not None) +
- (bytes is not None))
- if keywords_supplied != 1:
- raise TypeError("Reader() takes exactly 1 argument")
-
- # Will be the first 8 bytes, later on. See validate_signature.
- self.signature = None
- self.transparent = None
- # A pair of (len,type) if a chunk has been read but its data and
- # checksum have not (in other words the file position is just
- # past the 4 bytes that specify the chunk type).
- # See preamble method for how this is used.
- self.atchunk = None
-
- if _guess is not None:
- if isarray(_guess):
- bytes = _guess
- elif isinstance(_guess, str):
- filename = _guess
- elif hasattr(_guess, 'read'):
- file = _guess
-
- if bytes is not None:
- self.file = io.BytesIO(bytes)
- elif filename is not None:
- self.file = open(filename, "rb")
- elif file is not None:
- self.file = file
- else:
- raise ProtocolError("expecting filename, file or bytes array")
-
- def chunk(self, lenient=False):
- """
- Read the next PNG chunk from the input file;
- returns a (*type*, *data*) tuple.
- *type* is the chunk's type as a byte string
- (all PNG chunk types are 4 bytes long).
- *data* is the chunk's data content, as a byte string.
-
- If the optional `lenient` argument evaluates to `True`,
- checksum failures will raise warnings rather than exceptions.
- """
-
- self.validate_signature()
-
- # http://www.w3.org/TR/PNG/#5Chunk-layout
- if not self.atchunk:
- self.atchunk = self._chunk_len_type()
- if not self.atchunk:
- raise ChunkError("No more chunks.")
- length, type = self.atchunk
- self.atchunk = None
-
- data = self.file.read(length)
- if len(data) != length:
- raise ChunkError(
- 'Chunk %s too short for required %i octets.'
- % (type, length))
- checksum = self.file.read(4)
- if len(checksum) != 4:
- raise ChunkError('Chunk %s too short for checksum.' % type)
- verify = zlib.crc32(type)
- verify = zlib.crc32(data, verify)
- verify = struct.pack('!I', verify)
- if checksum != verify:
- (a, ) = struct.unpack('!I', checksum)
- (b, ) = struct.unpack('!I', verify)
- message = ("Checksum error in %s chunk: 0x%08X != 0x%08X."
- % (type.decode('ascii'), a, b))
- if lenient:
- warnings.warn(message, RuntimeWarning)
- else:
- raise ChunkError(message)
- return type, data
-
- def chunks(self):
- """Return an iterator that will yield each chunk as a
- (*chunktype*, *content*) pair.
- """
-
- while True:
- t, v = self.chunk()
- yield t, v
- if t == b'IEND':
- break
-
- def undo_filter(self, filter_type, scanline, previous):
- """
- Undo the filter for a scanline.
- `scanline` is a sequence of bytes that
- does not include the initial filter type byte.
- `previous` is decoded previous scanline
- (for straightlaced images this is the previous pixel row,
- but for interlaced images, it is
- the previous scanline in the reduced image,
- which in general is not the previous pixel row in the final image).
- When there is no previous scanline
- (the first row of a straightlaced image,
- or the first row in one of the passes in an interlaced image),
- then this argument should be ``None``.
-
- The scanline will have the effects of filtering removed;
- the result will be returned as a fresh sequence of bytes.
- """
-
- # :todo: Would it be better to update scanline in place?
- result = scanline
-
- if filter_type == 0:
- return result
-
- if filter_type not in (1, 2, 3, 4):
- raise FormatError(
- 'Invalid PNG Filter Type. '
- 'See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .')
-
- # Filter unit. The stride from one pixel to the corresponding
- # byte from the previous pixel. Normally this is the pixel
- # size in bytes, but when this is smaller than 1, the previous
- # byte is used instead.
- fu = max(1, self.psize)
-
- # For the first line of a pass, synthesize a dummy previous
- # line. An alternative approach would be to observe that on the
- # first line 'up' is the same as 'null', 'paeth' is the same
- # as 'sub', with only 'average' requiring any special case.
- if not previous:
- previous = bytearray([0] * len(scanline))
-
- # Call appropriate filter algorithm. Note that 0 has already
- # been dealt with.
- fn = (None,
- undo_filter_sub,
- undo_filter_up,
- undo_filter_average,
- undo_filter_paeth)[filter_type]
- fn(fu, scanline, previous, result)
- return result
-
- def _deinterlace(self, raw):
- """
- Read raw pixel data, undo filters, deinterlace, and flatten.
- Return a single array of values.
- """
-
- # Values per row (of the target image)
- vpr = self.width * self.planes
-
- # Values per image
- vpi = vpr * self.height
- # Interleaving writes to the output array randomly
- # (well, not quite), so the entire output array must be in memory.
- # Make a result array, and make it big enough.
- if self.bitdepth > 8:
- a = array('H', [0] * vpi)
- else:
- a = bytearray([0] * vpi)
- source_offset = 0
-
- for lines in adam7_generate(self.width, self.height):
- # The previous (reconstructed) scanline.
- # `None` at the beginning of a pass
- # to indicate that there is no previous line.
- recon = None
- for x, y, xstep in lines:
- # Pixels per row (reduced pass image)
- ppr = int(math.ceil((self.width - x) / float(xstep)))
- # Row size in bytes for this pass.
- row_size = int(math.ceil(self.psize * ppr))
-
- filter_type = raw[source_offset]
- source_offset += 1
- scanline = raw[source_offset: source_offset + row_size]
- source_offset += row_size
- recon = self.undo_filter(filter_type, scanline, recon)
- # Convert so that there is one element per pixel value
- flat = self._bytes_to_values(recon, width=ppr)
- if xstep == 1:
- assert x == 0
- offset = y * vpr
- a[offset: offset + vpr] = flat
- else:
- offset = y * vpr + x * self.planes
- end_offset = (y + 1) * vpr
- skip = self.planes * xstep
- for i in range(self.planes):
- a[offset + i: end_offset: skip] = \
- flat[i:: self.planes]
-
- return a
-
- def _iter_bytes_to_values(self, byte_rows):
- """
- Iterator that yields each scanline;
- each scanline being a sequence of values.
- `byte_rows` should be an iterator that yields
- the bytes of each row in turn.
- """
-
- for row in byte_rows:
- yield self._bytes_to_values(row)
-
- def _bytes_to_values(self, bs, width=None):
- """Convert a packed row of bytes into a row of values.
- Result will be a freshly allocated object,
- not shared with the argument.
- """
-
- if self.bitdepth == 8:
- return bytearray(bs)
- if self.bitdepth == 16:
- return array('H',
- struct.unpack('!%dH' % (len(bs) // 2), bs))
-
- assert self.bitdepth < 8
- if width is None:
- width = self.width
- # Samples per byte
- spb = 8 // self.bitdepth
- out = bytearray()
- mask = 2**self.bitdepth - 1
- shifts = [self.bitdepth * i
- for i in reversed(list(range(spb)))]
- for o in bs:
- out.extend([mask & (o >> i) for i in shifts])
- return out[:width]
-
- def _iter_straight_packed(self, byte_blocks):
- """Iterator that undoes the effect of filtering;
- yields each row as a sequence of packed bytes.
- Assumes input is straightlaced.
- `byte_blocks` should be an iterable that yields the raw bytes
- in blocks of arbitrary size.
- """
-
- # length of row, in bytes
- rb = self.row_bytes
- a = bytearray()
- # The previous (reconstructed) scanline.
- # None indicates first line of image.
- recon = None
- for some_bytes in byte_blocks:
- a.extend(some_bytes)
- while len(a) >= rb + 1:
- filter_type = a[0]
- scanline = a[1: rb + 1]
- del a[: rb + 1]
- recon = self.undo_filter(filter_type, scanline, recon)
- yield recon
- if len(a) != 0:
- # :file:format We get here with a file format error:
- # when the available bytes (after decompressing) do not
- # pack into exact rows.
- raise FormatError('Wrong size for decompressed IDAT chunk.')
- assert len(a) == 0
-
- def validate_signature(self):
- """
- If signature (header) has not been read then read and
- validate it; otherwise do nothing.
- No signature (empty read()) will raise EOFError;
- An invalid signature will raise FormatError.
- EOFError is raised to make possible the case where
- a program can read multiple PNG files from the same stream.
- The end of the stream can be distinguished from non-PNG files
- or corrupted PNG files.
- """
-
- if self.signature:
- return
- self.signature = self.file.read(8)
- if len(self.signature) == 0:
- raise EOFError("End of PNG stream.")
- if self.signature != signature:
- raise FormatError("PNG file has invalid signature.")
-
- def preamble(self, lenient=False):
- """
- Extract the image metadata by reading
- the initial part of the PNG file up to
- the start of the ``IDAT`` chunk.
- All the chunks that precede the ``IDAT`` chunk are
- read and either processed for metadata or discarded.
-
- If the optional `lenient` argument evaluates to `True`,
- checksum failures will raise warnings rather than exceptions.
- """
-
- self.validate_signature()
-
- while True:
- if not self.atchunk:
- self.atchunk = self._chunk_len_type()
- if self.atchunk is None:
- raise FormatError('This PNG file has no IDAT chunks.')
- if self.atchunk[1] == b'IDAT':
- return
- self.process_chunk(lenient=lenient)
-
- def _chunk_len_type(self):
- """
- Reads just enough of the input to
- determine the next chunk's length and type;
- return a (*length*, *type*) pair where *type* is a byte sequence.
- If there are no more chunks, ``None`` is returned.
- """
-
- x = self.file.read(8)
- if not x:
- return None
- if len(x) != 8:
- raise FormatError(
- 'End of file whilst reading chunk length and type.')
- length, type = struct.unpack('!I4s', x)
- if length > 2 ** 31 - 1:
- raise FormatError('Chunk %s is too large: %d.' % (type, length))
- # Check that all bytes are in valid ASCII range.
- # https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout
- type_bytes = set(bytearray(type))
- if not(type_bytes <= set(range(65, 91)) | set(range(97, 123))):
- raise FormatError(
- 'Chunk %r has invalid Chunk Type.'
- % list(type))
- return length, type
-
- def process_chunk(self, lenient=False):
- """
- Process the next chunk and its data.
- This only processes the following chunk types:
- ``IHDR``, ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``.
- All other chunk types are ignored.
-
- If the optional `lenient` argument evaluates to `True`,
- checksum failures will raise warnings rather than exceptions.
- """
-
- type, data = self.chunk(lenient=lenient)
- method = '_process_' + type.decode('ascii')
- m = getattr(self, method, None)
- if m:
- m(data)
-
- def _process_IHDR(self, data):
- # http://www.w3.org/TR/PNG/#11IHDR
- if len(data) != 13:
- raise FormatError('IHDR chunk has incorrect length.')
- (self.width, self.height, self.bitdepth, self.color_type,
- self.compression, self.filter,
- self.interlace) = struct.unpack("!2I5B", data)
-
- check_bitdepth_colortype(self.bitdepth, self.color_type)
-
- if self.compression != 0:
- raise FormatError(
- "Unknown compression method %d" % self.compression)
- if self.filter != 0:
- raise FormatError(
- "Unknown filter method %d,"
- " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
- % self.filter)
- if self.interlace not in (0, 1):
- raise FormatError(
- "Unknown interlace method %d, see "
- "http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods"
- " ."
- % self.interlace)
-
- # Derived values
- # http://www.w3.org/TR/PNG/#6Colour-values
- colormap = bool(self.color_type & 1)
- greyscale = not(self.color_type & 2)
- alpha = bool(self.color_type & 4)
- color_planes = (3, 1)[greyscale or colormap]
- planes = color_planes + alpha
-
- self.colormap = colormap
- self.greyscale = greyscale
- self.alpha = alpha
- self.color_planes = color_planes
- self.planes = planes
- self.psize = float(self.bitdepth) / float(8) * planes
- if int(self.psize) == self.psize:
- self.psize = int(self.psize)
- self.row_bytes = int(math.ceil(self.width * self.psize))
- # Stores PLTE chunk if present, and is used to check
- # chunk ordering constraints.
- self.plte = None
- # Stores tRNS chunk if present, and is used to check chunk
- # ordering constraints.
- self.trns = None
- # Stores sBIT chunk if present.
- self.sbit = None
-
- def _process_PLTE(self, data):
- # http://www.w3.org/TR/PNG/#11PLTE
- if self.plte:
- warnings.warn("Multiple PLTE chunks present.")
- self.plte = data
- if len(data) % 3 != 0:
- raise FormatError(
- "PLTE chunk's length should be a multiple of 3.")
- if len(data) > (2 ** self.bitdepth) * 3:
- raise FormatError("PLTE chunk is too long.")
- if len(data) == 0:
- raise FormatError("Empty PLTE is not allowed.")
-
- def _process_bKGD(self, data):
- try:
- if self.colormap:
- if not self.plte:
- warnings.warn(
- "PLTE chunk is required before bKGD chunk.")
- self.background = struct.unpack('B', data)
- else:
- self.background = struct.unpack("!%dH" % self.color_planes,
- data)
- except struct.error:
- raise FormatError("bKGD chunk has incorrect length.")
-
- def _process_tRNS(self, data):
- # http://www.w3.org/TR/PNG/#11tRNS
- self.trns = data
- if self.colormap:
- if not self.plte:
- warnings.warn("PLTE chunk is required before tRNS chunk.")
- else:
- if len(data) > len(self.plte) / 3:
- # Was warning, but promoted to Error as it
- # would otherwise cause pain later on.
- raise FormatError("tRNS chunk is too long.")
- else:
- if self.alpha:
- raise FormatError(
- "tRNS chunk is not valid with colour type %d." %
- self.color_type)
- try:
- self.transparent = \
- struct.unpack("!%dH" % self.color_planes, data)
- except struct.error:
- raise FormatError("tRNS chunk has incorrect length.")
-
- def _process_gAMA(self, data):
- try:
- self.gamma = struct.unpack("!L", data)[0] / 100000.0
- except struct.error:
- raise FormatError("gAMA chunk has incorrect length.")
-
- def _process_sBIT(self, data):
- self.sbit = data
- if (self.colormap and len(data) != 3 or
- not self.colormap and len(data) != self.planes):
- raise FormatError("sBIT chunk has incorrect length.")
-
- def _process_pHYs(self, data):
- # http://www.w3.org/TR/PNG/#11pHYs
- self.phys = data
- fmt = "!LLB"
- if len(data) != struct.calcsize(fmt):
- raise FormatError("pHYs chunk has incorrect length.")
- self.x_pixels_per_unit, self.y_pixels_per_unit, unit = \
- struct.unpack(fmt, data)
- self.unit_is_meter = bool(unit)
-
- def read(self, lenient=False):
- """
- Read the PNG file and decode it.
- Returns (`width`, `height`, `rows`, `info`).
-
- May use excessive memory.
-
- `rows` is a sequence of rows;
- each row is a sequence of values.
-
- If the optional `lenient` argument evaluates to True,
- checksum failures will raise warnings rather than exceptions.
- """
-
- def iteridat():
- """Iterator that yields all the ``IDAT`` chunks as strings."""
- while True:
- type, data = self.chunk(lenient=lenient)
- if type == b'IEND':
- # http://www.w3.org/TR/PNG/#11IEND
- break
- if type != b'IDAT':
- continue
- # type == b'IDAT'
- # http://www.w3.org/TR/PNG/#11IDAT
- if self.colormap and not self.plte:
- warnings.warn("PLTE chunk is required before IDAT chunk")
- yield data
-
- self.preamble(lenient=lenient)
- raw = decompress(iteridat())
-
- if self.interlace:
- def rows_from_interlace():
- """Yield each row from an interlaced PNG."""
- # It's important that this iterator doesn't read
- # IDAT chunks until it yields the first row.
- bs = bytearray(itertools.chain(*raw))
- arraycode = 'BH'[self.bitdepth > 8]
- # Like :meth:`group` but
- # producing an array.array object for each row.
- values = self._deinterlace(bs)
- vpr = self.width * self.planes
- for i in range(0, len(values), vpr):
- row = array(arraycode, values[i:i+vpr])
- yield row
- rows = rows_from_interlace()
- else:
- rows = self._iter_bytes_to_values(self._iter_straight_packed(raw))
- info = dict()
- for attr in 'greyscale alpha planes bitdepth interlace'.split():
- info[attr] = getattr(self, attr)
- info['size'] = (self.width, self.height)
- for attr in 'gamma transparent background'.split():
- a = getattr(self, attr, None)
- if a is not None:
- info[attr] = a
- if getattr(self, 'x_pixels_per_unit', None):
- info['physical'] = Resolution(self.x_pixels_per_unit,
- self.y_pixels_per_unit,
- self.unit_is_meter)
- if self.plte:
- info['palette'] = self.palette()
- return self.width, self.height, rows, info
-
- def read_flat(self):
- """
- Read a PNG file and decode it into a single array of values.
- Returns (*width*, *height*, *values*, *info*).
-
- May use excessive memory.
-
- `values` is a single array.
-
- The :meth:`read` method is more stream-friendly than this,
- because it returns a sequence of rows.
- """
-
- x, y, pixel, info = self.read()
- arraycode = 'BH'[info['bitdepth'] > 8]
- pixel = array(arraycode, itertools.chain(*pixel))
- return x, y, pixel, info
-
- def palette(self, alpha='natural'):
- """
- Returns a palette that is a sequence of 3-tuples or 4-tuples,
- synthesizing it from the ``PLTE`` and ``tRNS`` chunks.
- These chunks should have already been processed (for example,
- by calling the :meth:`preamble` method).
- All the tuples are the same size:
- 3-tuples if there is no ``tRNS`` chunk,
- 4-tuples when there is a ``tRNS`` chunk.
-
- Assumes that the image is colour type
- 3 and therefore a ``PLTE`` chunk is required.
-
- If the `alpha` argument is ``'force'`` then an alpha channel is
- always added, forcing the result to be a sequence of 4-tuples.
- """
-
- if not self.plte:
- raise FormatError(
- "Required PLTE chunk is missing in colour type 3 image.")
- plte = group(array('B', self.plte), 3)
- if self.trns or alpha == 'force':
- trns = array('B', self.trns or [])
- trns.extend([255] * (len(plte) - len(trns)))
- plte = list(map(operator.add, plte, group(trns, 1)))
- return plte
-
- def asDirect(self):
- """
- Returns the image data as a direct representation of
- an ``x * y * planes`` array.
- This removes the need for callers to deal with
- palettes and transparency themselves.
- Images with a palette (colour type 3) are converted to RGB or RGBA;
- images with transparency (a ``tRNS`` chunk) are converted to
- LA or RGBA as appropriate.
- When returned in this format the pixel values represent
- the colour value directly without needing to refer
- to palettes or transparency information.
-
- Like the :meth:`read` method this method returns a 4-tuple:
-
- (*width*, *height*, *rows*, *info*)
-
- This method normally returns pixel values with
- the bit depth they have in the source image, but
- when the source PNG has an ``sBIT`` chunk it is inspected and
- can reduce the bit depth of the result pixels;
- pixel values will be reduced according to the bit depth
- specified in the ``sBIT`` chunk.
- PNG nerds should note a single result bit depth is
- used for all channels:
- the maximum of the ones specified in the ``sBIT`` chunk.
- An RGB565 image will be rescaled to 6-bit RGB666.
-
- The *info* dictionary that is returned reflects
- the `direct` format and not the original source image.
- For example, an RGB source image with a ``tRNS`` chunk
- to represent a transparent colour,
- will start with ``planes=3`` and ``alpha=False`` for the
- source image,
- but the *info* dictionary returned by this method
- will have ``planes=4`` and ``alpha=True`` because
- an alpha channel is synthesized and added.
-
- *rows* is a sequence of rows;
- each row being a sequence of values
- (like the :meth:`read` method).
-
- All the other aspects of the image data are not changed.
- """
-
- self.preamble()
-
- # Simple case, no conversion necessary.
- if not self.colormap and not self.trns and not self.sbit:
- return self.read()
-
- x, y, pixels, info = self.read()
-
- if self.colormap:
- info['colormap'] = False
- info['alpha'] = bool(self.trns)
- info['bitdepth'] = 8
- info['planes'] = 3 + bool(self.trns)
- plte = self.palette()
-
- def iterpal(pixels):
- for row in pixels:
- row = [plte[x] for x in row]
- yield array('B', itertools.chain(*row))
- pixels = iterpal(pixels)
- elif self.trns:
- # It would be nice if there was some reasonable way
- # of doing this without generating a whole load of
- # intermediate tuples. But tuples does seem like the
- # easiest way, with no other way clearly much simpler or
- # much faster. (Actually, the L to LA conversion could
- # perhaps go faster (all those 1-tuples!), but I still
- # wonder whether the code proliferation is worth it)
- it = self.transparent
- maxval = 2 ** info['bitdepth'] - 1
- planes = info['planes']
- info['alpha'] = True
- info['planes'] += 1
- typecode = 'BH'[info['bitdepth'] > 8]
-
- def itertrns(pixels):
- for row in pixels:
- # For each row we group it into pixels, then form a
- # characterisation vector that says whether each
- # pixel is opaque or not. Then we convert
- # True/False to 0/maxval (by multiplication),
- # and add it as the extra channel.
- row = group(row, planes)
- opa = map(it.__ne__, row)
- opa = map(maxval.__mul__, opa)
- opa = list(zip(opa)) # convert to 1-tuples
- yield array(
- typecode,
- itertools.chain(*map(operator.add, row, opa)))
- pixels = itertrns(pixels)
- targetbitdepth = None
- if self.sbit:
- sbit = struct.unpack('%dB' % len(self.sbit), self.sbit)
- targetbitdepth = max(sbit)
- if targetbitdepth > info['bitdepth']:
- raise Error('sBIT chunk %r exceeds bitdepth %d' %
- (sbit, self.bitdepth))
- if min(sbit) <= 0:
- raise Error('sBIT chunk %r has a 0-entry' % sbit)
- if targetbitdepth:
- shift = info['bitdepth'] - targetbitdepth
- info['bitdepth'] = targetbitdepth
-
- def itershift(pixels):
- for row in pixels:
- yield [p >> shift for p in row]
- pixels = itershift(pixels)
- return x, y, pixels, info
-
- def _as_rescale(self, get, targetbitdepth):
- """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`."""
-
- width, height, pixels, info = get()
- maxval = 2**info['bitdepth'] - 1
- targetmaxval = 2**targetbitdepth - 1
- factor = float(targetmaxval) / float(maxval)
- info['bitdepth'] = targetbitdepth
-
- def iterscale():
- for row in pixels:
- yield [int(round(x * factor)) for x in row]
- if maxval == targetmaxval:
- return width, height, pixels, info
- else:
- return width, height, iterscale(), info
-
- def asRGB8(self):
- """
- Return the image data as an RGB pixels with 8-bits per sample.
- This is like the :meth:`asRGB` method except that
- this method additionally rescales the values so that
- they are all between 0 and 255 (8-bit).
- In the case where the source image has a bit depth < 8
- the transformation preserves all the information;
- where the source image has bit depth > 8, then
- rescaling to 8-bit values loses precision.
- No dithering is performed.
- Like :meth:`asRGB`,
- an alpha channel in the source image will raise an exception.
-
- This function returns a 4-tuple:
- (*width*, *height*, *rows*, *info*).
- *width*, *height*, *info* are as per the :meth:`read` method.
-
- *rows* is the pixel data as a sequence of rows.
- """
-
- return self._as_rescale(self.asRGB, 8)
-
- def asRGBA8(self):
- """
- Return the image data as RGBA pixels with 8-bits per sample.
- This method is similar to :meth:`asRGB8` and :meth:`asRGBA`:
- The result pixels have an alpha channel, *and*
- values are rescaled to the range 0 to 255.
- The alpha channel is synthesized if necessary
- (with a small speed penalty).
- """
-
- return self._as_rescale(self.asRGBA, 8)
-
- def asRGB(self):
- """
- Return image as RGB pixels.
- RGB colour images are passed through unchanged;
- greyscales are expanded into RGB triplets
- (there is a small speed overhead for doing this).
-
- An alpha channel in the source image will raise an exception.
-
- The return values are as for the :meth:`read` method except that
- the *info* reflect the returned pixels, not the source image.
- In particular,
- for this method ``info['greyscale']`` will be ``False``.
- """
-
- width, height, pixels, info = self.asDirect()
- if info['alpha']:
- raise Error("will not convert image with alpha channel to RGB")
- if not info['greyscale']:
- return width, height, pixels, info
- info['greyscale'] = False
- info['planes'] = 3
-
- if info['bitdepth'] > 8:
- def newarray():
- return array('H', [0])
- else:
- def newarray():
- return bytearray([0])
-
- def iterrgb():
- for row in pixels:
- a = newarray() * 3 * width
- for i in range(3):
- a[i::3] = row
- yield a
- return width, height, iterrgb(), info
-
- def asRGBA(self):
- """
- Return image as RGBA pixels.
- Greyscales are expanded into RGB triplets;
- an alpha channel is synthesized if necessary.
- The return values are as for the :meth:`read` method except that
- the *info* reflect the returned pixels, not the source image.
- In particular, for this method
- ``info['greyscale']`` will be ``False``, and
- ``info['alpha']`` will be ``True``.
- """
-
- width, height, pixels, info = self.asDirect()
- if info['alpha'] and not info['greyscale']:
- return width, height, pixels, info
- typecode = 'BH'[info['bitdepth'] > 8]
- maxval = 2**info['bitdepth'] - 1
- maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width
-
- if info['bitdepth'] > 8:
- def newarray():
- return array('H', maxbuffer)
- else:
- def newarray():
- return bytearray(maxbuffer)
-
- if info['alpha'] and info['greyscale']:
- # LA to RGBA
- def convert():
- for row in pixels:
- # Create a fresh target row, then copy L channel
- # into first three target channels, and A channel
- # into fourth channel.
- a = newarray()
- convert_la_to_rgba(row, a)
- yield a
- elif info['greyscale']:
- # L to RGBA
- def convert():
- for row in pixels:
- a = newarray()
- convert_l_to_rgba(row, a)
- yield a
- else:
- assert not info['alpha'] and not info['greyscale']
- # RGB to RGBA
-
- def convert():
- for row in pixels:
- a = newarray()
- convert_rgb_to_rgba(row, a)
- yield a
- info['alpha'] = True
- info['greyscale'] = False
- info['planes'] = 4
- return width, height, convert(), info
-
-
-def decompress(data_blocks):
- """
- `data_blocks` should be an iterable that
- yields the compressed data (from the ``IDAT`` chunks).
- This yields decompressed byte strings.
- """
-
- # Currently, with no max_length parameter to decompress,
- # this routine will do one yield per IDAT chunk: Not very
- # incremental.
- d = zlib.decompressobj()
- # Each IDAT chunk is passed to the decompressor, then any
- # remaining state is decompressed out.
- for data in data_blocks:
- # :todo: add a max_length argument here to limit output size.
- yield bytearray(d.decompress(data))
- yield bytearray(d.flush())
-
-
-def check_bitdepth_colortype(bitdepth, colortype):
- """
- Check that `bitdepth` and `colortype` are both valid,
- and specified in a valid combination.
- Returns (None) if valid, raise an Exception if not valid.
- """
-
- if bitdepth not in (1, 2, 4, 8, 16):
- raise FormatError("invalid bit depth %d" % bitdepth)
- if colortype not in (0, 2, 3, 4, 6):
- raise FormatError("invalid colour type %d" % colortype)
- # Check indexed (palettized) images have 8 or fewer bits
- # per pixel; check only indexed or greyscale images have
- # fewer than 8 bits per pixel.
- if colortype & 1 and bitdepth > 8:
- raise FormatError(
- "Indexed images (colour type %d) cannot"
- " have bitdepth > 8 (bit depth %d)."
- " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
- % (bitdepth, colortype))
- if bitdepth < 8 and colortype not in (0, 3):
- raise FormatError(
- "Illegal combination of bit depth (%d)"
- " and colour type (%d)."
- " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
- % (bitdepth, colortype))
-
-
-def is_natural(x):
- """A non-negative integer."""
- try:
- is_integer = int(x) == x
- except (TypeError, ValueError):
- return False
- return is_integer and x >= 0
-
-
-def undo_filter_sub(filter_unit, scanline, previous, result):
- """Undo sub filter."""
-
- ai = 0
- # Loops starts at index fu. Observe that the initial part
- # of the result is already filled in correctly with
- # scanline.
- for i in range(filter_unit, len(result)):
- x = scanline[i]
- a = result[ai]
- result[i] = (x + a) & 0xff
- ai += 1
-
-
-def undo_filter_up(filter_unit, scanline, previous, result):
- """Undo up filter."""
-
- for i in range(len(result)):
- x = scanline[i]
- b = previous[i]
- result[i] = (x + b) & 0xff
-
-
-def undo_filter_average(filter_unit, scanline, previous, result):
- """Undo up filter."""
-
- ai = -filter_unit
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = 0
- else:
- a = result[ai]
- b = previous[i]
- result[i] = (x + ((a + b) >> 1)) & 0xff
- ai += 1
-
-
-def undo_filter_paeth(filter_unit, scanline, previous, result):
- """Undo Paeth filter."""
-
- # Also used for ci.
- ai = -filter_unit
- for i in range(len(result)):
- x = scanline[i]
- if ai < 0:
- a = c = 0
- else:
- a = result[ai]
- c = previous[ai]
- b = previous[i]
- p = a + b - c
- pa = abs(p - a)
- pb = abs(p - b)
- pc = abs(p - c)
- if pa <= pb and pa <= pc:
- pr = a
- elif pb <= pc:
- pr = b
- else:
- pr = c
- result[i] = (x + pr) & 0xff
- ai += 1
-
-
-def convert_la_to_rgba(row, result):
- for i in range(3):
- result[i::4] = row[0::2]
- result[3::4] = row[1::2]
-
-
-def convert_l_to_rgba(row, result):
- """
- Convert a grayscale image to RGBA.
- This method assumes the alpha channel in result is
- already correctly initialized.
- """
- for i in range(3):
- result[i::4] = row
-
-
-def convert_rgb_to_rgba(row, result):
- """
- Convert an RGB image to RGBA.
- This method assumes the alpha channel in result is
- already correctly initialized.
- """
- for i in range(3):
- result[i::4] = row[i::3]
-
-
-# Only reason to include this in this module is that
-# several utilities need it, and it is small.
-def binary_stdin():
- """
- A sys.stdin that returns bytes.
- """
-
- return sys.stdin.buffer
-
-
-def binary_stdout():
- """
- A sys.stdout that accepts bytes.
- """
-
- stdout = sys.stdout.buffer
-
- # On Windows the C runtime file orientation needs changing.
- if sys.platform == "win32":
- import msvcrt
- import os
- msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
-
- return stdout
-
-
-def cli_open(path):
- if path == "-":
- return binary_stdin()
- return open(path, "rb")
-
-
-def main(argv):
- """
- Run command line PNG.
- Which reports version.
- """
- print(__version__, __file__)
-
-
-if __name__ == '__main__':
- try:
- main(sys.argv)
- except Error as e:
- print(e, file=sys.stderr)
diff --git a/tools/pokemontools/__init__.py b/tools/pokemontools/__init__.py
deleted file mode 100644
index a3e75900..00000000
--- a/tools/pokemontools/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# A subset of https://github.com/pret/pokemon-reverse-engineering-tools
diff --git a/tools/pokemontools/gfx.py b/tools/pokemontools/gfx.py
deleted file mode 100644
index 2979b5a7..00000000
--- a/tools/pokemontools/gfx.py
+++ /dev/null
@@ -1,938 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-import png
-from math import sqrt, floor, ceil
-import argparse
-import operator
-
-from lz import Compressed, Decompressed
-
-
-def split(list_, interval):
- """
- Split a list by length.
- """
- for i in xrange(0, len(list_), interval):
- j = min(i + interval, len(list_))
- yield list_[i:j]
-
-
-def hex_dump(data, length=0x10):
- """
- just use hexdump -C
- """
- margin = len('%x' % len(data))
- output = []
- address = 0
- for line in split(data, length):
- output += [
- hex(address)[2:].zfill(margin) +
- ' | ' +
- ' '.join('%.2x' % byte for byte in line)
- ]
- address += length
- return '\n'.join(output)
-
-
-def get_tiles(image):
- """
- Split a 2bpp image into 8x8 tiles.
- """
- return list(split(image, 0x10))
-
-def connect(tiles):
- """
- Combine 8x8 tiles into a 2bpp image.
- """
- return [byte for tile in tiles for byte in tile]
-
-def transpose(tiles, width=None):
- """
- Transpose a tile arrangement along line y=-x.
-
- 00 01 02 03 04 05 00 06 0c 12 18 1e
- 06 07 08 09 0a 0b 01 07 0d 13 19 1f
- 0c 0d 0e 0f 10 11 <-> 02 08 0e 14 1a 20
- 12 13 14 15 16 17 03 09 0f 15 1b 21
- 18 19 1a 1b 1c 1d 04 0a 10 16 1c 22
- 1e 1f 20 21 22 23 05 0b 11 17 1d 23
-
- 00 01 02 03 00 04 08
- 04 05 06 07 <-> 01 05 09
- 08 09 0a 0b 02 06 0a
- 03 07 0b
- """
- if width == None:
- width = int(sqrt(len(tiles))) # assume square image
- tiles = sorted(enumerate(tiles), key= lambda (i, tile): i % width)
- return [tile for i, tile in tiles]
-
-def transpose_tiles(image, width=None):
- return connect(transpose(get_tiles(image), width))
-
-def interleave(tiles, width):
- """
- 00 01 02 03 04 05 00 02 04 06 08 0a
- 06 07 08 09 0a 0b 01 03 05 07 09 0b
- 0c 0d 0e 0f 10 11 --> 0c 0e 10 12 14 16
- 12 13 14 15 16 17 0d 0f 11 13 15 17
- 18 19 1a 1b 1c 1d 18 1a 1c 1e 20 22
- 1e 1f 20 21 22 23 19 1b 1d 1f 21 23
- """
- interleaved = []
- left, right = split(tiles[::2], width), split(tiles[1::2], width)
- for l, r in zip(left, right):
- interleaved += l + r
- return interleaved
-
-def deinterleave(tiles, width):
- """
- 00 02 04 06 08 0a 00 01 02 03 04 05
- 01 03 05 07 09 0b 06 07 08 09 0a 0b
- 0c 0e 10 12 14 16 --> 0c 0d 0e 0f 10 11
- 0d 0f 11 13 15 17 12 13 14 15 16 17
- 18 1a 1c 1e 20 22 18 19 1a 1b 1c 1d
- 19 1b 1d 1f 21 23 1e 1f 20 21 22 23
- """
- deinterleaved = []
- rows = list(split(tiles, width))
- for left, right in zip(rows[::2], rows[1::2]):
- for l, r in zip(left, right):
- deinterleaved += [l, r]
- return deinterleaved
-
-def interleave_tiles(image, width):
- return connect(interleave(get_tiles(image), width))
-
-def deinterleave_tiles(image, width):
- return connect(deinterleave(get_tiles(image), width))
-
-
-def condense_image_to_map(image, pic=0):
- """
- Reduce an image of adjacent frames to an image containing a base frame and any unrepeated tiles.
- Returns the new image and the corresponding tilemap used to reconstruct the input image.
-
- If <pic> is 0, ignore the concept of frames. This behavior might be better off as another function.
- """
- tiles = get_tiles(image)
- new_tiles, tilemap = condense_tiles_to_map(tiles, pic)
- new_image = connect(new_tiles)
- return new_image, tilemap
-
-def condense_tiles_to_map(tiles, pic=0):
- """
- Reduce a sequence of tiles representing adjacent frames to a base frame and any unrepeated tiles.
- Returns the new tiles and the corresponding tilemap used to reconstruct the input tile sequence.
-
- If <pic> is 0, ignore the concept of frames. This behavior might be better off as another function.
- """
-
- # Leave the first frame intact for pics.
- new_tiles = tiles[:pic]
- tilemap = range(pic)
-
- for i, tile in enumerate(tiles[pic:]):
- if tile not in new_tiles:
- new_tiles.append(tile)
-
- if pic:
- # Match the first frame exactly where possible.
- # This reduces the space needed to replace tiles in pic animations.
- # For example, if a tile is repeated twice in the first frame,
- # but at the same relative index as the second tile, use the second index.
- # When creating a bitmask later, the second index would not require a replacement, but the first index would have.
- pic_i = i % pic
- if tile == new_tiles[pic_i]:
- tilemap.append(pic_i)
- else:
- tilemap.append(new_tiles.index(tile))
- else:
- tilemap.append(new_tiles.index(tile))
- return new_tiles, tilemap
-
-def test_condense_tiles_to_map():
- test = condense_tiles_to_map(list('abcadbae'))
- if test != (list('abcde'), [0, 1, 2, 0, 3, 1, 0, 4]):
- raise Exception(test)
- test = condense_tiles_to_map(list('abcadbae'), 2)
- if test != (list('abcde'), [0, 1, 2, 0, 3, 1, 0, 4]):
- raise Exception(test)
- test = condense_tiles_to_map(list('abcadbae'), 4)
- if test != (list('abcade'), [0, 1, 2, 3, 4, 1, 0, 5]):
- raise Exception(test)
- test = condense_tiles_to_map(list('abcadbea'), 4)
- if test != (list('abcade'), [0, 1, 2, 3, 4, 1, 5, 3]):
- raise Exception(test)
-
-
-def to_file(filename, data):
- """
- Apparently open(filename, 'wb').write(bytearray(data)) won't work.
- """
- file = open(filename, 'wb')
- for byte in data:
- file.write('%c' % byte)
- file.close()
-
-
-def decompress_file(filein, fileout=None):
- image = bytearray(open(filein).read())
- de = Decompressed(image)
-
- if fileout == None:
- fileout = os.path.splitext(filein)[0]
- to_file(fileout, de.output)
-
-
-def compress_file(filein, fileout=None):
- image = bytearray(open(filein).read())
- lz = Compressed(image)
-
- if fileout == None:
- fileout = filein + '.lz'
- to_file(fileout, lz.output)
-
-
-def bin_to_rgb(word):
- red = word & 0b11111
- word >>= 5
- green = word & 0b11111
- word >>= 5
- blue = word & 0b11111
- return (red, green, blue)
-
-def convert_binary_pal_to_text_by_filename(filename):
- pal = bytearray(open(filename).read())
- return convert_binary_pal_to_text(pal)
-
-def convert_binary_pal_to_text(pal):
- output = ''
- words = [hi * 0x100 + lo for lo, hi in zip(pal[::2], pal[1::2])]
- for word in words:
- red, green, blue = ['%.2d' % c for c in bin_to_rgb(word)]
- output += '\tRGB ' + ', '.join((red, green, blue))
- output += '\n'
- return output
-
-def read_rgb_macros(lines):
- colors = []
- for line in lines:
- macro = line.split(" ")[0].strip()
- if macro == 'RGB':
- params = ' '.join(line.split(" ")[1:]).split(',')
- red, green, blue = [int(v) for v in params]
- colors += [[red, green, blue]]
- return colors
-
-
-def rewrite_binary_pals_to_text(filenames):
- for filename in filenames:
- pal_text = convert_binary_pal_to_text_by_filename(filename)
- with open(filename, 'w') as out:
- out.write(pal_text)
-
-
-def flatten(planar):
- """
- Flatten planar 2bpp image data into a quaternary pixel map.
- """
- strips = []
- for bottom, top in split(planar, 2):
- bottom = bottom
- top = top
- strip = []
- for i in xrange(7,-1,-1):
- color = (
- (bottom >> i & 1) +
- (top *2 >> i & 2)
- )
- strip += [color]
- strips += strip
- return strips
-
-def to_lines(image, width):
- """
- Convert a tiled quaternary pixel map to lines of quaternary pixels.
- """
- tile_width = 8
- tile_height = 8
- num_columns = width / tile_width
- height = len(image) / width
-
- lines = []
- for cur_line in xrange(height):
- tile_row = cur_line / tile_height
- line = []
- for column in xrange(num_columns):
- anchor = (
- num_columns * tile_row * tile_width * tile_height +
- column * tile_width * tile_height +
- cur_line % tile_height * tile_width
- )
- line += image[anchor : anchor + tile_width]
- lines += [line]
- return lines
-
-
-def dmg2rgb(word):
- """
- For PNGs.
- """
- def shift(value):
- while True:
- yield value & (2**5 - 1)
- value >>= 5
- word = shift(word)
- # distribution is less even w/ << 3
- red, green, blue = [int(color * 8.25) for color in [word.next() for _ in xrange(3)]]
- alpha = 255
- return (red, green, blue, alpha)
-
-
-def rgb_to_dmg(color):
- """
- For PNGs.
- """
- word = (color['r'] / 8)
- word += (color['g'] / 8) << 5
- word += (color['b'] / 8) << 10
- return word
-
-
-def pal_to_png(filename):
- """
- Interpret a .pal file as a png palette.
- """
- with open(filename) as rgbs:
- colors = read_rgb_macros(rgbs.readlines())
- a = 255
- palette = []
- for color in colors:
- # even distribution over 000-255
- r, g, b = [int(hue * 8.25) for hue in color]
- palette += [(r, g, b, a)]
- white = (255,255,255,255)
- black = (000,000,000,255)
- if white not in palette and len(palette) < 4:
- palette = [white] + palette
- if black not in palette and len(palette) < 4:
- palette = palette + [black]
- return palette
-
-
-def png_to_rgb(palette):
- """
- Convert a png palette to rgb macros.
- """
- output = ''
- for color in palette:
- r, g, b = [color[c] / 8 for c in 'rgb']
- output += '\tRGB ' + ', '.join(['%.2d' % hue for hue in (r, g, b)])
- output += '\n'
- return output
-
-
-def read_filename_arguments(filename):
- """
- Infer graphics conversion arguments given a filename.
-
- Arguments are separated with '.'.
- """
- parsed_arguments = {}
-
- int_arguments = {
- 'w': 'width',
- 'h': 'height',
- 't': 'tile_padding',
- }
- arguments = os.path.splitext(filename)[0].lstrip('.').split('.')[1:]
- for argument in arguments:
-
- # Check for integer arguments first (i.e. "w128").
- arg = argument[0]
- param = argument[1:]
- if param.isdigit():
- arg = int_arguments.get(arg, False)
- if arg:
- parsed_arguments[arg] = int(param)
-
- elif argument == 'arrange':
- parsed_arguments['norepeat'] = True
- parsed_arguments['tilemap'] = True
-
- # Pic dimensions (i.e. "6x6").
- elif 'x' in argument and any(map(str.isdigit, argument)):
- w, h = argument.split('x')
- if w.isdigit() and h.isdigit():
- parsed_arguments['pic_dimensions'] = (int(w), int(h))
-
- else:
- parsed_arguments[argument] = True
-
- return parsed_arguments
-
-
-def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0, tile_padding=0, pic_dimensions=None, **kwargs):
-
- if fileout == None:
- fileout = os.path.splitext(filein)[0] + '.png'
-
- image = open(filein, 'rb').read()
-
- arguments = {
- 'width': width,
- 'height': height,
- 'pal_file': pal_file,
- 'tile_padding': tile_padding,
- 'pic_dimensions': pic_dimensions,
- }
- arguments.update(read_filename_arguments(filein))
-
- if pal_file == None:
- if os.path.exists(os.path.splitext(fileout)[0]+'.pal'):
- arguments['pal_file'] = os.path.splitext(fileout)[0]+'.pal'
-
- result = convert_2bpp_to_png(image, **arguments)
- width, height, palette, greyscale, bitdepth, px_map = result
-
- w = png.Writer(
- width,
- height,
- palette=palette,
- compression=9,
- greyscale=greyscale,
- bitdepth=bitdepth
- )
- with open(fileout, 'wb') as f:
- w.write(f, px_map)
-
-
-def convert_2bpp_to_png(image, **kwargs):
- """
- Convert a planar 2bpp graphic to png.
- """
-
- image = bytearray(image)
-
- pad_color = bytearray([0])
-
- width = kwargs.get('width', 0)
- height = kwargs.get('height', 0)
- tile_padding = kwargs.get('tile_padding', 0)
- pic_dimensions = kwargs.get('pic_dimensions', None)
- pal_file = kwargs.get('pal_file', None)
- interleave = kwargs.get('interleave', False)
-
- # Width must be specified to interleave.
- if interleave and width:
- image = interleave_tiles(image, width / 8)
-
- # Pad the image by a given number of tiles if asked.
- image += pad_color * 0x10 * tile_padding
-
- # Some images are transposed in blocks.
- if pic_dimensions:
- w, h = pic_dimensions
- if not width: width = w * 8
-
- pic_length = w * h * 0x10
-
- trailing = len(image) % pic_length
-
- pic = []
- for i in xrange(0, len(image) - trailing, pic_length):
- pic += transpose_tiles(image[i:i+pic_length], h)
- image = bytearray(pic) + image[len(image) - trailing:]
-
- # Pad out trailing lines.
- image += pad_color * 0x10 * ((w - (len(image) / 0x10) % h) % w)
-
- def px_length(img):
- return len(img) * 4
- def tile_length(img):
- return len(img) * 4 / (8*8)
-
- if width and height:
- tile_width = width / 8
- more_tile_padding = (tile_width - (tile_length(image) % tile_width or tile_width))
- image += pad_color * 0x10 * more_tile_padding
-
- elif width and not height:
- tile_width = width / 8
- more_tile_padding = (tile_width - (tile_length(image) % tile_width or tile_width))
- image += pad_color * 0x10 * more_tile_padding
- height = px_length(image) / width
-
- elif height and not width:
- tile_height = height / 8
- more_tile_padding = (tile_height - (tile_length(image) % tile_height or tile_height))
- image += pad_color * 0x10 * more_tile_padding
- width = px_length(image) / height
-
- # at least one dimension should be given
- if width * height != px_length(image):
- # look for possible combos of width/height that would form a rectangle
- matches = []
- # Height need not be divisible by 8, but width must.
- # See pokered gfx/minimize_pic.1bpp.
- for w in range(8, px_length(image) / 2 + 1, 8):
- h = px_length(image) / w
- if w * h == px_length(image):
- matches += [(w, h)]
- # go for the most square image
- if len(matches):
- width, height = sorted(matches, key= lambda (w, h): (h % 8 != 0, w + h))[0] # favor height
- else:
- raise Exception, 'Image can\'t be divided into tiles (%d px)!' % (px_length(image))
-
- # convert tiles to lines
- lines = to_lines(flatten(image), width)
-
- if pal_file == None:
- palette = None
- greyscale = True
- bitdepth = 2
- px_map = [[3 - pixel for pixel in line] for line in lines]
-
- else: # gbc color
- palette = pal_to_png(pal_file)
- greyscale = False
- bitdepth = 8
- px_map = [[pixel for pixel in line] for line in lines]
-
- return width, height, palette, greyscale, bitdepth, px_map
-
-
-def get_pic_animation(tmap, w, h):
- """
- Generate pic animation data from a combined tilemap of each frame.
- """
- frame_text = ''
- bitmask_text = ''
-
- frames = list(split(tmap, w * h))
- base = frames.pop(0)
- bitmasks = []
-
- for i in xrange(len(frames)):
- frame_text += '\tdw .frame{}\n'.format(i + 1)
-
- for i, frame in enumerate(frames):
- bitmask = map(operator.ne, frame, base)
- if bitmask not in bitmasks:
- bitmasks.append(bitmask)
- which_bitmask = bitmasks.index(bitmask)
-
- mask = iter(bitmask)
- masked_frame = filter(lambda _: mask.next(), frame)
-
- frame_text += '.frame{}\n'.format(i + 1)
- frame_text += '\tdb ${:02x} ; bitmask\n'.format(which_bitmask)
- if masked_frame:
- frame_text += '\tdb {}\n'.format(', '.join(
- map('${:02x}'.format, masked_frame)
- ))
-
- for i, bitmask in enumerate(bitmasks):
- bitmask_text += '; {}\n'.format(i)
- for byte in split(bitmask, 8):
- byte = int(''.join(map(int.__repr__, reversed(byte))), 2)
- bitmask_text += '\tdb %{:08b}\n'.format(byte)
-
- return frame_text, bitmask_text
-
-
-def export_png_to_2bpp(filein, fileout=None, palout=None, **kwargs):
-
- arguments = {
- 'tile_padding': 0,
- 'pic_dimensions': None,
- 'animate': False,
- 'stupid_bitmask_hack': [],
- }
- arguments.update(kwargs)
- arguments.update(read_filename_arguments(filein))
-
- image, arguments = png_to_2bpp(filein, **arguments)
-
- if fileout == None:
- fileout = os.path.splitext(filein)[0] + '.2bpp'
- to_file(fileout, image)
-
- tmap = arguments.get('tmap')
-
- if tmap != None and arguments['animate'] and arguments['pic_dimensions']:
- # Generate pic animation data.
- frame_text, bitmask_text = get_pic_animation(tmap, *arguments['pic_dimensions'])
-
- frames_path = os.path.join(os.path.split(fileout)[0], 'frames.asm')
- with open(frames_path, 'w') as out:
- out.write(frame_text)
-
- bitmask_path = os.path.join(os.path.split(fileout)[0], 'bitmask.asm')
-
- # The following Pokemon have a bitmask dummied out.
- for exception in arguments['stupid_bitmask_hack']:
- if exception in bitmask_path:
- bitmasks = bitmask_text.split(';')
- bitmasks[-1] = bitmasks[-1].replace('1', '0')
- bitmask_text = ';'.join(bitmasks)
-
- with open(bitmask_path, 'w') as out:
- out.write(bitmask_text)
-
- elif tmap != None and arguments.get('tilemap', False):
- tilemap_path = os.path.splitext(fileout)[0] + '.tilemap'
- to_file(tilemap_path, tmap)
-
- palette = arguments.get('palette')
- if palout == None:
- palout = os.path.splitext(fileout)[0] + '.pal'
- export_palette(palette, palout)
-
-
-def get_image_padding(width, height, wstep=8, hstep=8):
-
- padding = {
- 'left': 0,
- 'right': 0,
- 'top': 0,
- 'bottom': 0,
- }
-
- if width % wstep and width >= wstep:
- pad = float(width % wstep) / 2
- padding['left'] = int(ceil(pad))
- padding['right'] = int(floor(pad))
-
- if height % hstep and height >= hstep:
- pad = float(height % hstep) / 2
- padding['top'] = int(ceil(pad))
- padding['bottom'] = int(floor(pad))
-
- return padding
-
-
-def png_to_2bpp(filein, **kwargs):
- """
- Convert a png image to planar 2bpp.
- """
-
- arguments = {
- 'tile_padding': 0,
- 'pic_dimensions': False,
- 'interleave': False,
- 'norepeat': False,
- 'tilemap': False,
- }
- arguments.update(kwargs)
-
- if type(filein) is str:
- filein = open(filein)
-
- assert type(filein) is file
-
- width, height, rgba, info = png.Reader(filein).asRGBA8()
-
- # png.Reader returns flat pixel data. Nested is easier to work with
- len_px = len('rgba')
- image = []
- palette = []
- for line in rgba:
- newline = []
- for px in xrange(0, len(line), len_px):
- color = dict(zip('rgba', line[px:px+len_px]))
- if color not in palette:
- if len(palette) < 4:
- palette += [color]
- else:
- # TODO Find the nearest match
- print 'WARNING: %s: Color %s truncated to' % (filein, color),
- color = sorted(palette, key=lambda x: sum(x.values()))[0]
- print color
- newline += [color]
- image += [newline]
-
- assert len(palette) <= 4, '%s: palette should be 4 colors, is really %d (%s)' % (filein, len(palette), palette)
-
- # Pad out smaller palettes with greyscale colors
- greyscale = {
- 'black': { 'r': 0x00, 'g': 0x00, 'b': 0x00, 'a': 0xff },
- 'grey': { 'r': 0x55, 'g': 0x55, 'b': 0x55, 'a': 0xff },
- 'gray': { 'r': 0xaa, 'g': 0xaa, 'b': 0xaa, 'a': 0xff },
- 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff },
- }
- preference = 'white', 'black', 'grey', 'gray'
- for hue in map(greyscale.get, preference):
- if len(palette) >= 4:
- break
- if hue not in palette:
- palette += [hue]
-
- palette.sort(key=lambda x: sum(x.values()))
-
- # Game Boy palette order
- palette.reverse()
-
- # Map pixels to quaternary color ids
- padding = get_image_padding(width, height)
- width += padding['left'] + padding['right']
- height += padding['top'] + padding['bottom']
- pad = bytearray([0])
-
- qmap = []
- qmap += pad * width * padding['top']
- for line in image:
- qmap += pad * padding['left']
- for color in line:
- qmap += [palette.index(color)]
- qmap += pad * padding['right']
- qmap += pad * width * padding['bottom']
-
- # Graphics are stored in tiles instead of lines
- tile_width = 8
- tile_height = 8
- num_columns = max(width, tile_width) / tile_width
- num_rows = max(height, tile_height) / tile_height
- image = []
-
- for row in xrange(num_rows):
- for column in xrange(num_columns):
-
- # Split it up into strips to convert to planar data
- for strip in xrange(min(tile_height, height)):
- anchor = (
- row * num_columns * tile_width * tile_height +
- column * tile_width +
- strip * width
- )
- line = qmap[anchor : anchor + tile_width]
- bottom, top = 0, 0
- for bit, quad in enumerate(line):
- bottom += (quad & 1) << (7 - bit)
- top += (quad /2 & 1) << (7 - bit)
- image += [bottom, top]
-
- dim = arguments['pic_dimensions']
- if dim:
- if type(dim) in (tuple, list):
- w, h = dim
- else:
- # infer dimensions based on width.
- w = width / tile_width
- h = height / tile_height
- if h % w == 0:
- h = w
-
- tiles = get_tiles(image)
- pic_length = w * h
- tile_width = width / 8
- trailing = len(tiles) % pic_length
- new_image = []
- for block in xrange(len(tiles) / pic_length):
- offset = (h * tile_width) * ((block * w) / tile_width) + ((block * w) % tile_width)
- pic = []
- for row in xrange(h):
- index = offset + (row * tile_width)
- pic += tiles[index:index + w]
- new_image += transpose(pic, w)
- new_image += tiles[len(tiles) - trailing:]
- image = connect(new_image)
-
- # Remove any tile padding used to make the png rectangular.
- image = image[:len(image) - arguments['tile_padding'] * 0x10]
-
- tmap = None
-
- if arguments['interleave']:
- image = deinterleave_tiles(image, num_columns)
-
- if arguments['pic_dimensions']:
- image, tmap = condense_image_to_map(image, w * h)
- elif arguments['norepeat']:
- image, tmap = condense_image_to_map(image)
- if not arguments['tilemap']:
- tmap = None
-
- arguments.update({ 'palette': palette, 'tmap': tmap, })
-
- return image, arguments
-
-
-def export_palette(palette, filename):
- """
- Export a palette from png to rgb macros in a .pal file.
- """
-
- if os.path.exists(filename):
-
- # Pic palettes are 2 colors (black/white are added later).
- with open(filename) as rgbs:
- colors = read_rgb_macros(rgbs.readlines())
-
- if len(colors) == 2:
- palette = palette[1:3]
-
- text = png_to_rgb(palette)
- with open(filename, 'w') as out:
- out.write(text)
-
-
-def png_to_lz(filein):
-
- name = os.path.splitext(filein)[0]
-
- export_png_to_2bpp(filein)
- image = open(name+'.2bpp', 'rb').read()
- to_file(name+'.2bpp'+'.lz', Compressed(image).output)
-
-
-def convert_2bpp_to_1bpp(data):
- """
- Convert planar 2bpp image data to 1bpp. Assume images are two colors.
- """
- return data[::2]
-
-def convert_1bpp_to_2bpp(data):
- """
- Convert 1bpp image data to planar 2bpp (black/white).
- """
- output = []
- for i in data:
- output += [i, i]
- return output
-
-
-def export_2bpp_to_1bpp(filename):
- name, extension = os.path.splitext(filename)
- image = open(filename, 'rb').read()
- image = convert_2bpp_to_1bpp(image)
- to_file(name + '.1bpp', image)
-
-def export_1bpp_to_2bpp(filename):
- name, extension = os.path.splitext(filename)
- image = open(filename, 'rb').read()
- image = convert_1bpp_to_2bpp(image)
- to_file(name + '.2bpp', image)
-
-
-def export_1bpp_to_png(filename, fileout=None):
-
- if fileout == None:
- fileout = os.path.splitext(filename)[0] + '.png'
-
- arguments = read_filename_arguments(filename)
-
- image = open(filename, 'rb').read()
- image = convert_1bpp_to_2bpp(image)
-
- result = convert_2bpp_to_png(image, **arguments)
- width, height, palette, greyscale, bitdepth, px_map = result
-
- w = png.Writer(width, height, palette=palette, compression=9, greyscale=greyscale, bitdepth=bitdepth)
- with open(fileout, 'wb') as f:
- w.write(f, px_map)
-
-
-def export_png_to_1bpp(filename, fileout=None):
-
- if fileout == None:
- fileout = os.path.splitext(filename)[0] + '.1bpp'
-
- arguments = read_filename_arguments(filename)
- image = png_to_1bpp(filename, **arguments)
-
- to_file(fileout, image)
-
-def png_to_1bpp(filename, **kwargs):
- image, kwargs = png_to_2bpp(filename, **kwargs)
- return convert_2bpp_to_1bpp(image)
-
-
-def convert_to_2bpp(filenames=[]):
- for filename in filenames:
- filename, name, extension = try_decompress(filename)
- if extension == '.1bpp':
- export_1bpp_to_2bpp(filename)
- elif extension == '.2bpp':
- pass
- elif extension == '.png':
- export_png_to_2bpp(filename)
- else:
- raise Exception, "Don't know how to convert {} to 2bpp!".format(filename)
-
-def convert_to_1bpp(filenames=[]):
- for filename in filenames:
- filename, name, extension = try_decompress(filename)
- if extension == '.1bpp':
- pass
- elif extension == '.2bpp':
- export_2bpp_to_1bpp(filename)
- elif extension == '.png':
- export_png_to_1bpp(filename)
- else:
- raise Exception, "Don't know how to convert {} to 1bpp!".format(filename)
-
-def convert_to_png(filenames=[]):
- for filename in filenames:
- filename, name, extension = try_decompress(filename)
- if extension == '.1bpp':
- export_1bpp_to_png(filename)
- elif extension == '.2bpp':
- export_2bpp_to_png(filename)
- elif extension == '.png':
- pass
- else:
- raise Exception, "Don't know how to convert {} to png!".format(filename)
-
-def compress(filenames=[]):
- for filename in filenames:
- data = open(filename, 'rb').read()
- lz_data = Compressed(data).output
- to_file(filename + '.lz', lz_data)
-
-def decompress(filenames=[]):
- for filename in filenames:
- name, extension = os.path.splitext(filename)
- lz_data = open(filename, 'rb').read()
- data = Decompressed(lz_data).output
- to_file(name, data)
-
-def try_decompress(filename):
- """
- Try to decompress a graphic when determining the filetype.
- This skips the manual unlz step when attempting
- to convert lz-compressed graphics to png.
- """
- name, extension = os.path.splitext(filename)
- if extension == '.lz':
- decompress([filename])
- filename = name
- name, extension = os.path.splitext(filename)
- return filename, name, extension
-
-
-def main():
- ap = argparse.ArgumentParser()
- ap.add_argument('mode')
- ap.add_argument('filenames', nargs='*')
- args = ap.parse_args()
-
- method = {
- '2bpp': convert_to_2bpp,
- '1bpp': convert_to_1bpp,
- 'png': convert_to_png,
- 'lz': compress,
- 'unlz': decompress,
- }.get(args.mode, None)
-
- if method == None:
- raise Exception, "Unknown conversion method!"
-
- method(args.filenames)
-
-if __name__ == "__main__":
- main()
diff --git a/tools/pokemontools/lz.py b/tools/pokemontools/lz.py
deleted file mode 100644
index aef5c641..00000000
--- a/tools/pokemontools/lz.py
+++ /dev/null
@@ -1,580 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Pokemon Crystal data de/compression.
-"""
-
-"""
-A rundown of Pokemon Crystal's compression scheme:
-
-Control commands occupy bits 5-7.
-Bits 0-4 serve as the first parameter <n> for each command.
-"""
-lz_commands = {
- 'literal': 0, # n values for n bytes
- 'iterate': 1, # one value for n bytes
- 'alternate': 2, # alternate two values for n bytes
- 'blank': 3, # zero for n bytes
-}
-
-"""
-Repeater commands repeat any data that was just decompressed.
-They take an additional signed parameter <s> to mark a relative starting point.
-These wrap around (positive from the start, negative from the current position).
-"""
-lz_commands.update({
- 'repeat': 4, # n bytes starting from s
- 'flip': 5, # n bytes in reverse bit order starting from s
- 'reverse': 6, # n bytes backwards starting from s
-})
-
-"""
-The long command is used when 5 bits aren't enough. Bits 2-4 contain a new control code.
-Bits 0-1 are appended to a new byte as 8-9, allowing a 10-bit parameter.
-"""
-lz_commands.update({
- 'long': 7, # n is now 10 bits for a new control code
-})
-max_length = 1 << 10 # can't go higher than 10 bits
-lowmax = 1 << 5 # standard 5-bit param
-
-"""
-If 0xff is encountered instead of a command, decompression ends.
-"""
-lz_end = 0xff
-
-
-bit_flipped = [
- sum(((byte >> i) & 1) << (7 - i) for i in xrange(8))
- for byte in xrange(0x100)
-]
-
-
-class Compressed:
-
- """
- Usage:
- lz = Compressed(data).output
- or
- lz = Compressed().compress(data)
- or
- c = Compressed()
- c.data = data
- lz = c.compress()
-
- There are some issues with reproducing the target compressor.
- Some notes are listed here:
- - the criteria for detecting a lookback is inconsistent
- - sometimes lookbacks that are mostly 0s are pruned, sometimes not
- - target appears to skip ahead if it can use a lookback soon, stopping the current command short or in some cases truncating it with literals.
- - this has been implemented, but the specifics are unknown
- - self.min_scores: It's unknown if blank's minimum score should be 1 or 2. Most likely it's 1, with some other hack to account for edge cases.
- - may be related to the above
- - target does not appear to compress backwards
- """
-
- def __init__(self, *args, **kwargs):
-
- self.min_scores = {
- 'blank': 1,
- 'iterate': 2,
- 'alternate': 3,
- 'repeat': 3,
- 'reverse': 3,
- 'flip': 3,
- }
-
- self.preference = [
- 'repeat',
- 'blank',
- 'flip',
- 'reverse',
- 'iterate',
- 'alternate',
- #'literal',
- ]
-
- self.lookback_methods = 'repeat', 'reverse', 'flip'
-
- self.__dict__.update({
- 'data': None,
- 'commands': lz_commands,
- 'debug': False,
- 'literal_only': False,
- })
-
- self.arg_names = 'data', 'commands', 'debug', 'literal_only'
-
- self.__dict__.update(kwargs)
- self.__dict__.update(dict(zip(self.arg_names, args)))
-
- if self.data is not None:
- self.compress()
-
- def compress(self, data=None):
- if data is not None:
- self.data = data
-
- self.data = list(bytearray(self.data))
-
- self.indexes = {}
- self.lookbacks = {}
- for method in self.lookback_methods:
- self.lookbacks[method] = {}
-
- self.address = 0
- self.end = len(self.data)
- self.output = []
- self.literal = None
-
- while self.address < self.end:
-
- if self.score():
- self.do_literal()
- self.do_winner()
-
- else:
- if self.literal == None:
- self.literal = self.address
- self.address += 1
-
- self.do_literal()
-
- self.output += [lz_end]
- return self.output
-
- def reset_scores(self):
- self.scores = {}
- self.offsets = {}
- self.helpers = {}
- for method in self.min_scores.iterkeys():
- self.scores[method] = 0
-
- def bit_flip(self, byte):
- return bit_flipped[byte]
-
- def do_literal(self):
- if self.literal != None:
- length = abs(self.address - self.literal)
- start = min(self.literal, self.address + 1)
- self.helpers['literal'] = self.data[start:start+length]
- self.do_cmd('literal', length)
- self.literal = None
-
- def score(self):
- self.reset_scores()
-
- map(self.score_literal, ['iterate', 'alternate', 'blank'])
-
- for method in self.lookback_methods:
- self.scores[method], self.offsets[method] = self.find_lookback(method, self.address)
-
- self.stop_short()
-
- return any(
- score
- > self.min_scores[method] + int(score > lowmax)
- for method, score in self.scores.iteritems()
- )
-
- def stop_short(self):
- """
- If a lookback is close, reduce the scores of other commands.
- """
- best_method, best_score = max(
- self.scores.items(),
- key = lambda x: (
- x[1],
- -self.preference.index(x[0])
- )
- )
- for method in self.lookback_methods:
- min_score = self.min_scores[method]
- for address in xrange(self.address+1, self.address+best_score):
- length, index = self.find_lookback(method, address)
- if length > max(min_score, best_score):
- # BUG: lookbacks can reduce themselves. This appears to be a bug in the target also.
- for m, score in self.scores.items():
- self.scores[m] = min(score, address - self.address)
-
-
- def read(self, address=None):
- if address is None:
- address = self.address
- if 0 <= address < len(self.data):
- return self.data[address]
- return None
-
- def find_all_lookbacks(self):
- for method in self.lookback_methods:
- for address, byte in enumerate(self.data):
- self.find_lookback(method, address)
-
- def find_lookback(self, method, address=None):
- """Temporarily stubbed, because the real function doesn't run in polynomial time."""
- return 0, None
-
- def broken_find_lookback(self, method, address=None):
- if address is None:
- address = self.address
-
- existing = self.lookbacks.get(method, {}).get(address)
- if existing != None:
- return existing
-
- lookback = 0, None
-
- # Better to not carelessly optimize at the moment.
- """
- if address < 2:
- return lookback
- """
-
- byte = self.read(address)
- if byte is None:
- return lookback
-
- direction, mutate = {
- 'repeat': ( 1, int),
- 'reverse': (-1, int),
- 'flip': ( 1, self.bit_flip),
- }[method]
-
- # Doesn't seem to help
- """
- if mutate == self.bit_flip:
- if byte == 0:
- self.lookbacks[method][address] = lookback
- return lookback
- """
-
- data_len = len(self.data)
- is_two_byte_index = lambda index: int(index < address - 0x7f)
-
- for index in self.get_indexes(mutate(byte)):
-
- if index >= address:
- break
-
- old_length, old_index = lookback
- if direction == 1:
- if old_length > data_len - index: break
- else:
- if old_length > index: continue
-
- if self.read(index) in [None]: continue
-
- length = 1 # we know there's at least one match, or we wouldn't be checking this index
- while 1:
- this_byte = self.read(address + length)
- that_byte = self.read(index + length * direction)
- if that_byte == None or this_byte != mutate(that_byte):
- break
- length += 1
-
- score = length - is_two_byte_index(index)
- old_score = old_length - is_two_byte_index(old_index)
- if score >= old_score or (score == old_score and length > old_length):
- # XXX maybe avoid two-byte indexes when possible
- if score >= lookback[0] - is_two_byte_index(lookback[1]):
- lookback = length, index
-
- self.lookbacks[method][address] = lookback
- return lookback
-
- def get_indexes(self, byte):
- if not self.indexes.has_key(byte):
- self.indexes[byte] = []
- index = -1
- while 1:
- try:
- index = self.data.index(byte, index + 1)
- except ValueError:
- break
- self.indexes[byte].append(index)
- return self.indexes[byte]
-
- def score_literal(self, method):
- address = self.address
-
- compare = {
- 'blank': [0],
- 'iterate': [self.read(address)],
- 'alternate': [self.read(address), self.read(address + 1)],
- }[method]
-
- # XXX may or may not be correct
- if method == 'alternate' and compare[0] == 0:
- return
-
- length = 0
- while self.read(address + length) == compare[length % len(compare)]:
- length += 1
-
- self.scores[method] = length
- self.helpers[method] = compare
-
- def do_winner(self):
- winners = filter(
- lambda (method, score):
- score
- > self.min_scores[method] + int(score > lowmax),
- self.scores.iteritems()
- )
- winners.sort(
- key = lambda (method, score): (
- -(score - self.min_scores[method] - int(score > lowmax)),
- self.preference.index(method)
- )
- )
- winner, score = winners[0]
-
- length = min(score, max_length)
- self.do_cmd(winner, length)
- self.address += length
-
- def do_cmd(self, cmd, length):
- start_address = self.address
-
- cmd_length = length - 1
-
- output = []
-
- if length > lowmax:
- output.append(
- (self.commands['long'] << 5)
- + (self.commands[cmd] << 2)
- + (cmd_length >> 8)
- )
- output.append(
- cmd_length & 0xff
- )
- else:
- output.append(
- (self.commands[cmd] << 5)
- + cmd_length
- )
-
- self.helpers['blank'] = [] # quick hack
- output += self.helpers.get(cmd, [])
-
- if cmd in self.lookback_methods:
- offset = self.offsets[cmd]
- # Negative offsets are one byte.
- # Positive offsets are two.
- if 0 < start_address - offset - 1 <= 0x7f:
- offset = (start_address - offset - 1) | 0x80
- output += [offset]
- else:
- output += [offset / 0x100, offset % 0x100] # big endian
-
- if self.debug:
- print ' '.join(map(str, [
- cmd, length, '\t',
- ' '.join(map('{:02x}'.format, output)),
- self.data[start_address:start_address+length] if cmd in self.lookback_methods else '',
- ]))
-
- self.output += output
-
-
-
-class Decompressed:
- """
- Interpret and decompress lz-compressed data, usually 2bpp.
- """
-
- """
- Usage:
- data = Decompressed(lz).output
- or
- data = Decompressed().decompress(lz)
- or
- d = Decompressed()
- d.lz = lz
- data = d.decompress()
-
- To decompress from offset 0x80000 in a rom:
- data = Decompressed(rom, start=0x80000).output
- """
-
- lz = None
- start = 0
- commands = lz_commands
- debug = False
-
- arg_names = 'lz', 'start', 'commands', 'debug'
-
- def __init__(self, *args, **kwargs):
- self.__dict__.update(dict(zip(self.arg_names, args)))
- self.__dict__.update(kwargs)
-
- self.command_names = dict(map(reversed, self.commands.items()))
- self.address = self.start
-
- if self.lz is not None:
- self.decompress()
-
- if self.debug: print self.command_list()
-
-
- def command_list(self):
- """
- Print a list of commands that were used. Useful for debugging.
- """
-
- text = ''
-
- output_address = 0
- for name, attrs in self.used_commands:
- length = attrs['length']
- address = attrs['address']
- offset = attrs['offset']
- direction = attrs['direction']
-
- text += '{2:03x} {0}: {1}'.format(name, length, output_address)
- text += '\t' + ' '.join(
- '{:02x}'.format(int(byte))
- for byte in self.lz[ address : address + attrs['cmd_length'] ]
- )
-
- if offset is not None:
- repeated_data = self.output[ offset : offset + length * direction : direction ]
- if name == 'flip':
- repeated_data = map(bit_flipped.__getitem__, repeated_data)
- text += ' [' + ' '.join(map('{:02x}'.format, repeated_data)) + ']'
-
- text += '\n'
- output_address += length
-
- return text
-
-
- def decompress(self, lz=None):
-
- if lz is not None:
- self.lz = lz
-
- self.lz = bytearray(self.lz)
-
- self.used_commands = []
- self.output = []
-
- while 1:
-
- cmd_address = self.address
- self.offset = None
- self.direction = None
-
- if (self.byte == lz_end):
- self.next()
- break
-
- self.cmd = (self.byte & 0b11100000) >> 5
-
- if self.cmd_name == 'long':
- # 10-bit length
- self.cmd = (self.byte & 0b00011100) >> 2
- self.length = (self.next() & 0b00000011) * 0x100
- self.length += self.next() + 1
- else:
- # 5-bit length
- self.length = (self.next() & 0b00011111) + 1
-
- self.__class__.__dict__[self.cmd_name](self)
-
- self.used_commands += [(
- self.cmd_name,
- {
- 'length': self.length,
- 'address': cmd_address,
- 'offset': self.offset,
- 'cmd_length': self.address - cmd_address,
- 'direction': self.direction,
- }
- )]
-
- # Keep track of the data we just decompressed.
- self.compressed_data = self.lz[self.start : self.address]
-
-
- @property
- def byte(self):
- return self.lz[ self.address ]
-
- def next(self):
- byte = self.byte
- self.address += 1
- return byte
-
- @property
- def cmd_name(self):
- return self.command_names.get(self.cmd)
-
-
- def get_offset(self):
-
- if self.byte >= 0x80: # negative
- # negative
- offset = self.next() & 0x7f
- offset = len(self.output) - offset - 1
- else:
- # positive
- offset = self.next() * 0x100
- offset += self.next()
-
- self.offset = offset
-
-
- def literal(self):
- """
- Copy data directly.
- """
- self.output += self.lz[ self.address : self.address + self.length ]
- self.address += self.length
-
- def iterate(self):
- """
- Write one byte repeatedly.
- """
- self.output += [self.next()] * self.length
-
- def alternate(self):
- """
- Write alternating bytes.
- """
- alts = [self.next(), self.next()]
- self.output += [ alts[x & 1] for x in xrange(self.length) ]
-
- def blank(self):
- """
- Write zeros.
- """
- self.output += [0] * self.length
-
- def flip(self):
- """
- Repeat flipped bytes from output.
-
- Example: 11100100 -> 00100111
- """
- self._repeat(table=bit_flipped)
-
- def reverse(self):
- """
- Repeat reversed bytes from output.
- """
- self._repeat(direction=-1)
-
- def repeat(self):
- """
- Repeat bytes from output.
- """
- self._repeat()
-
- def _repeat(self, direction=1, table=None):
- self.get_offset()
- self.direction = direction
- # Note: appends must be one at a time (this way, repeats can draw from themselves if required)
- for i in xrange(self.length):
- byte = self.output[ self.offset + i * direction ]
- self.output.append( table[byte] if table else byte )
diff --git a/tools/pokemontools/png.py b/tools/pokemontools/png.py
deleted file mode 100644
index db6da128..00000000
--- a/tools/pokemontools/png.py
+++ /dev/null
@@ -1,2650 +0,0 @@
-#!/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)
diff --git a/tools/rgb555.py b/tools/rgb555.py
deleted file mode 100755
index bf650313..00000000
--- a/tools/rgb555.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python rgb555.py image.png
-
-Round all colors of the input image to RGB555.
-"""
-
-import sys
-
-import png
-
-def rgb8_to_rgb5(c):
- return (c & 0b11111000) | (c >> 5)
-
-def round_pal(filename):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = list(rows)
- for row in rows:
- del row[3::4]
- rows = [[rgb8_to_rgb5(c) for c in row] for row in rows]
- writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- round_pal(filename)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/sym_comments.py b/tools/sym_comments.py
deleted file mode 100755
index d1545a4e..00000000
--- a/tools/sym_comments.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python sym_comments.py file.asm [pokeyellow.sym] > file_commented.asm
-
-Comments each label in file.asm with its bank:address from the sym file.
-"""
-
-import sys
-import re
-
-def main():
- if len(sys.argv) not in {2, 3}:
- print(f'Usage: {sys.argv[0]} file.asm [pokeyellow.sym] > file_commented.asm', file=sys.stderr)
- sys.exit(1)
-
- wram_name = sys.argv[1]
- sym_name = sys.argv[2] if len(sys.argv) == 3 else 'pokeyellow.sym'
-
- sym_def_rx = re.compile(r'(^{sym})(:.*)|(^\.{sym})(.*)'.format(sym=r'[A-Za-z_][A-Za-z0-9_#@]*'))
-
- sym_addrs = {}
- with open(sym_name, 'r', encoding='utf-8') as file:
- for line in file:
- line = line.split(';', 1)[0].rstrip()
- parts = line.split(' ', 1)
- if len(parts) != 2:
- continue
- addr, sym = parts
- sym_addrs[sym] = addr
-
- with open(wram_name, 'r', encoding='utf-8') as file:
- cur_label = None
- for line in file:
- line = line.rstrip()
- if (m = re.match(sym_def_rx, line)):
- sym, rest = m.group(1), m.group(2)
- if sym is None and rest is None:
- sym, rest = m.group(3), m.group(4)
- key = sym
- if not sym.startswith('.'):
- cur_label = sym
- elif cur_label:
- key = cur_label + sym
- if key in sym_addrs:
- addr = sym_addrs[key]
- line = sym + rest + ' ; ' + addr
- print(line)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/toc.py b/tools/toc.py
deleted file mode 100755
index d9a3d963..00000000
--- a/tools/toc.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python toc.py file.md
-
-Replace a "## TOC" heading in a Markdown file with a table of contents,
-generated from the other headings in the file. Supports multiple files.
-Headings must start with "##" signs to be detected.
-"""
-
-import sys
-import re
-from collections import namedtuple
-from urllib.parse import quote
-
-toc_name = 'Contents'
-valid_toc_headings = {'## TOC', '##TOC'}
-
-TocItem = namedtuple('TocItem', ['name', 'anchor', 'level'])
-punctuation_rx = re.compile(r'[^\w\- ]+')
-numbered_heading_rx = re.compile(r'^[0-9]+\. ')
-specialchar_rx = re.compile(r'[⅔]+')
-
-def name_to_anchor(name):
- # GitHub's algorithm for generating anchors from headings
- # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
- anchor = name.strip().lower() # lowercase
- anchor = re.sub(punctuation_rx, '', anchor) # remove punctuation
- anchor = anchor.replace(' ', '-') # replace spaces with dash
- anchor = re.sub(specialchar_rx, '', anchor) # remove misc special chars
- anchor = quote(anchor) # url encode
- return anchor
-
-def get_toc_index(lines):
- toc_index = None
- for i, line in enumerate(lines):
- if line.rstrip() in valid_toc_headings:
- toc_index = i
- break
- return toc_index
-
-def get_toc_items(lines, toc_index):
- for i, line in enumerate(lines):
- if i <= toc_index:
- continue
- if line.startswith('##'):
- name = line.lstrip('#')
- level = len(line) - len(name) - len('##')
- name = name.strip()
- anchor = name_to_anchor(name)
- yield TocItem(name, anchor, level)
-
-def toc_string(toc_items):
- lines = [f'## {toc_name}', '']
- for name, anchor, level in toc_items:
- padding = ' ' * level
- if re.match(numbered_heading_rx, name):
- bullet, name = name.split('.', 1)
- bullet += '.'
- name = name.lstrip()
- else:
- bullet = '-'
- lines.append(f'{padding}{bullet} [{name}](#{anchor})')
- return '\n'.join(lines) + '\n'
-
-def add_toc(filename):
- with open(filename, 'r', encoding='utf-8') as file:
- lines = file.readlines()
- toc_index = get_toc_index(lines)
- if toc_index is None:
- return None # no TOC heading
- toc_items = list(get_toc_items(lines, toc_index))
- if not toc_items:
- return False # no content headings
- with open(filename, 'w', encoding='utf-8') as file:
- for i, line in enumerate(lines):
- if i == toc_index:
- file.write(toc_string(toc_items))
- else:
- file.write(line)
- return True # OK
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} file.md', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- print(filename)
- result = add_toc(filename)
- if result is None:
- print('Warning: No "## TOC" heading found', file=sys.stderr)
- elif result is False:
- print('Warning: No content headings found', file=sys.stderr)
- else:
- print('OK')
-
-if __name__ == '__main__':
- main()
diff --git a/tools/unique.py b/tools/unique.py
deleted file mode 100755
index 34530a3e..00000000
--- a/tools/unique.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python unique.py [-f|--flip] [-x|--cross] image.png
-
-Erase duplicate tiles from an input image.
--f or --flip counts X/Y mirrored tiles as duplicates.
--x or --cross erases with a cross instead of a blank tile.
-"""
-
-import sys
-
-import png
-
-def rgb5_pixels(row):
- yield from (tuple(c // 8 for c in row[x:x+3]) for x in range(0, len(row), 4))
-
-def rgb8_pixels(row):
- yield from (c * 8 + c // 4 for pixel in row for c in pixel)
-
-def gray_pixels(row):
- yield from (pixel[0] // 10 for pixel in row)
-
-def rows_to_tiles(rows, width, height):
- assert len(rows) == height and len(rows[0]) == width
- yield from (tuple(tuple(row[x:x+8]) for row in rows[y:y+8])
- for y in range(0, height, 8) for x in range(0, width, 8))
-
-def tiles_to_rows(tiles, width, height):
- assert width % 8 == 0 and height % 8 == 0
- width, height = width // 8, height // 8
- tiles = list(tiles)
- assert len(tiles) == width * height
- tile_rows = (tiles[y:y+width] for y in range(0, width * height, width))
- yield from ([tile[y][x] for tile in tile_row for x in range(8)]
- for tile_row in tile_rows for y in range(8))
-
-def tile_variants(tile, flip):
- yield tile
- if flip:
- yield tile[::-1]
- yield tuple(row[::-1] for row in tile)
- yield tuple(row[::-1] for row in tile[::-1])
-
-def unique_tiles(tiles, flip, cross):
- if cross:
- blank = [[(0, 0, 0)] * 8 for _ in range(8)]
- for y in range(8):
- blank[y][y] = blank[y][7 - y] = (31, 31, 31)
- blank = tuple(tuple(row) for row in blank)
- else:
- blank = tuple(tuple([(31, 31, 31)] * 8) for _ in range(8))
- seen = set()
- for tile in tiles:
- if any(variant in seen for variant in tile_variants(tile, flip)):
- yield blank
- else:
- yield tile
- seen.add(tile)
-
-def is_grayscale(colors):
- return (colors.issubset({(31, 31, 31), (21, 21, 21), (10, 10, 10), (0, 0, 0)}) or
- colors.issubset({(31, 31, 31), (20, 20, 20), (10, 10, 10), (0, 0, 0)}))
-
-def erase_duplicates(filename, flip, cross):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = [list(rgb5_pixels(row)) for row in rows]
- if width % 8 or height % 8:
- return False
- tiles = unique_tiles(rows_to_tiles(rows, width, height), flip, cross)
- rows = list(tiles_to_rows(tiles, width, height))
- if is_grayscale({c for row in rows for c in row}):
- rows = [list(gray_pixels(row)) for row in rows]
- writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
- else:
- rows = [list(rgb8_pixels(row)) for row in rows]
- writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
- return True
-
-def main():
- flip = cross = False
- while True:
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} [-f|--flip] [-x|--cross] tileset.png', file=sys.stderr)
- sys.exit(1)
- elif sys.argv[1] in {'-f', '--flip'}:
- flip = True
- elif sys.argv[1] in {'-x', '--cross'}:
- cross = True
- elif sys.argv[1] in {'-fx', '-xf'}:
- flip = cross = True
- else:
- break
- sys.argv.pop(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- elif not erase_duplicates(filename, flip, cross):
- print(f'{filename} is not divisible into 8x8 tiles!', file=sys.stderr)
-
-if __name__ == '__main__':
- main()