feat: re-encode cover images for rockbox compatibility

This commit is contained in:
Markury
2026-06-03 20:39:20 -04:00
parent 2beae8e327
commit 3d7d3ded1c
3 changed files with 37 additions and 3 deletions

View File

@@ -37,6 +37,14 @@ fn read_audio_metadata(path: String) -> Result<metadata::AudioMetadata, String>
metadata::read_audio_metadata(&path) metadata::read_audio_metadata(&path)
} }
/// Re-encode cover image as baseline JPEG for broad player compatibility
#[tauri::command]
async fn reencode_cover_image(data: Vec<u8>, quality: u8) -> Result<Vec<u8>, String> {
tauri::async_runtime::spawn_blocking(move || tagger::reencode_cover_image(&data, quality))
.await
.map_err(|e| format!("Re-encode task failed: {}", e))?
}
/// Decrypt Deezer track data (legacy - kept for backwards compatibility) /// Decrypt Deezer track data (legacy - kept for backwards compatibility)
#[tauri::command] #[tauri::command]
async fn decrypt_deezer_track(data: Vec<u8>, track_id: String) -> Result<Vec<u8>, String> { async fn decrypt_deezer_track(data: Vec<u8>, track_id: String) -> Result<Vec<u8>, String> {
@@ -405,6 +413,7 @@ pub fn run() {
greet, greet,
tag_audio_file, tag_audio_file,
read_audio_metadata, read_audio_metadata,
reencode_cover_image,
decrypt_deezer_track, decrypt_deezer_track,
download_and_decrypt_track, download_and_decrypt_track,
device_sync::index_and_compare, device_sync::index_and_compare,

View File

@@ -2,8 +2,10 @@ use id3::{
frame::{Picture, PictureType}, frame::{Picture, PictureType},
Tag as ID3Tag, TagLike, Version, Tag as ID3Tag, TagLike, Version,
}; };
use image::codecs::jpeg::JpegEncoder;
use metaflac::Tag as FlacTag; use metaflac::Tag as FlacTag;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::path::Path; use std::path::Path;
/// Metadata structure for audio file tagging /// Metadata structure for audio file tagging
@@ -356,3 +358,20 @@ fn detect_mime_type_str(data: &[u8]) -> &'static str {
"image/jpeg" "image/jpeg"
} }
} }
/// Re-encode image data as a baseline (non-progressive) JPEG in sRGB.
/// This ensures compatibility with players like Rockbox that can't
/// decode progressive JPEGs or non-sRGB colorspaces.
pub fn reencode_cover_image(data: &[u8], quality: u8) -> Result<Vec<u8>, String> {
let img = image::load_from_memory(data)
.map_err(|e| format!("Failed to decode image: {}", e))?;
let rgb = img.to_rgb8();
let mut buf = Cursor::new(Vec::new());
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
rgb.write_with_encoder(encoder)
.map_err(|e| format!("Failed to encode JPEG: {}", e))?;
Ok(buf.into_inner())
}

View File

@@ -87,14 +87,20 @@ export async function downloadTrack(
// Get user settings // Get user settings
const appSettings = get(settings); const appSettings = get(settings);
// Download cover art if enabled // Download cover art if enabled, re-encode as baseline JPEG for player compatibility
let coverData: Uint8Array | undefined; let coverData: Uint8Array | undefined;
if ((appSettings.embedCoverArt || appSettings.saveCoverToFolder) && track.albumCoverUrl) { if ((appSettings.embedCoverArt || appSettings.saveCoverToFolder) && track.albumCoverUrl) {
try { try {
console.log('Downloading cover art...'); console.log('Downloading cover art...');
coverData = await downloadCover(track.albumCoverUrl); const rawCover = await downloadCover(track.albumCoverUrl);
const reencoded = await invoke<number[]>('reencode_cover_image', {
data: Array.from(rawCover),
quality: appSettings.coverImageQuality
});
coverData = new Uint8Array(reencoded);
console.log(`[ImageDownload] Re-encoded cover: ${rawCover.length} -> ${coverData.length} bytes (q=${appSettings.coverImageQuality})`);
} catch (error) { } catch (error) {
console.warn('Failed to download cover art:', error); console.warn('Failed to download/re-encode cover art:', error);
} }
} }