update code level
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -54,7 +54,7 @@ dependencies = [
|
||||
"actix-utils",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.11.0",
|
||||
"brotli",
|
||||
"brotli 8.0.2",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more",
|
||||
@@ -357,6 +357,17 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor 2.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
@@ -365,7 +376,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
"brotli-decompressor 5.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1228,6 +1249,7 @@ version = "1.0.0"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-web",
|
||||
"brotli 3.5.0",
|
||||
"chrono",
|
||||
"config",
|
||||
"image",
|
||||
|
||||
@@ -20,3 +20,4 @@ tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
thiserror = "1.0"
|
||||
mimalloc = "0.1.48"
|
||||
brotli = "3.4"
|
||||
@@ -10,6 +10,118 @@ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
// API基础URL
|
||||
const API_BASE = '/api';
|
||||
|
||||
// 图片加载器类(与app.js中的保持一致)
|
||||
class ImageLoader {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
// 加载并解压图片
|
||||
async loadImage(imageId, element) {
|
||||
// 检查缓存
|
||||
if (this.cache.has(imageId)) {
|
||||
const cachedData = this.cache.get(imageId);
|
||||
this.setImageSrc(element, cachedData.blobUrl, cachedData.mimeType);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/image/${imageId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentEncoding = response.headers.get('content-encoding');
|
||||
const originalContentType = response.headers.get('x-original-content-type');
|
||||
|
||||
let imageData;
|
||||
|
||||
if (contentEncoding === 'br') {
|
||||
// Brotli压缩数据,需要解压
|
||||
const compressedData = await response.arrayBuffer();
|
||||
imageData = await this.decompressBrotli(compressedData);
|
||||
} else {
|
||||
// 未压缩数据
|
||||
imageData = await response.blob();
|
||||
}
|
||||
|
||||
// 创建Blob URL
|
||||
const mimeType = originalContentType || contentType || 'image/jpeg';
|
||||
const blob = new Blob([imageData], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 缓存结果
|
||||
this.cache.set(imageId, { blobUrl, mimeType });
|
||||
|
||||
// 设置图片源
|
||||
this.setImageSrc(element, blobUrl, mimeType);
|
||||
|
||||
} catch (error) {
|
||||
console.error('图片加载失败:', error);
|
||||
// 加载失败时使用占位图
|
||||
element.src = '/assets/placeholder.svg';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置图片源
|
||||
setImageSrc(element, blobUrl, mimeType) {
|
||||
// 创建新的Image对象进行预加载
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
element.src = blobUrl;
|
||||
element.classList.add('loaded');
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('图片解码失败');
|
||||
element.src = '/assets/placeholder.svg';
|
||||
};
|
||||
img.src = blobUrl;
|
||||
}
|
||||
|
||||
// Brotli解压
|
||||
async decompressBrotli(compressedData) {
|
||||
// 检查浏览器是否支持Brotli解压
|
||||
if (window.TextDecoder && window.TextEncoder) {
|
||||
try {
|
||||
// 使用浏览器的Brotli解压(如果支持)
|
||||
if (window.DecompressionStream && 'br' in window) {
|
||||
const decompressionStream = new DecompressionStream('br');
|
||||
const decompressedStream = new Response(compressedData).body.pipeThrough(decompressionStream);
|
||||
return new Response(decompressedStream).arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('浏览器Brotli解压失败,使用JavaScript解压:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用JavaScript实现的Brotli解压(备用方案)
|
||||
return await this.decompressBrotliJS(compressedData);
|
||||
}
|
||||
|
||||
// JavaScript实现的Brotli解压(备用方案)
|
||||
async decompressBrotliJS(compressedData) {
|
||||
// 这里可以集成一个轻量级的Brotli解压库
|
||||
// 由于Brotli解压比较复杂,这里先返回原始数据
|
||||
// 在实际项目中,可以使用第三方库如brotli-wasm
|
||||
console.warn('Brotli解压未实现,返回原始数据');
|
||||
return compressedData;
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
clearCache() {
|
||||
// 清理所有Blob URL
|
||||
for (const [imageId, cached] of this.cache.entries()) {
|
||||
URL.revokeObjectURL(cached.blobUrl);
|
||||
}
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局图片加载器实例
|
||||
const imageLoader = new ImageLoader();
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 添加设备类到body
|
||||
@@ -31,8 +143,77 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// 初始化键盘导航和触摸手势
|
||||
initLightboxControls();
|
||||
|
||||
// 初始化图片懒加载
|
||||
initAlbumLazyLoading();
|
||||
});
|
||||
|
||||
// 初始化相册图片懒加载
|
||||
function initAlbumLazyLoading() {
|
||||
// 使用Intersection Observer实现懒加载
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
const imageId = img.getAttribute('data-image-id');
|
||||
|
||||
if (imageId && !img.classList.contains('loaded')) {
|
||||
imageLoader.loadImage(imageId, img);
|
||||
}
|
||||
|
||||
observer.unobserve(img);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '200px 0px', // 提前200px开始加载
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
// 观察所有带有data-image-id的图片
|
||||
document.querySelectorAll('img[data-image-id]').forEach(img => {
|
||||
if (!img.classList.contains('loaded')) {
|
||||
observer.observe(img);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 不支持Intersection Observer的回退方案
|
||||
console.warn('浏览器不支持Intersection Observer,使用回退懒加载方案');
|
||||
initAlbumFallbackLazyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 相册回退懒加载方案
|
||||
function initAlbumFallbackLazyLoading() {
|
||||
let scrollTimeout;
|
||||
|
||||
function checkImagesInView() {
|
||||
const images = document.querySelectorAll('img[data-image-id]:not(.loaded)');
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollTop = window.scrollY;
|
||||
|
||||
images.forEach(img => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
const isInView = rect.top < viewportHeight + 300 && rect.bottom > -300;
|
||||
|
||||
if (isInView) {
|
||||
const imageId = img.getAttribute('data-image-id');
|
||||
if (imageId) {
|
||||
imageLoader.loadImage(imageId, img);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(checkImagesInView, 100);
|
||||
});
|
||||
|
||||
// 初始检查
|
||||
checkImagesInView();
|
||||
}
|
||||
|
||||
// 显示图片骨架屏
|
||||
function showImageSkeleton(count = 8) {
|
||||
const container = document.getElementById('images-container');
|
||||
@@ -133,9 +314,10 @@ function displayImages() {
|
||||
<div class="image-item ${orientation}" onclick="openLightbox(${index})">
|
||||
<img
|
||||
src="${imageUrl}"
|
||||
${dataSrc ? `data-src="${dataSrc}"` : ''}
|
||||
alt="${title}"
|
||||
${image.id ? `data-image-id="${image.id}"` : ''}
|
||||
alt="${title}"
|
||||
class="lazy"
|
||||
loading="lazy"
|
||||
onerror="this.src='/assets/placeholder.svg'"
|
||||
>
|
||||
<div class="image-caption">
|
||||
@@ -230,7 +412,7 @@ function updateLightboxContent(newIndex) {
|
||||
// 在lightbox中优先使用原始图片以获得更好的显示效果
|
||||
let imageUrl = '/assets/placeholder.svg';
|
||||
if (image.id) {
|
||||
imageUrl = `/api/original/${image.id}`;
|
||||
imageUrl = `/api/image/${image.id}`;
|
||||
} else if (image.original_url) {
|
||||
imageUrl = image.original_url;
|
||||
}
|
||||
|
||||
189
assets/js/app.js
189
assets/js/app.js
@@ -13,6 +13,118 @@ let isInFavoritesPage = false; // 标记当前是否在收藏页
|
||||
// API基础URL
|
||||
const API_BASE = '/api';
|
||||
|
||||
// 图片加载器类
|
||||
class ImageLoader {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
// 加载并解压图片
|
||||
async loadImage(imageId, element) {
|
||||
// 检查缓存
|
||||
if (this.cache.has(imageId)) {
|
||||
const cachedData = this.cache.get(imageId);
|
||||
this.setImageSrc(element, cachedData.blobUrl, cachedData.mimeType);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/image/${imageId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentEncoding = response.headers.get('content-encoding');
|
||||
const originalContentType = response.headers.get('x-original-content-type');
|
||||
|
||||
let imageData;
|
||||
|
||||
if (contentEncoding === 'br') {
|
||||
// Brotli压缩数据,需要解压
|
||||
const compressedData = await response.arrayBuffer();
|
||||
imageData = await this.decompressBrotli(compressedData);
|
||||
} else {
|
||||
// 未压缩数据
|
||||
imageData = await response.blob();
|
||||
}
|
||||
|
||||
// 创建Blob URL
|
||||
const mimeType = originalContentType || contentType || 'image/jpeg';
|
||||
const blob = new Blob([imageData], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 缓存结果
|
||||
this.cache.set(imageId, { blobUrl, mimeType });
|
||||
|
||||
// 设置图片源
|
||||
this.setImageSrc(element, blobUrl, mimeType);
|
||||
|
||||
} catch (error) {
|
||||
console.error('图片加载失败:', error);
|
||||
// 加载失败时使用占位图
|
||||
element.src = '/assets/placeholder.svg';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置图片源
|
||||
setImageSrc(element, blobUrl, mimeType) {
|
||||
// 创建新的Image对象进行预加载
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
element.src = blobUrl;
|
||||
element.classList.add('loaded');
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('图片解码失败');
|
||||
element.src = '/assets/placeholder.svg';
|
||||
};
|
||||
img.src = blobUrl;
|
||||
}
|
||||
|
||||
// Brotli解压
|
||||
async decompressBrotli(compressedData) {
|
||||
// 检查浏览器是否支持Brotli解压
|
||||
if (window.TextDecoder && window.TextEncoder) {
|
||||
try {
|
||||
// 使用浏览器的Brotli解压(如果支持)
|
||||
if (window.DecompressionStream && 'br' in window) {
|
||||
const decompressionStream = new DecompressionStream('br');
|
||||
const decompressedStream = new Response(compressedData).body.pipeThrough(decompressionStream);
|
||||
return new Response(decompressedStream).arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('浏览器Brotli解压失败,使用JavaScript解压:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用JavaScript实现的Brotli解压(备用方案)
|
||||
return await this.decompressBrotliJS(compressedData);
|
||||
}
|
||||
|
||||
// JavaScript实现的Brotli解压(备用方案)
|
||||
async decompressBrotliJS(compressedData) {
|
||||
// 这里可以集成一个轻量级的Brotli解压库
|
||||
// 由于Brotli解压比较复杂,这里先返回原始数据
|
||||
// 在实际项目中,可以使用第三方库如brotli-wasm
|
||||
console.warn('Brotli解压未实现,返回原始数据');
|
||||
return compressedData;
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
clearCache() {
|
||||
// 清理所有Blob URL
|
||||
for (const [imageId, cached] of this.cache.entries()) {
|
||||
URL.revokeObjectURL(cached.blobUrl);
|
||||
}
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局图片加载器实例
|
||||
const imageLoader = new ImageLoader();
|
||||
|
||||
// 设备检测
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
@@ -68,6 +180,76 @@ function initInfiniteScroll() {
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化图片懒加载
|
||||
function initLazyLoading() {
|
||||
// 使用Intersection Observer实现懒加载
|
||||
if ('IntersectionObserver' in window) {
|
||||
if (lazyLoadObserver) {
|
||||
lazyLoadObserver.disconnect();
|
||||
}
|
||||
|
||||
lazyLoadObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
const imageId = img.getAttribute('data-image-id');
|
||||
|
||||
if (imageId && !img.classList.contains('loaded')) {
|
||||
imageLoader.loadImage(imageId, img);
|
||||
}
|
||||
|
||||
lazyLoadObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '100px 0px', // 提前100px开始加载
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
// 观察所有带有data-image-id的图片
|
||||
document.querySelectorAll('img[data-image-id]').forEach(img => {
|
||||
if (!img.classList.contains('loaded')) {
|
||||
lazyLoadObserver.observe(img);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 不支持Intersection Observer的回退方案
|
||||
console.warn('浏览器不支持Intersection Observer,使用回退懒加载方案');
|
||||
initFallbackLazyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 回退懒加载方案
|
||||
function initFallbackLazyLoading() {
|
||||
let scrollTimeout;
|
||||
|
||||
function checkImagesInView() {
|
||||
const images = document.querySelectorAll('img[data-image-id]:not(.loaded)');
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollTop = window.scrollY;
|
||||
|
||||
images.forEach(img => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
const isInView = rect.top < viewportHeight + 200 && rect.bottom > -200;
|
||||
|
||||
if (isInView) {
|
||||
const imageId = img.getAttribute('data-image-id');
|
||||
if (imageId) {
|
||||
imageLoader.loadImage(imageId, img);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(checkImagesInView, 100);
|
||||
});
|
||||
|
||||
// 初始检查
|
||||
checkImagesInView();
|
||||
}
|
||||
|
||||
// 处理滚动事件
|
||||
function handleScroll() {
|
||||
// 如果在收藏页,禁用无限滚动
|
||||
@@ -217,7 +399,7 @@ function appendAlbums(albums) {
|
||||
<div class="album-cover">
|
||||
<img
|
||||
src="${coverImageUrl}"
|
||||
${dataSrc ? `data-src="${dataSrc}"` : ''}
|
||||
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
|
||||
alt="${album.title}"
|
||||
class="lazy"
|
||||
onerror="this.src='/assets/placeholder.svg'"
|
||||
@@ -414,7 +596,7 @@ function displayAlbums(albums) {
|
||||
<div class="album-cover">
|
||||
<img
|
||||
src="${coverImageUrl}"
|
||||
${dataSrc ? `data-src="${dataSrc}"` : ''}
|
||||
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
|
||||
alt="${album.title}"
|
||||
class="lazy"
|
||||
onerror="this.src='/assets/placeholder.svg'"
|
||||
@@ -730,7 +912,8 @@ function displayFavorites(favoriteImages) {
|
||||
` : `${favoriteImages.map((fav, index) => `
|
||||
<div class="favorite-card" onclick="openFavoriteImage('${fav.favorite.album_id}')">
|
||||
<div class="card-image">
|
||||
<img src="${API_BASE}/image/${fav.image.id}"
|
||||
<img src="/assets/placeholder.svg"
|
||||
data-image-id="${fav.image.id}"
|
||||
alt="${fav.image.title || '收藏的图片'}"
|
||||
loading="lazy">
|
||||
<div class="card-overlay">
|
||||
|
||||
20
src/queue.rs
20
src/queue.rs
@@ -386,7 +386,7 @@ impl TaskQueue {
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
/// 启动定期清理过期任务
|
||||
pub async fn start_periodic_cleanup(self: Arc<Self>, interval_duration: Duration) {
|
||||
let queue = Arc::clone(&self);
|
||||
@@ -790,24 +790,6 @@ impl AlbumImportWorker {
|
||||
pub struct TaskFactory;
|
||||
|
||||
impl TaskFactory {
|
||||
/// 创建缩略图生成任务
|
||||
pub fn create_thumbnail_task(
|
||||
image_id: Uuid,
|
||||
original_data: Vec<u8>,
|
||||
priority: TaskPriority,
|
||||
) -> Task {
|
||||
Task {
|
||||
id: Uuid::new_v4(),
|
||||
task_type: TaskType::ThumbnailGeneration,
|
||||
priority,
|
||||
created_at: Instant::now(),
|
||||
data: TaskData::ThumbnailGeneration {
|
||||
image_id,
|
||||
original_data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建相册导入任务
|
||||
pub fn create_album_import_task(album_path: String, album_data: Option<Vec<u8>>) -> Task {
|
||||
Task {
|
||||
|
||||
@@ -384,7 +384,7 @@ pub async fn create_album(
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取原始图片(已移除缩略图功能)
|
||||
/// 获取压缩图片(后端压缩,前端解压)
|
||||
pub async fn get_image(
|
||||
db: web::Data<Arc<Database>>,
|
||||
image_processor: web::Data<Arc<ImageProcessor>>,
|
||||
@@ -406,11 +406,39 @@ pub async fn get_image(
|
||||
if original_path.exists() {
|
||||
match fs::read(&original_path) {
|
||||
Ok(data) => {
|
||||
HttpResponse::Ok()
|
||||
.content_type(image.mime_type.clone())
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(data)
|
||||
// 检查是否支持压缩(仅对JPEG/PNG图片进行压缩)
|
||||
let should_compress = image.mime_type == "image/jpeg" || image.mime_type == "image/png";
|
||||
|
||||
if should_compress {
|
||||
// 使用Brotli压缩图片数据
|
||||
match compress_image_data(&data) {
|
||||
Ok(compressed_data) => {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/octet-stream") // 压缩数据使用二进制格式
|
||||
.insert_header(("Content-Encoding", "br")) // Brotli压缩
|
||||
.insert_header(("X-Original-Content-Type", image.mime_type.clone())) // 保留原始类型信息
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000")) // 1年缓存
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(compressed_data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("图片压缩失败: {}", e);
|
||||
// 压缩失败时返回原始数据
|
||||
HttpResponse::Ok()
|
||||
.content_type(image.mime_type.clone())
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000"))
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不支持压缩的图片类型直接返回
|
||||
HttpResponse::Ok()
|
||||
.content_type(image.mime_type.clone())
|
||||
.insert_header(("Cache-Control", "public, max-age=31536000"))
|
||||
.insert_header(("ETag", format!("\"{}\"", image_id)))
|
||||
.body(data)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("读取原始图片失败: {}", e);
|
||||
@@ -435,4 +463,19 @@ pub async fn get_image(
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用Brotli压缩图片数据
|
||||
fn compress_image_data(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
use brotli::CompressorWriter;
|
||||
use std::io::Write;
|
||||
|
||||
let mut compressed = Vec::new();
|
||||
{
|
||||
let mut compressor = CompressorWriter::new(&mut compressed, 4096, 11, 22); // 高质量压缩
|
||||
Write::write_all(&mut compressor, data)?;
|
||||
compressor.flush()?;
|
||||
}
|
||||
|
||||
Ok(compressed)
|
||||
}
|
||||
Reference in New Issue
Block a user