apftool 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apftool-1.2.0 → apftool-1.2.2}/PKG-INFO +8 -3
- {apftool-1.2.0 → apftool-1.2.2}/README.md +6 -2
- {apftool-1.2.0 → apftool-1.2.2}/apftool/__init__.py +5 -4
- {apftool-1.2.0 → apftool-1.2.2}/apftool/apf2.py +98 -35
- {apftool-1.2.0 → apftool-1.2.2}/apftool/apf2quant.py +7 -4
- {apftool-1.2.0 → apftool-1.2.2}/apftool/apfcli.py +22 -10
- apftool-1.2.2/apftool/asciipak.py +27 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool.egg-info/PKG-INFO +8 -3
- {apftool-1.2.0 → apftool-1.2.2}/apftool.egg-info/SOURCES.txt +1 -0
- apftool-1.2.2/apftool.egg-info/requires.txt +2 -0
- {apftool-1.2.0 → apftool-1.2.2}/pyproject.toml +2 -2
- apftool-1.2.0/apftool.egg-info/requires.txt +0 -1
- {apftool-1.2.0 → apftool-1.2.2}/LICENSE +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/apf.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/bittools.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/bruh.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/mqif.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/otb.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/scqoi.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool/wbmp.py +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool.egg-info/dependency_links.txt +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool.egg-info/entry_points.txt +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/apftool.egg-info/top_level.txt +0 -0
- {apftool-1.2.0 → apftool-1.2.2}/setup.cfg +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apftool
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Encoder and decoder for Aperture Science Image Format (apf), Aperture Science Image Format 2 (apf2), Wireless Bitmap (wbmp), Over-The-Air Bitmap (otb), and FaceDev's .bruh format
|
|
5
5
|
Author: Mari (kbity)
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: Pillow
|
|
10
|
+
Requires-Dist: psqueeze
|
|
10
11
|
Dynamic: license-file
|
|
11
12
|
|
|
12
13
|
# A library for converting to and from Aperture Science apf files, the custom extended apf2s, otb/wbmp images, and facedev's bruh format.
|
|
@@ -40,9 +41,9 @@ bruh is an intentionally terrible format made by facedev. info can be found [her
|
|
|
40
41
|
|
|
41
42
|
`apf.encode(img: bytes | Image.Image, lineskip: int = 1, findbestlineskip: bool = False, returnbytes: bool = False)` takes image bytes or pil image object and outputs apf string. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the possible max of 199 and uses the smallest one. returnbytes makes it return bytes instead of a string.
|
|
42
43
|
|
|
43
|
-
`apf2.encode(img: bytes | Image.Image, lineskip: int =
|
|
44
|
+
`apf2.encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: bool = False, legacy: bool = False, trans = False, pal: int = 95, desc: str = "", prepalette: str = None, dodithering: bool = False, returnbytes: bool = False, compress: bool = True, topside_first: bool = False, emit_redundant_flag: bool = False, mode: int = None, combine: bool = False, avoid_run_breaks: bool = False, width: int = None, height: int = None, maxrunerror: float = 0, maxmotionerror: float = 0, verbose: bool = False, useadvancedquant: bool = True, dither_mode: str = "fs", compressor: str = None, wrapper: bool = True, compresslevel: int = 9)`
|
|
44
45
|
|
|
45
|
-
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens.
|
|
46
|
+
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens. maxrunerror and maxmotionerror control how lossy the image is, verbose increases logging output, useadvancedquant allows you to enable/disable the new quantizer, dither_mode sets its ditherer, which can be "fs" (floyd-steinberg) or "ordered" (bayer), compressor sets the apf2 post-compression algorithm (supports gzip, bzip2, and asciipak (ascii packing)). wrapper toggles wether to append an identifyable magic string to a compressed apf2. compresslevel sets the compression level for bzip2 and gzip
|
|
46
47
|
|
|
47
48
|
`wbmp.encode(img: Image)` takes pil image object and outputs wbmp bytes
|
|
48
49
|
|
|
@@ -100,6 +101,10 @@ apftool provides many extensions tuples:
|
|
|
100
101
|
|
|
101
102
|
## changelog:
|
|
102
103
|
|
|
104
|
+
1.2.2 - add support for squeeze, improve handling for numpy/scipy not being installed. Signifigant performance improvement for Truecolor modes.
|
|
105
|
+
|
|
106
|
+
1.2.1 - add support for 3 of apf2's compressed modes [bzip2, gzip, ascii packing]
|
|
107
|
+
|
|
103
108
|
1.2.0 - increase apfcli verbosity, add lossy encoding options. Allow apfcli to take its own formats as inputs and outputs, also adds a much better quantization algorithm and removes the limitations on indexed animations.
|
|
104
109
|
|
|
105
110
|
1.1.3 - add version field, add width and height options to apf2 encoding
|
|
@@ -29,9 +29,9 @@ bruh is an intentionally terrible format made by facedev. info can be found [her
|
|
|
29
29
|
|
|
30
30
|
`apf.encode(img: bytes | Image.Image, lineskip: int = 1, findbestlineskip: bool = False, returnbytes: bool = False)` takes image bytes or pil image object and outputs apf string. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the possible max of 199 and uses the smallest one. returnbytes makes it return bytes instead of a string.
|
|
31
31
|
|
|
32
|
-
`apf2.encode(img: bytes | Image.Image, lineskip: int =
|
|
32
|
+
`apf2.encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: bool = False, legacy: bool = False, trans = False, pal: int = 95, desc: str = "", prepalette: str = None, dodithering: bool = False, returnbytes: bool = False, compress: bool = True, topside_first: bool = False, emit_redundant_flag: bool = False, mode: int = None, combine: bool = False, avoid_run_breaks: bool = False, width: int = None, height: int = None, maxrunerror: float = 0, maxmotionerror: float = 0, verbose: bool = False, useadvancedquant: bool = True, dither_mode: str = "fs", compressor: str = None, wrapper: bool = True, compresslevel: int = 9)`
|
|
33
33
|
|
|
34
|
-
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens.
|
|
34
|
+
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens. maxrunerror and maxmotionerror control how lossy the image is, verbose increases logging output, useadvancedquant allows you to enable/disable the new quantizer, dither_mode sets its ditherer, which can be "fs" (floyd-steinberg) or "ordered" (bayer), compressor sets the apf2 post-compression algorithm (supports gzip, bzip2, and asciipak (ascii packing)). wrapper toggles wether to append an identifyable magic string to a compressed apf2. compresslevel sets the compression level for bzip2 and gzip
|
|
35
35
|
|
|
36
36
|
`wbmp.encode(img: Image)` takes pil image object and outputs wbmp bytes
|
|
37
37
|
|
|
@@ -89,6 +89,10 @@ apftool provides many extensions tuples:
|
|
|
89
89
|
|
|
90
90
|
## changelog:
|
|
91
91
|
|
|
92
|
+
1.2.2 - add support for squeeze, improve handling for numpy/scipy not being installed. Signifigant performance improvement for Truecolor modes.
|
|
93
|
+
|
|
94
|
+
1.2.1 - add support for 3 of apf2's compressed modes [bzip2, gzip, ascii packing]
|
|
95
|
+
|
|
92
96
|
1.2.0 - increase apfcli verbosity, add lossy encoding options. Allow apfcli to take its own formats as inputs and outputs, also adds a much better quantization algorithm and removes the limitations on indexed animations.
|
|
93
97
|
|
|
94
98
|
1.1.3 - add version field, add width and height options to apf2 encoding
|
|
@@ -6,6 +6,7 @@ from .apfcli import main as cli
|
|
|
6
6
|
extensions = (".apf", ".apf2", ".aif", ".af2", ".ap2", ".aif2", ".a2k", ".ap2k")
|
|
7
7
|
extensions_apf = (".apf", ".aif")
|
|
8
8
|
extensions_apf2 = (".apf2", ".af2", ".ap2", ".aif2", ".a2k", ".ap2k")
|
|
9
|
+
extensions_capf2 = (".a2g", ".a2c", ".a2ci", ".apc", ".aq2", ".aqf", ".a2b")
|
|
9
10
|
extensions_wbmp = (".wbmp", ".wbitmap", ".wbm")
|
|
10
11
|
extensions_otb = (".otb", ".ota", ".otab")
|
|
11
12
|
extensions_bruh = (".bruh", ".brh")
|
|
@@ -13,10 +14,10 @@ extensions_mqif = (".mqif", ".mqi")
|
|
|
13
14
|
|
|
14
15
|
extensions_txt = (".txt", ".text") # txt is seen as a generic container for apf/apf2 because they're just txt files
|
|
15
16
|
|
|
16
|
-
version = "1.2.
|
|
17
|
+
version = "1.2.2"
|
|
17
18
|
|
|
18
19
|
extensions_all = []
|
|
19
|
-
extensions_all.extend(extensions_apf+extensions_apf2+extensions_wbmp+extensions_otb+extensions_bruh+extensions_mqif)
|
|
20
|
+
extensions_all.extend(extensions_apf+extensions_apf2+extensions_capf2+extensions_wbmp+extensions_otb+extensions_bruh+extensions_mqif)
|
|
20
21
|
|
|
21
22
|
def decode(data, format: str = None, returnImageObject: bool = False, provide_extra_data=True):
|
|
22
23
|
if isinstance(data, str):
|
|
@@ -25,7 +26,7 @@ def decode(data, format: str = None, returnImageObject: bool = False, provide_ex
|
|
|
25
26
|
return apf2.decode(data, format, returnImageObject) # apf and af2 decoder
|
|
26
27
|
|
|
27
28
|
elif isinstance(data, bytes):
|
|
28
|
-
if data.startswith(b'APERTURE IMAGE FORMAT (c) '): # this makes me GLaD
|
|
29
|
+
if data.startswith(b'APERTURE IMAGE FORMAT (c) ') or data.startswith(b'APF2') or data.startswith(b'BZh') or data.startswith(b'v\xff') or data.startswith(b'\x1f\x8b\x08') or data.startswith(b'\x83\x42\x2D\x2A\x95\x69\x45\x41'): # this makes me GLaD
|
|
29
30
|
if format is None:
|
|
30
31
|
format = "PNG"
|
|
31
32
|
return apf2.decode(data, format, returnImageObject, provide_extra_data=provide_extra_data)
|
|
@@ -60,7 +61,7 @@ def decode(data, format: str = None, returnImageObject: bool = False, provide_ex
|
|
|
60
61
|
return otb.decode(data, format, returnImageObject) # assume otb if it doesnt look like a wbmp or doesnt taste like a bruh
|
|
61
62
|
|
|
62
63
|
else:
|
|
63
|
-
Exception(f"decoding failed
|
|
64
|
+
Exception(f"decoding failed!. this likely means the format isnt supported by apftool.")
|
|
64
65
|
|
|
65
66
|
else:
|
|
66
67
|
raise Exception("Invalid data! Must be bytes or str")
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
from PIL import Image, ImageSequence
|
|
2
2
|
from collections import Counter
|
|
3
|
-
import io, textwrap, math
|
|
3
|
+
import io, textwrap, math, squeeze
|
|
4
|
+
|
|
5
|
+
compressgood = False
|
|
6
|
+
|
|
7
|
+
from . import asciipak
|
|
8
|
+
try:
|
|
9
|
+
import gzip, bz2
|
|
10
|
+
compressgood = True
|
|
11
|
+
except Exception as e:
|
|
12
|
+
print(f"Importing gzip or bz2 failed, compression will be limited to ascii packing\n{e}")
|
|
4
13
|
|
|
5
14
|
qmf = "basic"
|
|
6
15
|
|
|
@@ -25,6 +34,10 @@ def quant_to_base95(g: int):
|
|
|
25
34
|
mapping = []
|
|
26
35
|
for i in range(256):
|
|
27
36
|
mapping.append(chr(quant_to_base95(i)+32))
|
|
37
|
+
mapping_invert = {}
|
|
38
|
+
for i, c in enumerate(mapping):
|
|
39
|
+
if not c in mapping_invert:
|
|
40
|
+
mapping_invert[c] = i
|
|
28
41
|
|
|
29
42
|
# slow pure python version
|
|
30
43
|
def quantize_most_frequent_basic(img: Image.Image, n_colors: int, palette: list = None, dither: bool = False, dither_mode: str = None): # for use with A and P>256 images, dithering unsupported for basic mode
|
|
@@ -72,20 +85,11 @@ else:
|
|
|
72
85
|
def split_datapoints(data: str, datlen: int):
|
|
73
86
|
leng = len(data)
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
if not leng%datlen == 0:
|
|
88
|
+
if leng % datlen:
|
|
77
89
|
print("Error: Data must be divisible by the apf2 segment length! Attempting decode anyways...")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
split_points = []
|
|
81
|
-
|
|
82
|
-
for n in range(0, leng, datlen):
|
|
83
|
-
split_points.append(data[n:(n+datlen)])
|
|
84
|
-
|
|
85
|
-
if trunc:
|
|
86
|
-
split_points.pop(-1)
|
|
90
|
+
leng -= leng % datlen
|
|
87
91
|
|
|
88
|
-
return
|
|
92
|
+
return [data[n:n + datlen] for n in range(0, leng, datlen)]
|
|
89
93
|
|
|
90
94
|
def apf2_apfdecodedata(data: str, h: int, w: int, apfbuffer: list, lineskip: int, pals: list, trans: bool = False, topdown: bool = False):
|
|
91
95
|
x = 0
|
|
@@ -147,19 +151,19 @@ def flexTypeToRGB(inpt: str, mode: int, trans: bool = False):
|
|
|
147
151
|
# 7 - 95-level Grayscale
|
|
148
152
|
# 8 - 8-bit Grayscale
|
|
149
153
|
if mode == 3:
|
|
150
|
-
r =
|
|
151
|
-
g =
|
|
152
|
-
b =
|
|
154
|
+
r = mapping_invert[inpt[0]]
|
|
155
|
+
g = mapping_invert[inpt[1]]
|
|
156
|
+
b = mapping_invert[inpt[2]]
|
|
153
157
|
r = r+int(r == 254)
|
|
154
158
|
g = g+int(g == 254)
|
|
155
159
|
b = b+int(b == 254)
|
|
156
160
|
return r, g, b
|
|
157
161
|
|
|
158
162
|
elif mode == 4:
|
|
159
|
-
r =
|
|
160
|
-
g =
|
|
161
|
-
b =
|
|
162
|
-
a =
|
|
163
|
+
r = mapping_invert[inpt[0]]
|
|
164
|
+
g = mapping_invert[inpt[1]]
|
|
165
|
+
b = mapping_invert[inpt[2]]
|
|
166
|
+
a = mapping_invert[inpt[3]]
|
|
163
167
|
r = r+int(r == 254)
|
|
164
168
|
g = g+int(g == 254)
|
|
165
169
|
b = b+int(b == 254)
|
|
@@ -167,9 +171,9 @@ def flexTypeToRGB(inpt: str, mode: int, trans: bool = False):
|
|
|
167
171
|
return r, g, b, a
|
|
168
172
|
|
|
169
173
|
elif mode == 5:
|
|
170
|
-
r =
|
|
171
|
-
g =
|
|
172
|
-
b =
|
|
174
|
+
r = mapping_invert[inpt[0]]
|
|
175
|
+
g = mapping_invert[inpt[1]]
|
|
176
|
+
b = mapping_invert[inpt[2]]
|
|
173
177
|
|
|
174
178
|
# compute corrections
|
|
175
179
|
c = ord(inpt[3])-32
|
|
@@ -184,10 +188,10 @@ def flexTypeToRGB(inpt: str, mode: int, trans: bool = False):
|
|
|
184
188
|
return r+rc, g+gc, b+bc
|
|
185
189
|
|
|
186
190
|
elif mode == 6:
|
|
187
|
-
r =
|
|
188
|
-
g =
|
|
189
|
-
b =
|
|
190
|
-
a =
|
|
191
|
+
r = mapping_invert[inpt[0]]
|
|
192
|
+
g = mapping_invert[inpt[1]]
|
|
193
|
+
b = mapping_invert[inpt[2]]
|
|
194
|
+
a = mapping_invert[inpt[3]]
|
|
191
195
|
|
|
192
196
|
# compute corrections
|
|
193
197
|
c = ord(inpt[4])-32
|
|
@@ -199,7 +203,7 @@ def flexTypeToRGB(inpt: str, mode: int, trans: bool = False):
|
|
|
199
203
|
return r+rc, g+gc, b+bc, a+ac
|
|
200
204
|
|
|
201
205
|
elif mode == 7:
|
|
202
|
-
r =
|
|
206
|
+
r = mapping_invert[inpt[0]]
|
|
203
207
|
return [r, r, r]
|
|
204
208
|
|
|
205
209
|
elif mode == 8:
|
|
@@ -334,7 +338,7 @@ def graypal(perfect: bool = False):
|
|
|
334
338
|
else:
|
|
335
339
|
for n in set(mapping):
|
|
336
340
|
if not n in pal:
|
|
337
|
-
pal[n] = (
|
|
341
|
+
pal[n] = (mapping_invert[n], mapping_invert[n], mapping_invert[n])
|
|
338
342
|
return pal
|
|
339
343
|
|
|
340
344
|
def int_alpha_composit(l1: tuple, l2: tuple):
|
|
@@ -390,7 +394,34 @@ def diff_images(base, overlay, magenter = False):
|
|
|
390
394
|
|
|
391
395
|
def decode(apf2: str | bytes, format: str = 'PNG', returnImageObject: bool = False, provide_extra_data: bool = False, composite_layers: bool = True):
|
|
392
396
|
if type(apf2) == bytes:
|
|
393
|
-
|
|
397
|
+
if apf2.startswith(b"APF2"):
|
|
398
|
+
compressed_blob = apf2[6:]
|
|
399
|
+
alg = apf2[4:6]
|
|
400
|
+
if alg == b'b2':
|
|
401
|
+
apf2 = bz2.decompress(compressed_blob)
|
|
402
|
+
elif alg == b'gz':
|
|
403
|
+
apf2 = gzip.decompress(compressed_blob)
|
|
404
|
+
elif alg == b'ci':
|
|
405
|
+
apf2 = asciipak.decompress(compressed_blob)
|
|
406
|
+
elif alg == b'sq':
|
|
407
|
+
apf2 = squeeze.decompress(compressed_blob)
|
|
408
|
+
else:
|
|
409
|
+
raise Exception("Invalid/Unsupported Compression Algorithm")
|
|
410
|
+
apf2 = apf2.decode("ascii")
|
|
411
|
+
elif apf2.startswith(b'BZh'):
|
|
412
|
+
apf2 = bz2.decompress(apf2)
|
|
413
|
+
apf2 = apf2.decode("ascii")
|
|
414
|
+
elif apf2.startswith(b'\x1f\x8b\x08'):
|
|
415
|
+
apf2 = gzip.decompress(apf2)
|
|
416
|
+
apf2 = apf2.decode("ascii")
|
|
417
|
+
elif apf2.startswith(b'v\xff'):
|
|
418
|
+
apf2 = squeeze.decompress(apf2)
|
|
419
|
+
apf2 = apf2.decode("ascii")
|
|
420
|
+
elif apf2.startswith(b'\x83\x42\x2D\x2A\x95\x69\x45\x41'):
|
|
421
|
+
apf2 = asciipak.decompress(apf2)
|
|
422
|
+
apf2 = apf2.decode("ascii")
|
|
423
|
+
else: # also add the check for unwrapped gzip/bzip2 apf2 here
|
|
424
|
+
apf2 = apf2.decode("ascii")
|
|
394
425
|
|
|
395
426
|
apf_list = apf2.splitlines()
|
|
396
427
|
apf_lines = []
|
|
@@ -762,7 +793,7 @@ def generate_runs_apf2_l(bitmap: list, lineskip: int, w: int, h: int, topside_fi
|
|
|
762
793
|
|
|
763
794
|
def uint8_to_base95_with_corrector(val):
|
|
764
795
|
d = mapping[val]
|
|
765
|
-
c =
|
|
796
|
+
c = mapping_invert[d]
|
|
766
797
|
e = val - c
|
|
767
798
|
return d, e
|
|
768
799
|
|
|
@@ -1159,12 +1190,24 @@ def encode_wrapper(legacy, bitmaps, gray, truecolor, lineskip, w, h, topside_fir
|
|
|
1159
1190
|
|
|
1160
1191
|
return output, apf2pal
|
|
1161
1192
|
|
|
1162
|
-
def encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: bool = False, legacy: bool = False, trans = False, pal: int = 95, desc: str = "", prepalette: str = None, dodithering: bool = False, returnbytes: bool = False, compress: bool = True, topside_first: bool = False, emit_redundant_flag: bool = False, mode: int = None, combine: bool = False, avoid_run_breaks: bool = False, width: int = None, height: int = None, maxrunerror: float = 0, maxmotionerror: float = 0, verbose: bool = False, useadvancedquant: bool = True, dither_mode: str = "fs"):
|
|
1193
|
+
def encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: bool = False, legacy: bool = False, trans = False, pal: int = 95, desc: str = "", prepalette: str = None, dodithering: bool = False, returnbytes: bool = False, compress: bool = True, topside_first: bool = False, emit_redundant_flag: bool = False, mode: int = None, combine: bool = False, avoid_run_breaks: bool = False, width: int = None, height: int = None, maxrunerror: float = 0, maxmotionerror: float = 0, verbose: bool = False, useadvancedquant: bool = True, dither_mode: str = "fs", compressor: str = None, wrapper: bool = True, compresslevel: int = 9):
|
|
1163
1194
|
frametiming = None
|
|
1164
1195
|
bakedpal = None
|
|
1165
1196
|
dim = False
|
|
1166
1197
|
transmag = False
|
|
1167
1198
|
|
|
1199
|
+
if not compressor in (None, "none", "bzip2", "gzip", "asciipak", "squeeze"):
|
|
1200
|
+
raise Exception('Error: Invalid Compressor! Options: none, bzip2, gzip, asciipak, squeeze')
|
|
1201
|
+
|
|
1202
|
+
if compressor in ("bzip2", "gzip") and not compressgood:
|
|
1203
|
+
raise Exception('Error: bzip2 and gzip is not currently supported by your python install!')
|
|
1204
|
+
|
|
1205
|
+
if compressor == "none":
|
|
1206
|
+
compressor = None # set it to the real nonetype
|
|
1207
|
+
|
|
1208
|
+
if useadvancedquant and qmf == "basic": # disable the good quantizer
|
|
1209
|
+
useadvancedquant = False
|
|
1210
|
+
|
|
1168
1211
|
trans = int(trans) # compatability filter
|
|
1169
1212
|
if trans > 2:
|
|
1170
1213
|
raise Exception("Error: Invalid Transparency Mode!")
|
|
@@ -1476,7 +1519,27 @@ def encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: boo
|
|
|
1476
1519
|
print(f"APF2 Version: {version}")
|
|
1477
1520
|
print(f"APF2 Header Line: {apflist[1]}")
|
|
1478
1521
|
|
|
1479
|
-
if
|
|
1480
|
-
|
|
1522
|
+
if compressor is None:
|
|
1523
|
+
if returnbytes:
|
|
1524
|
+
return apftext.encode()
|
|
1525
|
+
else:
|
|
1526
|
+
return apftext
|
|
1481
1527
|
else:
|
|
1482
|
-
|
|
1528
|
+
apfbin = apftext.encode()
|
|
1529
|
+
# "none", "bzip2", "gzip", "asciipak", "squeeze"
|
|
1530
|
+
wraphead = b"APF2"
|
|
1531
|
+
if compressor == "bzip2":
|
|
1532
|
+
apfbin_c = bz2.compress(apfbin, compresslevel=compresslevel)
|
|
1533
|
+
wraphead+=b"b2"
|
|
1534
|
+
elif compressor == "gzip":
|
|
1535
|
+
apfbin_c = gzip.compress(apfbin, compresslevel=compresslevel)
|
|
1536
|
+
wraphead+=b"gz"
|
|
1537
|
+
elif compressor == "squeeze":
|
|
1538
|
+
apfbin_c = squeeze.compress(apfbin)
|
|
1539
|
+
wraphead+=b"sq"
|
|
1540
|
+
else:
|
|
1541
|
+
apfbin_c = asciipak.compress(apfbin)
|
|
1542
|
+
wraphead+=b"ci"
|
|
1543
|
+
if wrapper:
|
|
1544
|
+
apfbin_c = wraphead+apfbin_c
|
|
1545
|
+
return apfbin_c
|
|
@@ -16,9 +16,12 @@ Pipeline
|
|
|
16
16
|
"""
|
|
17
17
|
import heapq
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from
|
|
19
|
+
try:
|
|
20
|
+
import numpy as np
|
|
21
|
+
from PIL import Image
|
|
22
|
+
from scipy.spatial import cKDTree
|
|
23
|
+
except Exception:
|
|
24
|
+
print("Error Loading numpy or scipy")
|
|
22
25
|
|
|
23
26
|
# ---------------------------------------------------------------- Oklab ----
|
|
24
27
|
_M1 = np.array([[0.4122214708, 0.5363325363, 0.0514459929],
|
|
@@ -233,4 +236,4 @@ def quantize_perceptual(img: Image.Image, n_colors: int, palette: list = None,
|
|
|
233
236
|
raise ValueError("dither_mode must be 'ordered' or 'fs'")
|
|
234
237
|
|
|
235
238
|
quantized = pal_rgba[idx].reshape(h, w, 4)
|
|
236
|
-
return Image.fromarray(quantized, "RGBA"), [tuple(int(v) for v in c) for c in pal_rgba]
|
|
239
|
+
return Image.fromarray(quantized, "RGBA"), [tuple(int(v) for v in c) for c in pal_rgba]
|
|
@@ -36,6 +36,9 @@ encoding:
|
|
|
36
36
|
--quality=INT - (APF2) (0 to 100) sets both --run-quality and --motion-quality [Alias: -Q=INT]
|
|
37
37
|
--pillow - (APF2) use pillow's quantizer [Alias: -P]
|
|
38
38
|
--bayer - (APF2) use faster bayer/ordered dithering for perceptual mode instead of the slower, but prettier floyd-steinberg
|
|
39
|
+
--compressor=[bzip2, gzip, asciipak, none] - (APF2) set APF2 post-compressor
|
|
40
|
+
--compresslevel=[1-9] - (APF2) set compression level for apf2 compressor (gzip/bzip2) (default: 9)
|
|
41
|
+
--nowrap - (APF2) disable the Compressed APF2 wrapper on compressed APF2 Files
|
|
39
42
|
--mode=INT - (APF2) color mode (specified in A2K) for APF2. [Alias: -M=INT]
|
|
40
43
|
0 - 2 colors
|
|
41
44
|
1 - 95 colors
|
|
@@ -75,12 +78,12 @@ TODO: MQIF encoding
|
|
|
75
78
|
lineskip = None
|
|
76
79
|
dither = False
|
|
77
80
|
description = ""
|
|
78
|
-
if opext in apftool.
|
|
79
|
-
wid = None
|
|
80
|
-
hei = None
|
|
81
|
-
else:
|
|
81
|
+
if opext in apftool.extensions_otb:
|
|
82
82
|
wid = 255
|
|
83
83
|
hei = 255
|
|
84
|
+
else:
|
|
85
|
+
wid = None
|
|
86
|
+
hei = None
|
|
84
87
|
transcolor = None
|
|
85
88
|
flip = False
|
|
86
89
|
compress = True
|
|
@@ -91,6 +94,9 @@ TODO: MQIF encoding
|
|
|
91
94
|
maxrunerror = 0
|
|
92
95
|
useadvancedquant = True
|
|
93
96
|
dither_mode = "fs"
|
|
97
|
+
compressor = None
|
|
98
|
+
wrapper = True
|
|
99
|
+
compresslevel = 9
|
|
94
100
|
if args:
|
|
95
101
|
if "--findbestlineskip" in args:
|
|
96
102
|
fbls = True
|
|
@@ -114,6 +120,8 @@ TODO: MQIF encoding
|
|
|
114
120
|
useadvancedquant = False
|
|
115
121
|
if "--bayer" in args:
|
|
116
122
|
dither_mode = "ordered"
|
|
123
|
+
if "--nowrap" in args:
|
|
124
|
+
wrapper = False
|
|
117
125
|
for arg in args:
|
|
118
126
|
if arg.startswith("--desc=") or arg.startswith("-D="):
|
|
119
127
|
description = arg.replace("--desc=", "").replace("-D=", "")
|
|
@@ -131,10 +139,11 @@ TODO: MQIF encoding
|
|
|
131
139
|
if arg.startswith("--height=") or arg.startswith("-H="):
|
|
132
140
|
hei = int(arg.replace("--height=", "").replace("-H=", ""))
|
|
133
141
|
|
|
134
|
-
if arg.startswith("--mode="):
|
|
135
|
-
mode = int(arg.replace("--mode=", ""))
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
if arg.startswith("--mode=") or arg.startswith("-M="):
|
|
143
|
+
mode = int(arg.replace("--mode=", "").replace("-M=", ""))
|
|
144
|
+
|
|
145
|
+
if arg.startswith("--compresslevel="):
|
|
146
|
+
compresslevel = int(arg.replace("--compresslevel=", ""))
|
|
138
147
|
|
|
139
148
|
if arg.startswith("--quality=") or arg.startswith("-Q="):
|
|
140
149
|
qual = int(arg.replace("--quality=", "").replace("-Q=", ""))/2
|
|
@@ -156,6 +165,9 @@ TODO: MQIF encoding
|
|
|
156
165
|
tcb = int(transcolor_hex[4:6], 16)
|
|
157
166
|
transcolor = (tcr, tcg, tcb)
|
|
158
167
|
|
|
168
|
+
if arg.startswith("--compressor="):
|
|
169
|
+
compressor = arg.replace("--compressor=", "")
|
|
170
|
+
|
|
159
171
|
if mode is None:
|
|
160
172
|
if maxpalette > 9025 and 857375 >= maxpalette:
|
|
161
173
|
maxpalette = 95
|
|
@@ -203,8 +215,8 @@ TODO: MQIF encoding
|
|
|
203
215
|
elif opext in apftool.extensions_mqif:
|
|
204
216
|
encoded = apftool.mqif.encode(img_bytes, transcolor)
|
|
205
217
|
|
|
206
|
-
elif opext in apftool.extensions_apf2:
|
|
207
|
-
encoded = apftool.apf2.encode(img_bytes, lineskip=lineskip, findbestlineskip=fbls, legacy=legacy, trans=trans, pal=maxpalette, desc=description, prepalette= None, dodithering=dither, returnbytes=True, compress=compress, topside_first=flip, mode=mode, combine=combine, avoid_run_breaks=avoid_run_breaks, width=wid, height=hei, maxrunerror = maxrunerror, maxmotionerror = maxmotionerror, verbose = True, useadvancedquant=useadvancedquant, dither_mode=dither_mode)
|
|
218
|
+
elif opext in apftool.extensions_apf2 or opext in apftool.extensions_capf2:
|
|
219
|
+
encoded = apftool.apf2.encode(img_bytes, lineskip=lineskip, findbestlineskip=fbls, legacy=legacy, trans=trans, pal=maxpalette, desc=description, prepalette= None, dodithering=dither, returnbytes=True, compress=compress, topside_first=flip, mode=mode, combine=combine, avoid_run_breaks=avoid_run_breaks, width=wid, height=hei, maxrunerror = maxrunerror, maxmotionerror = maxmotionerror, verbose = True, useadvancedquant=useadvancedquant, dither_mode=dither_mode, compressor=compressor, wrapper=wrapper, compresslevel=compresslevel)
|
|
208
220
|
|
|
209
221
|
else:
|
|
210
222
|
raise ValueError("Unsupported Image Format!")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .bittools import byte_to_boollist, boollist_to_int_var
|
|
2
|
+
|
|
3
|
+
def compress(inpt: bytes):
|
|
4
|
+
buffr = []
|
|
5
|
+
output = bytearray()
|
|
6
|
+
for byte in inpt:
|
|
7
|
+
buffr.extend(byte_to_boollist(byte)[1:])
|
|
8
|
+
if len(buffr) >= 8:
|
|
9
|
+
output.append(boollist_to_int_var(buffr[0:8]))
|
|
10
|
+
buffr = buffr[8:]
|
|
11
|
+
if buffr:
|
|
12
|
+
while len(buffr) < 8:
|
|
13
|
+
buffr.append(False)
|
|
14
|
+
output.append(boollist_to_int_var(buffr))
|
|
15
|
+
return output
|
|
16
|
+
|
|
17
|
+
def decompress(inpt: bytes):
|
|
18
|
+
buffr = []
|
|
19
|
+
output = bytearray()
|
|
20
|
+
for byte in inpt:
|
|
21
|
+
buffr.extend(byte_to_boollist(byte))
|
|
22
|
+
while len(buffr) >= 8:
|
|
23
|
+
v = boollist_to_int_var([0]+buffr[0:7])
|
|
24
|
+
if v:
|
|
25
|
+
output.append(v)
|
|
26
|
+
buffr = buffr[7:]
|
|
27
|
+
return output
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apftool
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Encoder and decoder for Aperture Science Image Format (apf), Aperture Science Image Format 2 (apf2), Wireless Bitmap (wbmp), Over-The-Air Bitmap (otb), and FaceDev's .bruh format
|
|
5
5
|
Author: Mari (kbity)
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: Pillow
|
|
10
|
+
Requires-Dist: psqueeze
|
|
10
11
|
Dynamic: license-file
|
|
11
12
|
|
|
12
13
|
# A library for converting to and from Aperture Science apf files, the custom extended apf2s, otb/wbmp images, and facedev's bruh format.
|
|
@@ -40,9 +41,9 @@ bruh is an intentionally terrible format made by facedev. info can be found [her
|
|
|
40
41
|
|
|
41
42
|
`apf.encode(img: bytes | Image.Image, lineskip: int = 1, findbestlineskip: bool = False, returnbytes: bool = False)` takes image bytes or pil image object and outputs apf string. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the possible max of 199 and uses the smallest one. returnbytes makes it return bytes instead of a string.
|
|
42
43
|
|
|
43
|
-
`apf2.encode(img: bytes | Image.Image, lineskip: int =
|
|
44
|
+
`apf2.encode(img: bytes | Image.Image, lineskip: int = None, findbestlineskip: bool = False, legacy: bool = False, trans = False, pal: int = 95, desc: str = "", prepalette: str = None, dodithering: bool = False, returnbytes: bool = False, compress: bool = True, topside_first: bool = False, emit_redundant_flag: bool = False, mode: int = None, combine: bool = False, avoid_run_breaks: bool = False, width: int = None, height: int = None, maxrunerror: float = 0, maxmotionerror: float = 0, verbose: bool = False, useadvancedquant: bool = True, dither_mode: str = "fs", compressor: str = None, wrapper: bool = True, compresslevel: int = 9)`
|
|
44
45
|
|
|
45
|
-
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens.
|
|
46
|
+
takes image bytes or pil image object and outputs apf2 as a string or ascii bytes. lineskip is interleave value, findbestlineskip brute-forces different interleave values to the provided lineskip and uses the smallest one. legacy uses apf1-style 2 color data instead of a 95 color palette. trans sets transparency mode, with mode 0 being off, mode 1 being index 0 transparency (GIF-like, it overrides a color), and mode 2 being indexed alpha. pal allows you to manually force a smaller or larger palette anything over 95 will use APF2-1994's Dual Indexed Mode. Prepalette is an already encoded APF2 palette you input into the encoder to make the colors deterministic. dodithering enables or disables dithering. returnbytes makes it return bytes instead of a string. These options use the APF2-2000 mode: compress - disable RLE compression, allows for smaller file sizes where RLE is not beneficial. Topside_first makes the image scan top-to-bottom, like most formats. emit_redundant_flag - adds the implicit i flag explicitally to baseline 95 color images. mode - selects apf2 coding mode [0: 2 colors, 1: 95 colors, 2: 9025 colors, 3: near-truecolor, 4: near-truecolor with alpha, 5: truecolor, 6: truecolor with alpha, 7: quantized grayscale (based on 95 color mode), 8: 8-bit grayscale (based on 9025 color mode)], combine attempts to do cross-frame delta coding. avoid_run_breaks reduces the amount of transparency pixels emitted by combine, typically harms efficiency. width and height force the output resolution of the image. if both are specified, the output is streched to the specified resolution. if only 1 is present, the other is determined based on aspect ratio. If unspecfied, no scaling happens. maxrunerror and maxmotionerror control how lossy the image is, verbose increases logging output, useadvancedquant allows you to enable/disable the new quantizer, dither_mode sets its ditherer, which can be "fs" (floyd-steinberg) or "ordered" (bayer), compressor sets the apf2 post-compression algorithm (supports gzip, bzip2, and asciipak (ascii packing)). wrapper toggles wether to append an identifyable magic string to a compressed apf2. compresslevel sets the compression level for bzip2 and gzip
|
|
46
47
|
|
|
47
48
|
`wbmp.encode(img: Image)` takes pil image object and outputs wbmp bytes
|
|
48
49
|
|
|
@@ -100,6 +101,10 @@ apftool provides many extensions tuples:
|
|
|
100
101
|
|
|
101
102
|
## changelog:
|
|
102
103
|
|
|
104
|
+
1.2.2 - add support for squeeze, improve handling for numpy/scipy not being installed. Signifigant performance improvement for Truecolor modes.
|
|
105
|
+
|
|
106
|
+
1.2.1 - add support for 3 of apf2's compressed modes [bzip2, gzip, ascii packing]
|
|
107
|
+
|
|
103
108
|
1.2.0 - increase apfcli verbosity, add lossy encoding options. Allow apfcli to take its own formats as inputs and outputs, also adds a much better quantization algorithm and removes the limitations on indexed animations.
|
|
104
109
|
|
|
105
110
|
1.1.3 - add version field, add width and height options to apf2 encoding
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apftool"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "Encoder and decoder for Aperture Science Image Format (apf), Aperture Science Image Format 2 (apf2), Wireless Bitmap (wbmp), Over-The-Air Bitmap (otb), and FaceDev's .bruh format"
|
|
9
9
|
authors = [{name = "Mari (kbity)"}]
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
|
-
dependencies = ["Pillow"]
|
|
12
|
+
dependencies = ["Pillow", "psqueeze"]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
|
15
15
|
apfcli = "apftool:cli"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Pillow
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|