146 lines
6.0 KiB
JavaScript
146 lines
6.0 KiB
JavaScript
import { Buffer } from 'buffer';
|
||
import RNFS from 'react-native-fs';
|
||
// upng-js: used for PNG encode of CPU recolor.
|
||
import UPNG from 'upng-js';
|
||
import { renderPaintedImageOffscreen } from './paintShaderRuntime';
|
||
import { writePngBase64ToFile, writePngBytesToFile } from './exportUtils';
|
||
/**
|
||
* CPU recolor: directly map pick codes to painted BGR colors (or copy origin).
|
||
* Produces RGBA PNG bytes via upng-js. This is the *fallback* path when rich shader offscreen
|
||
* is not available or fails. It produces flat colors without the editor's lighting + freq texture.
|
||
*/
|
||
function cpuRecolorToPngBytes(originBgr, pickBuffer, paintedRegions, cols, rows) {
|
||
const pixelCount = cols * rows;
|
||
const rgba = new Uint8Array(pixelCount * 4);
|
||
const colorByPickCode = new Map();
|
||
for (const [regionId, color] of paintedRegions) {
|
||
colorByPickCode.set(regionId + 1, color);
|
||
}
|
||
for (let i = 0; i < pixelCount; i++) {
|
||
const code = pickBuffer[i];
|
||
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
||
const d = i * 4;
|
||
if (color) {
|
||
rgba[d] = color.r;
|
||
rgba[d + 1] = color.g;
|
||
rgba[d + 2] = color.b;
|
||
rgba[d + 3] = 255;
|
||
}
|
||
else {
|
||
const s = i * 3;
|
||
rgba[d] = originBgr[s + 2]; // RGB <- BGR
|
||
rgba[d + 1] = originBgr[s + 1];
|
||
rgba[d + 2] = originBgr[s];
|
||
rgba[d + 3] = 255;
|
||
}
|
||
}
|
||
const png = UPNG.encode([rgba.buffer], cols, rows, 0);
|
||
return new Uint8Array(png);
|
||
}
|
||
/** 将上色区域导出为 recolored PNG。
|
||
* 优先级(从好到保底):
|
||
* 1. exportPngBytes(调用方用 makeImageSnapshot 在高分辨率 Canvas 上捕获的完整 shader 结果)—— 推荐的“保存快照”路径,无 CPU 逐像素,无二次 drawAsImage。
|
||
* 2. shaderTextures + render*(通过 renderPaintedImageOffscreen / drawAsImage 重建同一套 PaintShaderLayer + SkSL)。
|
||
* 3. CPU 逐像素 recolor(flat,无光照/纹理,仅作最后兜底,保证保存不中断)。
|
||
*/
|
||
export async function compositePaintedImage(input) {
|
||
const { originBuffer, cols, rows, pickBuffer, paintedRegions, destDir, exportPngBase64, exportPngBytes, shaderTextures, renderWidth, renderHeight, } = input;
|
||
if (paintedRegions.size === 0) {
|
||
throw new Error('No painted regions, cannot save');
|
||
}
|
||
if (pickBuffer.length !== cols * rows) {
|
||
const msg = 'pickMap size does not match image';
|
||
console.error('[VIZ-SAVE] composite will throw:', msg, { pickLen: pickBuffer.length, expected: cols * rows, cols, rows });
|
||
throw new Error(msg);
|
||
}
|
||
if (originBuffer.length !== cols * rows * 3) {
|
||
const msg = 'Original buffer size does not match image';
|
||
console.error('[VIZ-SAVE] composite will throw:', msg, { originLen: originBuffer.length, expected: cols * rows * 3, cols, rows });
|
||
throw new Error(msg);
|
||
}
|
||
let pngBytesForWrite;
|
||
let pngBase64ForWrite;
|
||
let usedSnapshot = false;
|
||
let usedRichShader = false;
|
||
// 1) Highest priority: pre-encoded PNG base64 from makeImageSnapshot (no extra conversion).
|
||
if (exportPngBase64 && exportPngBase64.length > 0) {
|
||
pngBase64ForWrite = exportPngBase64;
|
||
usedSnapshot = true;
|
||
}
|
||
// 1b) Snapshot bytes fallback (legacy callers).
|
||
else if (exportPngBytes && exportPngBytes.length > 0) {
|
||
pngBytesForWrite = exportPngBytes;
|
||
usedSnapshot = true;
|
||
}
|
||
// 2) 回退 rich:用 live 纹理 + drawAsImage 重建与编辑器一致的 shader 结果(带光照 + 频率纹理)。
|
||
else if (shaderTextures && renderWidth && renderHeight) {
|
||
try {
|
||
const offImg = await renderPaintedImageOffscreen({
|
||
originImage: shaderTextures.originImage,
|
||
paintColorMap: shaderTextures.paintColorMap,
|
||
lowFreqImage: shaderTextures.lowFreqImage,
|
||
highFreqImage: shaderTextures.highFreqImage,
|
||
width: renderWidth,
|
||
height: renderHeight,
|
||
showOrigin: false,
|
||
});
|
||
if (offImg) {
|
||
let b64 = '';
|
||
try {
|
||
const enc = offImg.encodeToBase64;
|
||
if (typeof enc === 'function') {
|
||
b64 = enc.call(offImg) || '';
|
||
}
|
||
}
|
||
catch (encErr) {
|
||
console.warn('[VIZ-SAVE] offscreen encodeToBase64 failed:', encErr);
|
||
}
|
||
if (b64 && b64.length > 0) {
|
||
pngBytesForWrite = new Uint8Array(Buffer.from(b64, 'base64'));
|
||
usedRichShader = true;
|
||
}
|
||
try {
|
||
const disp = offImg.dispose;
|
||
if (typeof disp === 'function')
|
||
disp.call(offImg);
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
catch (e) {
|
||
console.warn('[VIZ-SAVE] rich shader offscreen for export failed (will fallback):', e);
|
||
}
|
||
}
|
||
// 3) 最后兜底:CPU 逐像素(flat 颜色,无 editor 质感)。
|
||
if (!pngBase64ForWrite && !pngBytesForWrite) {
|
||
try {
|
||
pngBytesForWrite = cpuRecolorToPngBytes(originBuffer, pickBuffer, paintedRegions, cols, rows);
|
||
}
|
||
catch (e) {
|
||
throw new Error('CPU recolor PNG decoding failed');
|
||
}
|
||
}
|
||
const dir = destDir ?? RNFS.CachesDirectoryPath;
|
||
const filePath = `${dir}/painted_${Date.now()}.png`;
|
||
try {
|
||
if (pngBase64ForWrite) {
|
||
await writePngBase64ToFile(filePath, pngBase64ForWrite);
|
||
}
|
||
else {
|
||
await writePngBytesToFile(filePath, pngBytesForWrite);
|
||
}
|
||
void usedSnapshot;
|
||
void usedRichShader;
|
||
}
|
||
catch (e) {
|
||
console.error('[VIZ-SAVE] composite writeFile threw:', e, { filePath, dir });
|
||
throw e;
|
||
}
|
||
return {
|
||
filePath,
|
||
width: cols,
|
||
height: rows,
|
||
paintedCount: paintedRegions.size,
|
||
};
|
||
}
|
||
//# sourceMappingURL=compositePaintedImage.js.map
|