Skip to content

Instantly share code, notes, and snippets.

@JimChengLin
Last active September 30, 2025 23:21
Show Gist options
  • Save JimChengLin/8c79561b68664492f7a69169af530832 to your computer and use it in GitHub Desktop.
Save JimChengLin/8c79561b68664492f7a69169af530832 to your computer and use it in GitHub Desktop.
Smart HDR Compressor (Detect then Process)
// ==UserScript==
// @name Smart HDR Compressor (Detect then Process)
// @name:zh-CN 智能HDR压缩器 (先检测后处理)
// @namespace http://tampermonkey.net/
// @version 4.1
// @description Only processes images detected as HDR. First, checks if the display is in HDR mode. Then, does a quick scan for bright pixels. If found, performs a high-quality, color-preserving highlight compression in a web worker.
// @description:zh-CN 仅处理检测到的HDR图片。首先检查显示器是否为HDR模式,然后快速扫描图片是否存在高光像素。如果存在,则在后台使用Web Worker进行高质量、无色差的高光压缩。
// @author Gemini
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// 使用一个立即执行函数表达式 (IIFE) 来包裹整个脚本,
// 这样可以避免污染全局命名空间,确保脚本内部变量不会与网页自身的脚本冲突。
// =================================================================================
// STAGE 1: 全局环境检测 (Global Environment Check)
// 这是脚本的第一道,也是最重要的一道性能关卡。
// =================================================================================
// 使用 window.matchMedia API 来检查 CSS 媒体查询 '(dynamic-range: high)' 是否匹配。
// 这能判断当前显示器和浏览器环境是否处于HDR(高动态范围)模式。
if (!window.matchMedia || !window.matchMedia('(dynamic-range: high)').matches) {
// 如果不匹配,说明当前不是HDR环境,那么HDR图片过曝的问题也就不存在。
console.log("HDR 压缩器: 当前显示环境非HDR模式,脚本将不会运行。");
// 直接 return,终止脚本的后续所有操作,实现零资源消耗。
return;
}
console.log("HDR 压缩器: 检测到HDR显示环境,脚本已激活。");
// =================================================================================
// CONFIGURATION: 用户可配置参数
// 你可以在这里微调脚本的行为,以适应你的个人偏好和显示器特性。
// =================================================================================
// 亮度阈值 (0-255)。一个像素的亮度高于此值,就会被认为是“高光像素”。
// 这个值是触发后续压缩处理的“扳机”。
// - 调低此值 (如 235): 会让脚本更敏感,更多的高光会被处理。
// - 调高此值 (如 245): 只有最极端、最刺眼的亮部才会被处理。
const BRIGHTNESS_THRESHOLD = 240;
// 压缩系数 (0.0 - 1.0)。它决定了对高光部分的压缩强度。
// 值为 0.5 意味着,超过阈值的亮度部分将被“削减一半”。
// 例如: 阈值是240,一个像素亮度是250,超出的部分是10。压缩后,这部分变为 10 * 0.5 = 5。最终亮度为 240 + 5 = 245。
// - 调高此值 (如 0.7): 压缩更“狠”,高光部分会变得更暗。
// - 调低此值 (如 0.3): 压缩更“温柔”,效果更细微。
const COMPRESSION_FACTOR = 0.5;
// =================================================================================
// WEB WORKER: 后台像素处理线程
// 这是脚本的核心计算部分,被封装在一个Web Worker里。
// =================================================================================
// 将Worker的代码定义为一个字符串。这是在油猴脚本中内嵌Worker的常用技巧,无需一个单独的.js文件。
const workerCode = `
// Worker 线程的入口点。通过 self.onmessage 接收来自主线程的消息。
self.onmessage = function(event) {
// 从主线程传来的数据中解构出需要用到的变量。
const { imageData, threshold, compressionFactor } = event.data;
const data = imageData.data; // 这是一个包含所有像素RGBA值的一维数组。
const threshold_normalized = threshold / 255; // 将阈值归一化到 0.0-1.0 范围,便于计算。
// 遍历像素数据数组。步长为4,因为每个像素由R, G, B, A四个值组成。
for (let i = 0; i < data.length; i += 4) {
// 将当前像素的R,G,B值也归一化到 0.0-1.0 范围。
const r = data[i] / 255;
const g = data[i + 1] / 255;
const b = data[i + 2] / 255;
// 使用标准的亮度计算公式 (Luminance),它更符合人眼的感知。
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// 如果当前像素的亮度超过了我们设定的阈值...
if (luminance > threshold_normalized) {
// ...那么它就是一个需要被压缩的高光像素。
const excess = luminance - threshold_normalized; // 计算超出阈值的部分。
const new_luminance = threshold_normalized + (excess * compressionFactor); // 根据压缩系数,计算新的亮度值。
// 计算一个缩放比例。这是保证色彩(色相/饱和度)不变的关键!
const scale = new_luminance / luminance;
// 只有当缩放比例小于1时(即确实是降低亮度),才进行操作。
if (scale < 1) {
// 将R, G, B三个通道等比例缩放,这样就不会产生色差。
data[i] *= scale;
data[i + 1] *= scale;
data[i + 2] *= scale;
}
}
}
// 处理完成后,通过 self.postMessage 将修改后的像素数据传回主线程。
self.postMessage(imageData);
};
`;
// 将字符串代码转换成一个Blob对象。
const workerBlob = new Blob([workerCode], { type: 'application/javascript' });
// 为这个Blob对象创建一个URL,以便之后可以创建Worker实例。
const workerUrl = URL.createObjectURL(workerBlob);
// =================================================================================
// STAGE 2: 快速高光检测函数 (Quick Highlight Detection)
// 这是脚本的第二道性能关卡,用于快速判断一张图片是否“值得”进行昂贵的处理。
// =================================================================================
function containsHighlights(imageData, threshold) {
const data = imageData.data;
// 遍历像素数据。
for (let i = 0; i < data.length; i += 4) {
// 为了极致的速度,这里的检测使用一个简化的亮度算法 (R,G,B平均值)。
// 因为它仅用于“检测”而非“精确计算”,所以这点精度损失是值得的。
const brightness = (data[i] + data[i+1] + data[i+2]) / 3;
if (brightness > threshold) {
// 一旦找到任何一个过亮的像素,就立即返回 true,停止继续扫描。
// 这极大地提升了处理SDR图片时的效率。
return true;
}
}
// 如果遍历完所有像素都没找到高光,说明是SDR图片,返回 false。
return false;
}
// =================================================================================
// STAGE 3: 图像处理总调度函数 (Image Processing Orchestrator)
// 这个函数负责调度以上所有功能,处理单张图片。
// =================================================================================
function processImage(img) {
// 通过 `dataset` 属性检查图片的处理状态,防止重复处理。
// 同时忽略没有 src 或者 src 是 data: URL 的图片。
if (img.dataset.hdrProcessed || !img.src || img.src.startsWith('data:')) {
return;
}
// 标记为“处理中”,避免被再次触发。
img.dataset.hdrProcessed = 'pending';
// 创建一个临时的Image对象来加载图片。
// 这样做是为了能设置 crossOrigin 属性,以解决跨域(CORS)问题。
const tempImg = new Image();
tempImg.crossOrigin = "anonymous"; // 尝试以匿名模式请求图片,让服务器返回CORS头部。
tempImg.src = img.src;
// 当图片加载成功后执行...
tempImg.onload = () => {
// 创建一个内存中的Canvas。
const canvas = document.createElement('canvas');
// `willReadFrequently: true` 是一个给浏览器的性能提示,告诉它我们接下来会频繁读取此Canvas的数据。
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = tempImg.naturalWidth;
canvas.height = tempImg.naturalHeight;
try {
// 将图片绘制到Canvas上。
ctx.drawImage(tempImg, 0, 0);
// 从Canvas获取原始像素数据。如果图片跨域且服务器不允许,这一步会抛出安全错误。
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// --- 这是整个脚本最核心的决策点 ---
// 调用快速检测函数,判断图片是否包含高光。
if (containsHighlights(imageData, BRIGHTNESS_THRESHOLD)) {
// 如果包含高光,我们认为它是HDR图片,并进行后续处理。
console.log("HDR 压缩器: 检测到HDR图片,开始处理...", img.src);
img.dataset.hdrProcessed = 'processing';
// 创建后台Worker实例。
const worker = new Worker(workerUrl);
// 设置回调,当Worker处理完成并返回数据时执行。
worker.onmessage = (event) => {
// 将处理后的像素数据写回Canvas。
ctx.putImageData(event.data, 0, 0);
// 将Canvas的内容转换成一个 base64 的 data: URL。
// 然后替换掉原始图片的 src,用户就能看到处理后的效果了。
img.src = canvas.toDataURL();
img.dataset.hdrProcessed = 'done';
// 任务完成,终止Worker以释放内存。
worker.terminate();
};
worker.onerror = (err) => {
console.error("HDR 压缩器 Worker 发生错误:", err);
worker.terminate();
};
// 启动Worker,并将像素数据和配置参数发送给它。
worker.postMessage({ imageData, threshold: BRIGHTNESS_THRESHOLD, compressionFactor: COMPRESSION_FACTOR });
} else {
// 如果不包含高光,我们认为它是SDR图片,直接跳过。
console.log("HDR 压缩器: 检测到SDR图片,跳过处理。", img.src);
img.dataset.hdrProcessed = 'sdr_skipped';
}
} catch (e) {
// 捕获上面 getImageData 可能抛出的CORS错误。
console.warn(`HDR 压缩器: 无法处理图片 ${img.src}。可能受到了跨域保护(CORS)。`, e);
img.dataset.hdrProcessed = 'failed';
}
};
// 如果图片加载失败...
tempImg.onerror = () => {
console.warn(`HDR 压缩器: 无法加载图片 ${img.src}。`);
img.dataset.hdrProcessed = 'failed';
};
}
// =================================================================================
// OBSERVERS: 页面监听器
// 使用现代的API来高效地监听页面上的图片。
// =================================================================================
// 1. IntersectionObserver (交叉观察器)
// 它的作用是“懒加载”处理。只有当图片滚动到屏幕视窗内时,才触发处理函数。
// 这极大地节省了资源,避免了一次性处理页面上所有(包括屏幕外)的图片。
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
processImage(entry.target);
// 一旦触发,就停止对该元素的观察,避免重复触发。
intersectionObserver.unobserve(entry.target);
}
});
});
// 2. MutationObserver (变动观察器)
// 它的作用是监听整个DOM树的变化。当有新的节点(比如图片)被添加到页面上时
// (例如在无限滚动、SPA页面切换等场景下),它能捕捉到这些新图片。
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
// 如果新添加的节点本身就是一张图片...
if (node.nodeName === 'IMG') {
// ...就让 IntersectionObserver 开始观察它。
intersectionObserver.observe(node);
// 如果新节点不是图片,但可能包含图片...
} else if (node.querySelectorAll) {
// ...就查找它内部的所有图片,并让 IntersectionObserver 观察它们。
node.querySelectorAll('img').forEach(img => intersectionObserver.observe(img));
}
});
});
});
// 脚本启动时,开始用 MutationObserver 观察整个文档的变化。
mutationObserver.observe(document.documentElement, {
childList: true, // 观察子节点的添加或删除。
subtree: true // 观察所有后代节点。
});
// 对于脚本运行时页面上已经存在的图片,也需要让 IntersectionObserver 开始观察它们。
document.querySelectorAll('img').forEach(img => intersectionObserver.observe(img));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment