Skip to content

Commit 54617e0

Browse files
committed
Add Image Transcode And New Format Support
* Add image-to-image conversion support for PNG, JPG, WEBP, BMP, TIFF, HEIC, and HEIF * Register new conversion routes in the conversion hub * Extend probing logic with stricter format detection for WEBP, BMP, TIFF, HEIC, HEIF, and AVIF * Improve MIME validation, stream signature checks, and metadata tag parsing * Introduce codec wrapper classes for MP3, OGG, WAV, MP4, and image codecs * Centralize decode entry points to standardize media handling * Refactor internal codec initialization flow for better extensibility * Expand test coverage for new image formats and validation paths * Update documentation to reflect new features and current audio conversion limitations
1 parent 2fa2ed1 commit 54617e0

26 files changed

+1492
-29
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ CodecMedia is a Java library for media probing, validation, metadata sidecar per
1515
- WAV (RIFF/WAVE)
1616
- PNG
1717
- JPEG
18+
- WebP
19+
- BMP
20+
- TIFF
21+
- HEIC/HEIF/AVIF (basic BMFF parsing)
1822
- MP4 (basic ISO BMFF parsing)
19-
- Validation with size limits and strict parser checks for MP3/OGG/WAV/PNG/JPEG/MP4
23+
- Validation with size limits and strict parser checks for MP3/OGG/WAV/PNG/JPEG/WebP/BMP/TIFF/HEIC/HEIF/AVIF/MP4
2024
- Metadata read/write with sidecar persistence (`.codecmedia.properties`)
21-
- In-Java extraction and conversion file operations (no external transcoder required for current stub flows)
25+
- In-Java extraction and conversion file operations
26+
- Image-to-image conversion in Java for: `png`, `jpg`/`jpeg`, `webp`, `bmp`, `tif`/`tiff`, `heic`/`heif`
2227
- Playback API with dry-run support and optional desktop-open backend
2328
- Conversion hub routing with explicit unsupported routes and a stub `wav <-> pcm` path
2429

@@ -37,7 +42,9 @@ CodecMedia is a Java library for media probing, validation, metadata sidecar per
3742

3843
- Current probing focuses on **technical media info** (mime/type/streams/basic tags).
3944
- `readMetadata` currently uses sidecar metadata persistence; it is **not** a full embedded tag extractor (for example ID3 album art/APIC).
40-
- Current audio-to-audio conversion is mostly unsupported except a temporary stub copy route for `wav <-> pcm`.
45+
- Audio-to-audio conversion is not implemented yet for real transcode cases (for example `mp3 -> ogg`).
46+
- The only temporary audio conversion path is a stub `wav <-> pcm` route.
47+
- For OpenAL workflows that require OGG from MP3 input, use an external transcoder first (for example ffmpeg), then play the produced OGG.
4148

4249
## Requirements
4350

@@ -82,3 +89,7 @@ mvn test
8289
## License
8390

8491
This project is licensed under the Apache License 2.0.
92+
93+
---
94+
95+
*by TamKungZ_*

icon.png

22.4 KB
Loading

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>me.tamkungz.codecmedia</groupId>
99
<artifactId>codecmedia</artifactId>
10-
<version>1.0.0</version>
10+
<version>1.0.1</version>
1111
<packaging>jar</packaging>
1212

1313
<name>CodecMedia</name>

src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,31 @@
1616

1717
import me.tamkungz.codecmedia.CodecMediaEngine;
1818
import me.tamkungz.codecmedia.CodecMediaException;
19+
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3Codec;
1920
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3Parser;
2021
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3ProbeInfo;
22+
import me.tamkungz.codecmedia.internal.audio.ogg.OggCodec;
2123
import me.tamkungz.codecmedia.internal.audio.ogg.OggParser;
2224
import me.tamkungz.codecmedia.internal.audio.ogg.OggProbeInfo;
25+
import me.tamkungz.codecmedia.internal.audio.wav.WavCodec;
2326
import me.tamkungz.codecmedia.internal.audio.wav.WavParser;
2427
import me.tamkungz.codecmedia.internal.audio.wav.WavProbeInfo;
2528
import me.tamkungz.codecmedia.internal.convert.ConversionHub;
2629
import me.tamkungz.codecmedia.internal.convert.ConversionRequest;
2730
import me.tamkungz.codecmedia.internal.convert.DefaultConversionHub;
31+
import me.tamkungz.codecmedia.internal.image.bmp.BmpParser;
32+
import me.tamkungz.codecmedia.internal.image.bmp.BmpProbeInfo;
33+
import me.tamkungz.codecmedia.internal.image.heif.HeifParser;
34+
import me.tamkungz.codecmedia.internal.image.heif.HeifProbeInfo;
2835
import me.tamkungz.codecmedia.internal.image.jpeg.JpegParser;
2936
import me.tamkungz.codecmedia.internal.image.jpeg.JpegProbeInfo;
3037
import me.tamkungz.codecmedia.internal.image.png.PngParser;
3138
import me.tamkungz.codecmedia.internal.image.png.PngProbeInfo;
39+
import me.tamkungz.codecmedia.internal.image.tiff.TiffParser;
40+
import me.tamkungz.codecmedia.internal.image.tiff.TiffProbeInfo;
41+
import me.tamkungz.codecmedia.internal.image.webp.WebpParser;
42+
import me.tamkungz.codecmedia.internal.image.webp.WebpProbeInfo;
43+
import me.tamkungz.codecmedia.internal.video.mp4.Mp4Codec;
3244
import me.tamkungz.codecmedia.internal.video.mp4.Mp4Parser;
3345
import me.tamkungz.codecmedia.internal.video.mp4.Mp4ProbeInfo;
3446
import me.tamkungz.codecmedia.model.ConversionResult;
@@ -70,7 +82,7 @@ public ProbeResult probe(Path input) throws CodecMediaException {
7082
if ("mp3".equals(extension) || isLikelyMp3(bytes)) {
7183
if (bytes.length >= 4) {
7284
try {
73-
Mp3ProbeInfo info = Mp3Parser.parse(bytes);
85+
Mp3ProbeInfo info = Mp3Codec.decode(bytes, input);
7486
return new ProbeResult(
7587
input,
7688
"audio/mpeg",
@@ -91,7 +103,7 @@ public ProbeResult probe(Path input) throws CodecMediaException {
91103
}
92104

93105
if ("ogg".equals(extension) || isLikelyOgg(bytes)) {
94-
OggProbeInfo info = OggParser.parse(bytes);
106+
OggProbeInfo info = OggCodec.decode(bytes, input);
95107
return new ProbeResult(
96108
input,
97109
"audio/ogg",
@@ -108,7 +120,7 @@ public ProbeResult probe(Path input) throws CodecMediaException {
108120

109121
if ("wav".equals(extension) || WavParser.isLikelyWav(bytes)) {
110122
try {
111-
WavProbeInfo info = WavParser.parse(bytes);
123+
WavProbeInfo info = WavCodec.decode(bytes, input);
112124
return new ProbeResult(
113125
input,
114126
"audio/wav",
@@ -169,12 +181,118 @@ public ProbeResult probe(Path input) throws CodecMediaException {
169181
return new ProbeResult(input, "image/jpeg", outputExt, MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
170182
}
171183

184+
if ("webp".equals(extension) || WebpParser.isLikelyWebp(bytes)) {
185+
try {
186+
WebpProbeInfo info = WebpParser.parse(bytes);
187+
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
188+
tags.put("sizeBytes", String.valueOf(size));
189+
if (info.bitDepth() != null) {
190+
tags.put("bitDepth", String.valueOf(info.bitDepth()));
191+
}
192+
return new ProbeResult(
193+
input,
194+
"image/webp",
195+
"webp",
196+
MediaType.IMAGE,
197+
null,
198+
List.of(new StreamInfo(0, StreamKind.VIDEO, "webp", null, null, null, info.width(), info.height(), null)),
199+
tags
200+
);
201+
} catch (CodecMediaException ignored) {
202+
// Fall back to extension-only probe for malformed/partial files.
203+
}
204+
return new ProbeResult(input, "image/webp", "webp", MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
205+
}
206+
207+
if ("bmp".equals(extension) || BmpParser.isLikelyBmp(bytes)) {
208+
try {
209+
BmpProbeInfo info = BmpParser.parse(bytes);
210+
return new ProbeResult(
211+
input,
212+
"image/bmp",
213+
"bmp",
214+
MediaType.IMAGE,
215+
null,
216+
List.of(new StreamInfo(0, StreamKind.VIDEO, "bmp", null, null, null, info.width(), info.height(), null)),
217+
Map.of(
218+
"sizeBytes", String.valueOf(size),
219+
"bitsPerPixel", String.valueOf(info.bitsPerPixel())
220+
)
221+
);
222+
} catch (CodecMediaException ignored) {
223+
// Fall back to extension-only probe for malformed/partial files.
224+
}
225+
return new ProbeResult(input, "image/bmp", "bmp", MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
226+
}
227+
228+
if ("tif".equals(extension) || "tiff".equals(extension) || TiffParser.isLikelyTiff(bytes)) {
229+
String outputExt = "tiff".equals(extension) ? "tiff" : "tif";
230+
try {
231+
TiffProbeInfo info = TiffParser.parse(bytes);
232+
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
233+
tags.put("sizeBytes", String.valueOf(size));
234+
if (info.bitDepth() != null) {
235+
tags.put("bitDepth", String.valueOf(info.bitDepth()));
236+
}
237+
return new ProbeResult(
238+
input,
239+
"image/tiff",
240+
outputExt,
241+
MediaType.IMAGE,
242+
null,
243+
List.of(new StreamInfo(0, StreamKind.VIDEO, "tiff", null, null, null, info.width(), info.height(), null)),
244+
tags
245+
);
246+
} catch (CodecMediaException ignored) {
247+
// Fall back to extension-only probe for malformed/partial files.
248+
}
249+
return new ProbeResult(input, "image/tiff", outputExt, MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
250+
}
251+
252+
if ("heic".equals(extension) || "heif".equals(extension) || "avif".equals(extension) || HeifParser.isLikelyHeif(bytes)) {
253+
String outputExt = "heif".equals(extension) ? "heif" : "heic";
254+
if ("avif".equals(extension)) {
255+
outputExt = "avif";
256+
}
257+
String mimeType = "image/" + outputExt;
258+
try {
259+
HeifProbeInfo info = HeifParser.parse(bytes);
260+
String majorBrand = info.majorBrand();
261+
if ("avif".equals(majorBrand) || "avis".equals(majorBrand)) {
262+
outputExt = "avif";
263+
mimeType = "image/avif";
264+
}
265+
java.util.List<StreamInfo> streams = List.of();
266+
if (info.width() != null && info.height() != null) {
267+
streams = List.of(new StreamInfo(0, StreamKind.VIDEO, outputExt, null, null, null, info.width(), info.height(), null));
268+
}
269+
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
270+
tags.put("sizeBytes", String.valueOf(size));
271+
tags.put("majorBrand", majorBrand);
272+
if (info.bitDepth() != null) {
273+
tags.put("bitDepth", String.valueOf(info.bitDepth()));
274+
}
275+
return new ProbeResult(
276+
input,
277+
mimeType,
278+
outputExt,
279+
MediaType.IMAGE,
280+
null,
281+
streams,
282+
tags
283+
);
284+
} catch (CodecMediaException ignored) {
285+
// Fall back to extension-only probe for malformed/partial files.
286+
}
287+
return new ProbeResult(input, mimeType, outputExt, MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
288+
}
289+
172290
if ("mp4".equals(extension) || "m4a".equals(extension) || Mp4Parser.isLikelyMp4(bytes)) {
173291
String outputExt = "m4a".equals(extension) ? "m4a" : "mp4";
174292
String mimeType = "m4a".equals(outputExt) ? "audio/mp4" : "video/mp4";
175293
MediaType mediaType = "m4a".equals(outputExt) ? MediaType.AUDIO : MediaType.VIDEO;
176294
try {
177-
Mp4ProbeInfo info = Mp4Parser.parse(bytes);
295+
Mp4ProbeInfo info = Mp4Codec.decode(bytes, input);
178296
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
179297
tags.put("sizeBytes", String.valueOf(size));
180298
if (info.majorBrand() != null && !info.majorBrand().isBlank()) {
@@ -211,12 +329,18 @@ public ProbeResult probe(Path input) throws CodecMediaException {
211329
case "wav" -> "audio/wav";
212330
case "png" -> "image/png";
213331
case "jpg", "jpeg" -> "image/jpeg";
332+
case "webp" -> "image/webp";
333+
case "bmp" -> "image/bmp";
334+
case "tif", "tiff" -> "image/tiff";
335+
case "heic" -> "image/heic";
336+
case "heif" -> "image/heif";
337+
case "avif" -> "image/avif";
214338
default -> "application/octet-stream";
215339
};
216340
MediaType mediaType = switch (extension) {
217341
case "mp4" -> MediaType.VIDEO;
218342
case "m4a", "mp3", "ogg", "wav" -> MediaType.AUDIO;
219-
case "png", "jpg", "jpeg" -> MediaType.IMAGE;
343+
case "png", "jpg", "jpeg", "webp", "bmp", "tif", "tiff", "heic", "heif", "avif" -> MediaType.IMAGE;
220344
default -> MediaType.UNKNOWN;
221345
};
222346

@@ -441,6 +565,30 @@ public ValidationResult validate(Path input, ValidationOptions options) {
441565
} catch (CodecMediaException e) {
442566
return new ValidationResult(false, List.of(), List.of("Strict validation failed for " + extension + ": " + e.getMessage()));
443567
}
568+
} else if ("webp".equals(extension)) {
569+
try {
570+
WebpParser.parse(bytes);
571+
} catch (CodecMediaException e) {
572+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for webp: " + e.getMessage()));
573+
}
574+
} else if ("bmp".equals(extension)) {
575+
try {
576+
BmpParser.parse(bytes);
577+
} catch (CodecMediaException e) {
578+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for bmp: " + e.getMessage()));
579+
}
580+
} else if ("tif".equals(extension) || "tiff".equals(extension)) {
581+
try {
582+
TiffParser.parse(bytes);
583+
} catch (CodecMediaException e) {
584+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for tif/tiff: " + e.getMessage()));
585+
}
586+
} else if ("heic".equals(extension) || "heif".equals(extension) || "avif".equals(extension)) {
587+
try {
588+
HeifParser.parse(bytes);
589+
} catch (CodecMediaException e) {
590+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for heic/heif/avif: " + e.getMessage()));
591+
}
444592
}
445593
}
446594

@@ -504,7 +652,7 @@ private static MediaType mediaTypeByExtension(String extension) {
504652
return switch (normalizeExtension(extension)) {
505653
case "mp3", "ogg", "wav", "pcm", "m4a", "aac", "flac" -> MediaType.AUDIO;
506654
case "mp4", "mkv", "mov", "avi", "webm" -> MediaType.VIDEO;
507-
case "png", "jpg", "jpeg", "gif", "bmp", "webp" -> MediaType.IMAGE;
655+
case "png", "jpg", "jpeg", "gif", "bmp", "webp", "tif", "tiff", "heic", "heif", "avif" -> MediaType.IMAGE;
508656
default -> MediaType.UNKNOWN;
509657
};
510658
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package me.tamkungz.codecmedia.internal.audio.mp3;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
7+
import me.tamkungz.codecmedia.CodecMediaException;
8+
9+
public final class Mp3Codec {
10+
11+
private Mp3Codec() {
12+
}
13+
14+
public static Mp3ProbeInfo decode(Path input) throws CodecMediaException {
15+
try {
16+
byte[] bytes = Files.readAllBytes(input);
17+
return decode(bytes, input);
18+
} catch (IOException e) {
19+
throw new CodecMediaException("Failed to decode MP3: " + input, e);
20+
}
21+
}
22+
23+
public static Mp3ProbeInfo decode(byte[] bytes, Path sourceRef) throws CodecMediaException {
24+
Mp3ProbeInfo info = Mp3Parser.parse(bytes);
25+
validateDecodedProbe(info, sourceRef);
26+
return info;
27+
}
28+
29+
public static void encode(byte[] encodedMp3Data, Path output) throws CodecMediaException {
30+
if (encodedMp3Data == null || encodedMp3Data.length == 0) {
31+
throw new CodecMediaException("MP3 encoded data is empty");
32+
}
33+
try {
34+
Files.write(output, encodedMp3Data);
35+
} catch (IOException e) {
36+
throw new CodecMediaException("Failed to encode MP3: " + output, e);
37+
}
38+
}
39+
40+
private static void validateDecodedProbe(Mp3ProbeInfo info, Path input) throws CodecMediaException {
41+
if (info.sampleRate() <= 0 || info.channels() <= 0) {
42+
throw new CodecMediaException("Decoded MP3 has invalid stream values: " + input);
43+
}
44+
}
45+
}
46+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package me.tamkungz.codecmedia.internal.audio.ogg;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
7+
import me.tamkungz.codecmedia.CodecMediaException;
8+
9+
public final class OggCodec {
10+
11+
private OggCodec() {
12+
}
13+
14+
public static OggProbeInfo decode(Path input) throws CodecMediaException {
15+
try {
16+
byte[] bytes = Files.readAllBytes(input);
17+
return decode(bytes, input);
18+
} catch (IOException e) {
19+
throw new CodecMediaException("Failed to decode OGG: " + input, e);
20+
}
21+
}
22+
23+
public static OggProbeInfo decode(byte[] bytes, Path sourceRef) throws CodecMediaException {
24+
OggProbeInfo info = OggParser.parse(bytes);
25+
validateDecodedProbe(info, sourceRef);
26+
return info;
27+
}
28+
29+
public static void encode(byte[] encodedOggData, Path output) throws CodecMediaException {
30+
if (encodedOggData == null || encodedOggData.length == 0) {
31+
throw new CodecMediaException("OGG encoded data is empty");
32+
}
33+
try {
34+
Files.write(output, encodedOggData);
35+
} catch (IOException e) {
36+
throw new CodecMediaException("Failed to encode OGG: " + output, e);
37+
}
38+
}
39+
40+
private static void validateDecodedProbe(OggProbeInfo info, Path input) throws CodecMediaException {
41+
if (info.sampleRate() <= 0 || info.channels() <= 0) {
42+
throw new CodecMediaException("Decoded OGG has invalid stream values: " + input);
43+
}
44+
}
45+
}
46+

0 commit comments

Comments
 (0)