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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apftool
3
- Version: 1.2.0
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 = 1, 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)`
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 = 1, 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)`
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.0"
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! {e}. this likely means the format isnt supported by apftool.")
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
- trunc = False
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
- trunc = True
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 split_points
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 = mapping.index(inpt[0])
151
- g = mapping.index(inpt[1])
152
- b = mapping.index(inpt[2])
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 = mapping.index(inpt[0])
160
- g = mapping.index(inpt[1])
161
- b = mapping.index(inpt[2])
162
- a = mapping.index(inpt[3])
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 = mapping.index(inpt[0])
171
- g = mapping.index(inpt[1])
172
- b = mapping.index(inpt[2])
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 = mapping.index(inpt[0])
188
- g = mapping.index(inpt[1])
189
- b = mapping.index(inpt[2])
190
- a = mapping.index(inpt[3])
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 = mapping.index(inpt[0])
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] = (mapping.index(n), mapping.index(n), mapping.index(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
- apf2 = apf2.decode("ascii")
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 = mapping.index(d)
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 returnbytes:
1480
- return apftext.encode()
1522
+ if compressor is None:
1523
+ if returnbytes:
1524
+ return apftext.encode()
1525
+ else:
1526
+ return apftext
1481
1527
  else:
1482
- return apftext
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
- import numpy as np
20
- from PIL import Image
21
- from scipy.spatial import cKDTree
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.extensions_apf2:
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
- if arg.startswith("-M="):
137
- mode = int(arg.replace("-M=", ""))
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.0
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 = 1, 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)`
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
@@ -7,6 +7,7 @@ apftool/apf.py
7
7
  apftool/apf2.py
8
8
  apftool/apf2quant.py
9
9
  apftool/apfcli.py
10
+ apftool/asciipak.py
10
11
  apftool/bittools.py
11
12
  apftool/bruh.py
12
13
  apftool/mqif.py
@@ -0,0 +1,2 @@
1
+ Pillow
2
+ psqueeze
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apftool"
7
- version = "1.2.0"
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