lsfp 1.0.0__py3-none-any.whl
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.
- lsfp/__init__.py +168 -0
- lsfp-1.0.0.dist-info/METADATA +47 -0
- lsfp-1.0.0.dist-info/RECORD +5 -0
- lsfp-1.0.0.dist-info/WHEEL +5 -0
- lsfp-1.0.0.dist-info/top_level.txt +1 -0
lsfp/__init__.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
import gzip
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
class LSFP:
|
|
6
|
+
@staticmethod
|
|
7
|
+
def encode(image_path, output_path, quality=4):
|
|
8
|
+
"""
|
|
9
|
+
将普通图片编码为 LSFP
|
|
10
|
+
:param quality: 颜色量化精度 (4 表示分桶保留高 4 位,对应网页版)
|
|
11
|
+
"""
|
|
12
|
+
img = Image.open(image_path).convert('RGB')
|
|
13
|
+
width, height = img.size
|
|
14
|
+
pixels = list(img.getdata())
|
|
15
|
+
|
|
16
|
+
# 1. 颜色统计与量化
|
|
17
|
+
color_counts = {}
|
|
18
|
+
for r, g, b in pixels:
|
|
19
|
+
hex_color = (r << 16) | (g << 8) | b
|
|
20
|
+
color_counts[hex_color] = color_counts.get(hex_color, 0) + 1
|
|
21
|
+
|
|
22
|
+
sorted_colors = sorted(color_counts.items(), key=lambda x: x[1], reverse=True)
|
|
23
|
+
palette = []
|
|
24
|
+
color_map = {}
|
|
25
|
+
|
|
26
|
+
if len(sorted_colors) <= 256:
|
|
27
|
+
# 颜色少于 256 种,全量保留
|
|
28
|
+
for hex_color, _ in sorted_colors:
|
|
29
|
+
r, g, b = (hex_color >> 16) & 0xFF, (hex_color >> 8) & 0xFF, hex_color & 0xFF
|
|
30
|
+
color_map[hex_color] = len(palette)
|
|
31
|
+
palette.append((r, g, b))
|
|
32
|
+
else:
|
|
33
|
+
# 颜色过多,采用 4x4x4 分桶量化 (防变暗)
|
|
34
|
+
buckets = {}
|
|
35
|
+
for hex_color, count in sorted_colors:
|
|
36
|
+
r, g, b = (hex_color >> 16) & 0xFF, (hex_color >> 8) & 0xFF, hex_color & 0xFF
|
|
37
|
+
bucket_key = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4)
|
|
38
|
+
if bucket_key not in buckets:
|
|
39
|
+
buckets[bucket_key] = {'r': 0, 'g': 0, 'b': 0, 'count': 0}
|
|
40
|
+
bucket = buckets[bucket_key]
|
|
41
|
+
bucket['r'] += r * count
|
|
42
|
+
bucket['g'] += g * count
|
|
43
|
+
bucket['b'] += b * count
|
|
44
|
+
bucket['count'] += count
|
|
45
|
+
|
|
46
|
+
# 取最大的 256 个桶求平均
|
|
47
|
+
sorted_buckets = sorted(buckets.values(), key=lambda x: x['count'], reverse=True)[:256]
|
|
48
|
+
for bucket in sorted_buckets:
|
|
49
|
+
avg_r = round(bucket['r'] / bucket['count'])
|
|
50
|
+
avg_g = round(bucket['g'] / bucket['count'])
|
|
51
|
+
avg_b = round(bucket['b'] / bucket['count'])
|
|
52
|
+
palette.append((avg_r, avg_g, avg_b))
|
|
53
|
+
|
|
54
|
+
# 映射每个像素到最近的调色板颜色
|
|
55
|
+
for hex_color, _ in sorted_colors:
|
|
56
|
+
r, g, b = (hex_color >> 16) & 0xFF, (hex_color >> 8) & 0xFF, hex_color & 0xFF
|
|
57
|
+
min_dist = float('inf')
|
|
58
|
+
best_idx = 0
|
|
59
|
+
for i, (pr, pg, pb) in enumerate(palette):
|
|
60
|
+
dist = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
|
|
61
|
+
if dist < min_dist:
|
|
62
|
+
min_dist = dist
|
|
63
|
+
best_idx = i
|
|
64
|
+
color_map[hex_color] = best_idx
|
|
65
|
+
|
|
66
|
+
# 2. 生成像素索引
|
|
67
|
+
pixel_indices = []
|
|
68
|
+
for r, g, b in pixels:
|
|
69
|
+
hex_color = (r << 16) | (g << 8) | b
|
|
70
|
+
pixel_indices.append(color_map.get(hex_color, 0))
|
|
71
|
+
|
|
72
|
+
# 3. 生成 RLE 数据
|
|
73
|
+
rle_data = bytearray()
|
|
74
|
+
i = 0
|
|
75
|
+
total_pixels = width * height
|
|
76
|
+
while i < total_pixels:
|
|
77
|
+
current_id = pixel_indices[i]
|
|
78
|
+
run_length = 1
|
|
79
|
+
while i + run_length < total_pixels and pixel_indices[i + run_length] == current_id and run_length < 65535:
|
|
80
|
+
run_length += 1
|
|
81
|
+
rle_data.extend(struct.pack('<HH', current_id, run_length))
|
|
82
|
+
i += run_length
|
|
83
|
+
|
|
84
|
+
# 4. 组装二进制流
|
|
85
|
+
buffer = bytearray()
|
|
86
|
+
# Header (LSFP + v4 + flags + width + height)
|
|
87
|
+
buffer.extend(b'LSFP')
|
|
88
|
+
buffer.extend(struct.pack('<BBII', 4, 0, width, height))
|
|
89
|
+
|
|
90
|
+
# Palette
|
|
91
|
+
buffer.extend(struct.pack('<H', len(palette)))
|
|
92
|
+
for r, g, b in palette:
|
|
93
|
+
buffer.extend(struct.pack('<BBB', r, g, b))
|
|
94
|
+
|
|
95
|
+
# RLE Chunk (0x01)
|
|
96
|
+
buffer.extend(struct.pack('<B', 0x01))
|
|
97
|
+
buffer.extend(struct.pack('<I', len(rle_data)))
|
|
98
|
+
buffer.extend(rle_data)
|
|
99
|
+
|
|
100
|
+
# End Chunk (0xFF)
|
|
101
|
+
buffer.extend(struct.pack('<B', 0xFF))
|
|
102
|
+
buffer.extend(struct.pack('<I', 0))
|
|
103
|
+
|
|
104
|
+
# 5. Gzip 压缩
|
|
105
|
+
with gzip.open(output_path, 'wb') as f:
|
|
106
|
+
f.write(buffer)
|
|
107
|
+
|
|
108
|
+
print(f"✅ 压缩成功: {output_path}")
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def decode(lsfp_path, output_path):
|
|
112
|
+
"""将 LSFP 解码为普通图片"""
|
|
113
|
+
with open(lsfp_path, 'rb') as f:
|
|
114
|
+
data = f.read()
|
|
115
|
+
|
|
116
|
+
# 自动检测是否 Gzip 压缩 (1F 8B)
|
|
117
|
+
if data[:2] == b'\x1f\x8b':
|
|
118
|
+
data = gzip.decompress(data)
|
|
119
|
+
|
|
120
|
+
offset = 0
|
|
121
|
+
magic = data[offset:offset+4]
|
|
122
|
+
if magic != b'LSFP':
|
|
123
|
+
raise ValueError("❌ 这不是一个有效的 LSFP 文件")
|
|
124
|
+
|
|
125
|
+
offset += 6 # 跳过 magic + version + flags
|
|
126
|
+
width, height = struct.unpack('<II', data[offset:offset+8])
|
|
127
|
+
offset += 8
|
|
128
|
+
|
|
129
|
+
# 读取调色板
|
|
130
|
+
color_count = struct.unpack('<H', data[offset:offset+2])[0]
|
|
131
|
+
offset += 2
|
|
132
|
+
palette = []
|
|
133
|
+
for _ in range(color_count):
|
|
134
|
+
r, g, b = struct.unpack('<BBB', data[offset:offset+3])
|
|
135
|
+
palette.append((r, g, b))
|
|
136
|
+
offset += 3
|
|
137
|
+
|
|
138
|
+
# 创建空白画布
|
|
139
|
+
img = Image.new('RGB', (width, height))
|
|
140
|
+
pixels = []
|
|
141
|
+
|
|
142
|
+
# 解析 Chunk
|
|
143
|
+
while offset < len(data):
|
|
144
|
+
block_type = data[offset]
|
|
145
|
+
offset += 1
|
|
146
|
+
if block_type == 0xFF:
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
block_length = struct.unpack('<I', data[offset:offset+4])[0]
|
|
150
|
+
offset += 4
|
|
151
|
+
|
|
152
|
+
if block_type == 0x01:
|
|
153
|
+
end_of_block = offset + block_length
|
|
154
|
+
while offset < end_of_block:
|
|
155
|
+
color_id, run_length = struct.unpack('<HH', data[offset:offset+4])
|
|
156
|
+
offset += 4
|
|
157
|
+
pixels.extend([palette[color_id]] * run_length)
|
|
158
|
+
else:
|
|
159
|
+
# 遇到不认识的块(比如 v4.0 的高级块),直接跳过
|
|
160
|
+
offset += block_length
|
|
161
|
+
|
|
162
|
+
# 将像素填入画布
|
|
163
|
+
if len(pixels) == width * height:
|
|
164
|
+
img.putdata(pixels)
|
|
165
|
+
img.save(output_path)
|
|
166
|
+
print(f"✅ 解压成功: {output_path}")
|
|
167
|
+
else:
|
|
168
|
+
print(f"❌ 错误: 像素数量不匹配 (预期 {width*height},实际 {len(pixels)})")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lsfp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A hybrid image compression format by LelelelingStudio
|
|
5
|
+
Author-email: LelelelingStudio <hello@leleleling.site>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: Pillow>=9.0.0
|
|
13
|
+
|
|
14
|
+
# LSFP for Python (LelelelingStudio's Format Picture)
|
|
15
|
+
|
|
16
|
+
An ultra-fast, hybrid image compression library for Python.
|
|
17
|
+
|
|
18
|
+
LSFP is designed to reduce file sizes drastically for images with large solid-color areas, UI screenshots, and icons by combining **Color Quantization**, **Run-Length Encoding (RLE)**, and **Gzip compression**.
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
*Note: This package is currently under development and will be available on PyPI soon.*
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install lsfp
|
|
26
|
+
🚀 Quick Start
|
|
27
|
+
The library automatically installs its only dependency (Pillow) for reading and writing standard image formats.
|
|
28
|
+
|
|
29
|
+
python
|
|
30
|
+
import lsfp
|
|
31
|
+
|
|
32
|
+
# 1. Encode a standard image to LSFP
|
|
33
|
+
lsfp.LSFP.encode("input.png", "output.lsfp")
|
|
34
|
+
|
|
35
|
+
# 2. Decode an LSFP file back to a standard image
|
|
36
|
+
lsfp.LSFP.decode("output.lsfp", "restored.png")
|
|
37
|
+
✨ Features
|
|
38
|
+
Extreme Compression: Up to 80% smaller than PNG for UI screenshots.
|
|
39
|
+
|
|
40
|
+
Cross-Platform: Fully compatible with the LSFP Web implementation (JS).
|
|
41
|
+
|
|
42
|
+
Zero-Config: Built-in intelligent color quantization prevents image darkening.
|
|
43
|
+
|
|
44
|
+
📄 License
|
|
45
|
+
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
|
|
46
|
+
|
|
47
|
+
Made with ❤️ by LelelelingStudio.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
lsfp/__init__.py,sha256=hc-yVtzznnuYhPLbPETauvnZqYvy1W5Q97i0BeFocbU,6335
|
|
2
|
+
lsfp-1.0.0.dist-info/METADATA,sha256=R9gvjy_VMz3vY_CYeUHW6RCY5QrU0iY-LAAzo3z1GJE,1609
|
|
3
|
+
lsfp-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
+
lsfp-1.0.0.dist-info/top_level.txt,sha256=EDnKoDXEn9psP6KGdi4ulu0qQABrfYRGLrr_XMS5h1E,5
|
|
5
|
+
lsfp-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lsfp
|