XenevaOS
Loading...
Searching...
No Matches
miniid3.h
Go to the documentation of this file.
1/*
2 * miniid3.h -- minimal single-header ID3v2 album-art extractor
3 *
4 * Supports:
5 * ID3v2.2 frame "PIC" (3-char ID, 3-byte big-endian size, 3-char image format)
6 * ID3v2.3 frame "APIC" (4-char ID, 4-byte big-endian size, MIME string)
7 * ID3v2.4 frame "APIC" (4-char ID, 4-byte synchsafe size -- with iTunes
8 * broken-big-endian fallback)
9 * Extended header skipping (ID3v2.3 / v2.4 flag bit 6)
10 * UTF-16 / UTF-16BE description double-null termination
11 * ID3v2.4 data-length-indicator prefix (frame flag bit 0)
12 *
13 * Usage:
14 * #define MINIID3_IMPLEMENTATION (in exactly ONE .c / .cpp)
15 * #include "miniid3.h"
16 *
17 * uint8_t* imgBuf = NULL;
18 * size_t imgLen = 0;
19 * if (miniid3_read_album_art("/music/song.mp3", &imgBuf, &imgLen)) {
20 * // imgBuf contains raw JPEG/PNG bytes
21 * free(imgBuf);
22 * }
23 *
24 * Public domain -- no warranty, use at your own risk.
25 */
26
27#ifndef MINIID3_H
28#define MINIID3_H
29
30#include <stdint.h>
31#include <stddef.h>
32
33#ifdef __cplusplus
34extern "C" {
35#endif
36
37/*
38 * miniid3_read_album_art
39 *
40 * Opens mp3_path, locates the first APIC (or PIC in v2.2) frame, and returns
41 * a malloc'd buffer containing the raw image bytes (JPEG or PNG).
42 *
43 * Returns 1 on success, 0 if no art was found or on any error.
44 * On success *out_buf must be free()'d by the caller.
45 */
46int miniid3_read_album_art(const char* mp3_path,
47 uint8_t** out_buf,
48 size_t* out_len);
49
50#ifdef __cplusplus
51}
52#endif
53
54#endif /* MINIID3_H */
55
56/* =========================================================================
57 IMPLEMENTATION
58 ========================================================================= */
59#ifdef MINIID3_IMPLEMENTATION
60
61#include <stdio.h>
62#include <stdlib.h>
63#include <string.h>
64
65/* ---- internal helpers -------------------------------------------------- */
66
67static uint32_t _id3_synchsafe(const uint8_t* p) {
68 return ((uint32_t)(p[0] & 0x7f) << 21)
69 | ((uint32_t)(p[1] & 0x7f) << 14)
70 | ((uint32_t)(p[2] & 0x7f) << 7)
71 | (uint32_t)(p[3] & 0x7f);
72}
73
74static uint32_t _id3_be32(const uint8_t* p) {
75 return ((uint32_t)p[0] << 24)
76 | ((uint32_t)p[1] << 16)
77 | ((uint32_t)p[2] << 8)
78 | (uint32_t)p[3];
79}
80
81static uint32_t _id3_be24(const uint8_t* p) {
82 return ((uint32_t)p[0] << 16)
83 | ((uint32_t)p[1] << 8)
84 | (uint32_t)p[2];
85}
86
87/*
88 * _id3_is_valid_jpeg -- check for JPEG SOI marker (0xFF 0xD8)
89 * Most embedded album art is JPEG; this is a quick sanity check before
90 * handing data to the JPEG decoder so we don't crash on garbage.
91 */
92static int _id3_is_valid_image(const uint8_t* p, size_t len) {
93 if (len < 4) return 0;
94 /* JPEG: FF D8 FF */
95 if (p[0] == 0xFF && p[1] == 0xD8 && p[2] == 0xFF) return 1;
96 /* PNG: 89 50 4E 47 */
97 if (p[0] == 0x89 && p[1] == 0x50 && p[2] == 0x4E && p[3] == 0x47) return 1;
98 return 0;
99}
100
101/*
102 * _id3_skip_desc -- advance *pp past the description field.
103 * encoding 0/3 (Latin-1 / UTF-8) → single null terminator
104 * encoding 1/2 (UTF-16 / UTF-16BE) → double null terminator (0x00 0x00)
105 * Returns 1 if a valid terminator was found before fend.
106 */
107static int _id3_skip_desc(uint8_t encoding, uint8_t** pp, const uint8_t* fend) {
108 uint8_t* p = *pp;
109 if (encoding == 0x01 || encoding == 0x02) {
110 /* UTF-16: step 2 bytes at a time looking for 0x00 0x00 */
111 while (p + 1 < fend) {
112 if (p[0] == 0 && p[1] == 0) { p += 2; break; }
113 p += 2;
114 }
115 } else {
116 /* Latin-1 / UTF-8: single null */
117 while (p < fend && *p) p++;
118 if (p < fend) p++; /* skip the null itself */
119 }
120 *pp = p;
121 return (p <= fend);
122}
123
124/* =========================================================================
125 Public API
126 ========================================================================= */
127int miniid3_read_album_art(const char* mp3_path,
128 uint8_t** out_buf,
129 size_t* out_len) {
130 *out_buf = NULL;
131 *out_len = 0;
132
133 FILE* f = fopen(mp3_path, "rb");
134 if (!f) return 0;
135
136 /* ---- 10-byte ID3v2 main header ---- */
137 uint8_t hdr[10];
138 if (fread(hdr, 1, 10, f) != (size_t)10) { fclose(f); return 0; }
139
140 /* Verify "ID3" magic */
141 if (hdr[0] != 'I' || hdr[1] != 'D' || hdr[2] != '3') { fclose(f); return 0; }
142
143 uint8_t ver = hdr[3]; /* major version: 2, 3, or 4 */
144 uint8_t flags = hdr[5];
145
146 /* Tag body size is always synchsafe in all ID3v2.x */
147 uint32_t tag_size = _id3_synchsafe(hdr + 6);
148 if (tag_size == 0) { fclose(f); return 0; }
149
150 uint8_t* tag = (uint8_t*)malloc(tag_size);
151 if (!tag) { fclose(f); return 0; }
152
153 if (fread(tag, 1, tag_size, f) != (size_t)tag_size) {
154 free(tag); fclose(f); return 0;
155 }
156 fclose(f);
157
158 uint32_t off = 0;
159
160 /* ---- Skip extended header (v2.3 / v2.4, flag bit 6) ---- */
161 if (ver >= 3 && (flags & 0x40) && off + 4 <= tag_size) {
162 uint32_t ext_size;
163 if (ver >= 4) {
164 /* v2.4: synchsafe size *includes* the 4 size bytes */
165 ext_size = _id3_synchsafe(tag + off);
166 off += ext_size;
167 } else {
168 /* v2.3: plain big-endian size, does NOT include the 4 bytes */
169 ext_size = _id3_be32(tag + off);
170 off += 4 + ext_size;
171 }
172 if (off > tag_size) { free(tag); return 0; }
173 }
174
175 /* ---- Per-version frame header geometry ----
176 * ID3v2.2: [3-char ID][3-byte size] = 6 bytes, no flags
177 * ID3v2.3+: [4-char ID][4-byte size][2 flags] = 10 bytes */
178 uint32_t fhdr_sz = (ver == 2) ? 6 : 10;
179
180 int found = 0;
181
182 while (!found && off + fhdr_sz <= tag_size) {
183 uint8_t* fh = tag + off;
184
185 /* Padding / end of frames */
186 if (fh[0] == 0) break;
187
188 uint32_t fsize = 0;
189 int is_pic = 0;
190
191 if (ver == 2) {
192 /* ID3v2.2 */
193 fsize = _id3_be24(fh + 3);
194 is_pic = (fh[0]=='P' && fh[1]=='I' && fh[2]=='C');
195 } else {
196 /* ID3v2.3 / v2.4
197 *
198 * IMPORTANT: Many taggers (iTunes, TagLib pre-2014, fre:ac) write
199 * ID3v2.4 files but store frame sizes as plain big-endian instead
200 * of synchsafe integers. We try synchsafe first; if that puts us
201 * past the tag boundary we fall back to plain big-endian. */
202 if (ver >= 4) {
203 fsize = _id3_synchsafe(fh + 4);
204 if (off + fhdr_sz + fsize > tag_size) {
205 /* Synchsafe size is invalid -- try big-endian fallback */
206 fsize = _id3_be32(fh + 4);
207 }
208 } else {
209 fsize = _id3_be32(fh + 4);
210 }
211 is_pic = (fh[0]=='A' && fh[1]=='P' && fh[2]=='I' && fh[3]=='C');
212 }
213
214 /* Sanity: frame must fit inside the tag */
215 if (off + fhdr_sz + fsize > tag_size) break;
216
217 if (is_pic) {
218 uint8_t* fdata = fh + fhdr_sz;
219 uint8_t* fend = fdata + fsize;
220 uint8_t* p = fdata;
221
222 /* ID3v2.4: data-length-indicator flag (frame flag byte [1], bit 0)
223 * When set, 4 synchsafe bytes of original data length prefix the
224 * actual APIC payload -- skip them. */
225 if (ver >= 4 && fhdr_sz == 10 && (fh[9] & 0x01)) {
226 p += 4;
227 }
228
229 if (p >= fend) break;
230
231 /* ---- Parse APIC / PIC body ----
232 *
233 * ID3v2.3/v2.4 APIC:
234 * [1] text encoding (0=Latin1, 1=UTF16, 2=UTF16BE, 3=UTF8)
235 * [n] MIME type, null-terminated ASCII e.g. "image/jpeg\0"
236 * [1] picture type (0x03 = front cover, but accept any)
237 * [m] description, null (or double-null) terminated
238 * [?] image data
239 *
240 * ID3v2.2 PIC:
241 * [1] text encoding
242 * [3] image format ("JPG" or "PNG", NOT null-terminated)
243 * [1] picture type
244 * [m] description
245 * [?] image data */
246
247 uint8_t encoding = *p++; /* save encoding for description field */
248
249 if (ver == 2) {
250 /* skip fixed 3-char format string */
251 p += 3;
252 } else {
253 /* skip variable-length null-terminated MIME type */
254 while (p < fend && *p) p++;
255 if (p < fend) p++; /* skip the null */
256 }
257
258 if (p >= fend) break;
259 p++; /* picture type byte */
260
261 /* skip description (encoding-aware null termination) */
262 if (!_id3_skip_desc(encoding, &p, fend)) break;
263
264 size_t img_len = (size_t)(fend - p);
265
266 /* Validate image signature before accepting */
267 if (img_len > 0 && _id3_is_valid_image(p, img_len)) {
268 uint8_t* img = (uint8_t*)malloc(img_len);
269 if (img) {
270 memcpy(img, p, img_len);
271 *out_buf = img;
272 *out_len = img_len;
273 found = 1;
274 }
275 } else if (img_len > 0) {
276 /* Image signature check failed.
277 * Some taggers prepend a UTF-16 BOM (0xFF 0xFE or 0xFE 0xFF)
278 * to the image data by mistake. Try skipping 2 bytes. */
279 if (img_len > 2 && _id3_is_valid_image(p + 2, img_len - 2)) {
280 p += 2;
281 img_len -= 2;
282 uint8_t* img = (uint8_t*)malloc(img_len);
283 if (img) {
284 memcpy(img, p, img_len);
285 *out_buf = img;
286 *out_len = img_len;
287 found = 1;
288 }
289 }
290 }
291 break; /* APIC found -- stop regardless of whether we got data */
292 }
293
294 off += fhdr_sz + fsize;
295 }
296
297 free(tag);
298 return found;
299}
300
301#endif /* MINIID3_IMPLEMENTATION */
302
uint64_t size_t
Definition stdint.h:104
uint8_t flags
Definition virtio.h:0
XE_LIB FILE * fopen(const char *name, const char *mode)
Definition stdio.cpp:44
XE_LIB int fclose(FILE *fp)
Definition stdio.cpp:226
XE_LIB size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
Definition stdio.cpp:96
unsigned char * img
Definition main.cpp:64
unsigned int uint32_t
Definition acefiex.h:163
unsigned char uint8_t
Definition acefiex.h:161
#define NULL
Definition actypes.h:561
const char * f
Definition drm_print.h:205
size_t len
Definition initial.c:23
void memcpy(void)
int miniid3_read_album_art(const char *mp3_path, uint8_t **out_buf, size_t *out_len)
NetRingHdr * hdr
Definition netproxy.c:39
#define malloc(s)
Definition quickjs.c:2290
#define free(p)
Definition quickjs.c:2291
Definition stdio.h:41