注入ICC配置文件生成亮瞎眼HDR图片

借助AI的纯前端实现,向常用图片格式(JPG, PNG, WEBP)注入Rec.2100 PQ ICC配置,实现亮瞎眼的HDR效果。

Demo

ICC插入工具

Rec.2100 PQ ICC 文件下载

ICC文件是用python的PILLOW库,从别人的图片中提取的,下载链接 Rec.2100 PQ

提取代码很简单:

1
2
3
4
5
6
from PIL import Image

img = Image.open("source.png")
icc = img.info.get("icc_profile")
with open("profile.icc", "wb") as f:
f.write(icc)

主要功能的代码实现

插入JPG文件的代码最简单,插入PNG需要CRC32校验以及zlib压缩,插入WEBP需要更新或者构造VP8X块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
const rec2100ICC = new Uint8Array([
// 自行输入icc配置
])

function detectImageType(buf) {
if (buf[0] === 0xFF && buf[1] === 0xD8) return 'jpeg';
if (buf[0] === 0x89 && buf[1] === 0x50) return 'png';
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46) return 'webp';
return null;
}

// JPEG: 插入 APP2 ICC_PROFILE 块
function injectICCToJPEG(buf, icc) {
const header = [...'ICC_PROFILE\u0000'].map(c => c.charCodeAt(0));
const app2 = new Uint8Array(icc.length + header.length + 6);
app2.set([0xFF, 0xE2], 0);
const len = app2.length - 2;
app2[2] = (len >> 8) & 0xFF;
app2[3] = len & 0xFF;
app2.set(header, 4);
app2[4 + header.length] = 1;
app2[5 + header.length] = 1;
app2.set(icc, 6 + header.length);
const out = new Uint8Array(buf.length + app2.length);
out.set(buf.slice(0, 2), 0);
out.set(app2, 2);
out.set(buf.slice(2), 2 + app2.length);
return out;
}

// PNG: 插入 iCCP 块,使用 zlib 压缩
function injectICCToPNG(buf, icc) {
// CRC32 for PNG
function crc32(buf) {
let c = ~0;
for (let i = 0; i < buf.length; i++) {
c ^= buf[i];
for (let j = 0; j < 8; j++) {
c = (c >>> 1) ^ (c & 1 ? 0xEDB88320 : 0);
}
}
return ~c >>> 0;
}
const compressed = pako.deflate(icc);
const profileName = 'ICCProfile';
const nameBytes = [...profileName].map(c => c.charCodeAt(0));
const nullByte = [0x00];
const compressionByte = [0x00];
const data = new Uint8Array(nameBytes.length + 1 + 1 + compressed.length);
data.set(nameBytes, 0);
data.set(nullByte, nameBytes.length);
data.set(compressionByte, nameBytes.length + 1);
data.set(compressed, nameBytes.length + 2);

const type = [0x69, 0x43, 0x43, 0x50]; // 'iCCP'
const crc = crc32(new Uint8Array([...type, ...data]));

const chunk = new Uint8Array(4 + 4 + data.length + 4);
const dv = new DataView(chunk.buffer);
dv.setUint32(0, data.length); // 仅数据部分的长度
chunk.set(type, 4); // chunk type
chunk.set(data, 8); // chunk data
dv.setUint32(8 + data.length, crc); // CRC

// 插入到 IHDR 后
const ihdrEnd = 8 + 25;
const out = new Uint8Array(buf.length + chunk.length);
out.set(buf.slice(0, ihdrEnd), 0);
out.set(chunk, ihdrEnd);
out.set(buf.slice(ihdrEnd), ihdrEnd + chunk.length);
return out;
}

// WebP: 插入 ICCP 块,并更新 VP8X 和 RIFF 封装
function injectICCToWebP(buf, icc) {
function readLE24(buf, offset) {
return buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16);
}

function makeVP8X(width, height) {
const vp8x = new Uint8Array(18);
const view = new DataView(vp8x.buffer);
vp8x.set(new TextEncoder().encode("VP8X"), 0);
view.setUint32(4, 10, true);
vp8x[8] = 0x20; // ICC enabled
vp8x.set([0, 0, 0], 9);
const w = width - 1, h = height - 1;
vp8x[12] = w & 0xff;
vp8x[13] = (w >> 8) & 0xff;
vp8x[14] = (w >> 16) & 0xff;
vp8x[15] = h & 0xff;
vp8x[16] = (h >> 8) & 0xff;
vp8x[17] = (h >> 16) & 0xff;
return vp8x;
}

function makeICCPChunk(data) {
const paddedLength = data.length + (data.length % 2);
const chunk = new Uint8Array(8 + paddedLength);
chunk.set(new TextEncoder().encode("ICCP"), 0);
new DataView(chunk.buffer).setUint32(4, data.length, true);
chunk.set(data, 8);
if (data.length % 2 === 1) chunk[8 + data.length] = 0;
return chunk;
}

const chunks = [];
const view = new DataView(buf.buffer);
let offset = 12;
let foundVP8X = false;
let width = 0, height = 0;
let vp8xChunk = null;

while (offset + 8 <= buf.length) {
const type = String.fromCharCode(...buf.slice(offset, offset + 4));
const size = view.getUint32(offset + 4, true);
const chunkEnd = offset + 8 + size + (size % 2);
const chunkData = buf.slice(offset, chunkEnd);

if (type === "VP8X") {
foundVP8X = true;
vp8xChunk = new Uint8Array(chunkData);
vp8xChunk[8] |= 0x20; // 打开 ICC 位
}

if (!foundVP8X && (type === "VP8 " || type === "VP8L")) {
if (type === "VP8 ") {
const w = view.getUint16(offset + 8 + 6, true);
const h = view.getUint16(offset + 8 + 8, true);
width = w & 0x3fff;
height = h & 0x3fff;
} else {
const b = buf;
const b0 = b[offset + 8 + 1];
const b1 = b[offset + 8 + 2];
const b2 = b[offset + 8 + 3];
const b3 = b[offset + 8 + 4];
width = ((b1 & 0x3F) << 8 | b0) + 1;
height = ((b3 & 0x0F) << 10 | (b2 << 2) | (b1 >> 6)) + 1;
}
}

chunks.push(chunkData);
offset = chunkEnd;
}

if (!foundVP8X) {
if (width === 0 || height === 0) {
alert("无法推断尺寸");
return;
}
vp8xChunk = makeVP8X(width, height);
chunks.unshift(vp8xChunk);
}

const iccpChunk = makeICCPChunk(icc);
chunks.splice(1, 0, iccpChunk); // 插入 VP8X 后

// 重建 RIFF
const size = chunks.reduce((sum, c) => sum + c.length, 0);
const out = new Uint8Array(12 + size);
const dv = new DataView(out.buffer);
out.set(new TextEncoder().encode("RIFF"), 0);
dv.setUint32(4, out.length - 8, true);
out.set(new TextEncoder().encode("WEBP"), 8);

let pos = 12;
for (const chunk of chunks) {
out.set(chunk, pos);
pos += chunk.length;
}
return out;
}

注入ICC配置文件生成亮瞎眼HDR图片

https://psu.monster/post/2025/b1cc9347df5b

作者

psu

发布于

2025-07-12

更新于

2025-07-12

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×