311 lines
12 KiB
Markdown
311 lines
12 KiB
Markdown
# PRD – Java‑native Bildkonvertierung (Spring Boot, OSS)
|
||
|
||
**Ziel:** Plattformneutrale (Linux/Windows, Docker/K8s) Java‑Komponente zur Konvertierung beliebiger Eingangsbildformate in ein konfigurierbares Zielformat (Standard: PNG), ohne native Abhängigkeiten oder OS‑Pakete.
|
||
|
||
---
|
||
|
||
## 1. Hintergrund & Motivation
|
||
|
||
- Ingest erhält Bilder aus heterogenen Quellen (JPEG, TIFF, PSD, PNM/TGA/IFF/ICNS, WebP, ggf. JP2 …).
|
||
- Kundenanforderung: *Keine* nativen Tools/OS‑Pakete; alles soll innerhalb des Java‑Artefakts laufen.
|
||
- ImageIO + Plugins (TwelveMonkeys, JP2/WebP) decken breite Formate ab. HEIC ist **out of scope** in dieser OSS‑Variante (kann später mit kommerziellen Libs ergänzt werden).
|
||
|
||
---
|
||
|
||
## 2. Scope
|
||
|
||
**In Scope**
|
||
|
||
- Java/Spring Boot‑Bibliothek + optionaler REST‑Controller.
|
||
- Übergabe **byte[]** (Quellbild), Liste **erlaubter Zielformate** und **präferiertes Zielformat**.
|
||
- Wenn Quellformat ∈ erlaubte Zielformate → **unverändert** zurückgeben.
|
||
- Sonst Konvertierung → **präferiertes Zielformat**.
|
||
- Optionaler Qualitätsparameter (0..1) für verlustbehaftete Formate (JPEG/WebP).
|
||
- Mehrseitige Formate (TIFF/ICO/animierte GIFs): mindestens **Frame 0**; API erweitertbar für `frameIndex`.
|
||
- SPI‑basierte Formatdetektion (ImageIO) ohne MIME‑Erkennung; Tika optional (Adapter, nicht standardmäßig aktiv).
|
||
|
||
**Out of Scope (Phase 1)**
|
||
|
||
- HEIC/HEIF, AVIF, JPEG‑XL (kann in Phase 2 über JDeli/Aspose Imaging ergänzt werden).
|
||
- Komplettes Metadaten‑/ICC‑Profile‑Roundtripping (nur baseline: übernehmen, sofern Writer/Format es unterstützt).
|
||
- Animations-Erhalt (z. B. GIF→GIF animiert). Standard: erstes Frame.
|
||
|
||
---
|
||
|
||
## 3. Ziele & Akzeptanzkriterien
|
||
|
||
**Funktional**
|
||
|
||
1. `negotiateAndConvert(byte[], allowed, preferred, quality)` liefert:
|
||
- **Unverändert**es Byte‑Array falls Quellformat∈allowed erkannt wird.
|
||
- Andernfalls konvertiert in `preferred` unter Beachtung des Quality‑Hints (falls anwendbar).
|
||
2. Erkennung des Quellformats ohne MIME‑Type ausschließlich über ImageIO/SPI.
|
||
3. Unterstützte Ziel‑Formate mindestens: **PNG, JPEG, GIF, BMP, TIFF**, optional **WEBP**, optional **JP2**.
|
||
|
||
**Nicht‑funktional**
|
||
|
||
- **Portabel**: läuft identisch auf **Linux** und **Windows** Containern.
|
||
- **Zero‑native**: keine OS‑Pakete, nur Java‑Abhängigkeiten.
|
||
- **Lieferform**: Spring‑Boot‑Fat‑JAR oder Layered‑JAR; Container‑Image ohne zusätzliche Systemlibs.
|
||
- **Performance**: Einzelbild‑Konvertierung < 150 ms (warm) für 2–8 MP JPEG→PNG auf x86\_64 Server‑HW (Anhaltswert).
|
||
- **Speicher**: hartes Limit pro Bild verifizierbar (z. B. 128 MB Heap‑Budget pro Request) via Größe‑/Dimension‑Checks.
|
||
|
||
---
|
||
|
||
## 4. Architektur & Design
|
||
|
||
### 4.1 Komponenten
|
||
|
||
- ``** (enum)**: hält `extension`, `mimeType`, `imageIoFormatName` (z. B. PNG→"png").
|
||
- ``: generische Konvertierung `InputStream → byte[]` in gewünschtes Zielformat; optional `frameIndex` & `quality`.
|
||
- ``: nimmt `byte[]` + `allowedTargets` + `preferredTarget`; nutzt ImageIO‑SPI, entscheidet Pass‑Through vs. Konvertierung.
|
||
- **(Optional) REST‑Controller**: Multipart‑Endpoint `/convert` und `/negotiate`.
|
||
|
||
### 4.2 Datenfluss
|
||
|
||
1. `negotiateAndConvert` erstellt `ImageInputStream` auf `byte[]`.
|
||
2. `ImageIO.getImageReaders(iis)` → erster `ImageReader` + `ImageReaderSpi` → Namen/Suffixe/MIMEs sammeln; Vergleich mit `allowedTargets`.
|
||
3. Wenn Match → Rückgabe `byte[]` unverändert. Sonst → `ImageConversionService.convert(..., preferred)`.
|
||
|
||
### 4.3 SPI & Plugins
|
||
|
||
- **ImageIO‑SPI** lädt Reader/Writer per `META-INF/services` der Abhängigkeiten.
|
||
- **TwelveMonkeys** erweitert u. a. TIFF/PSD/PNM/TGA/IFF/ICNS u. v. m.
|
||
- **JP2** über `jai-imageio-jpeg2000` (optional; Lizenzhinweis JJ2000 beachten).
|
||
- **WebP** über `org.sejda.imageio:webp-imageio` (optional).
|
||
|
||
### 4.4 Fehlermanagement
|
||
|
||
- **Technische Fehler**: `IOException` (I/O), `IndexOutOfBoundsException` (Frame), `IllegalArgumentException` (Parametrisierung) → REST: 4xx/5xx mapping.
|
||
- **Unsupported**: kein Reader/Writer gefunden → 415 (Unsupported Media Type) im REST‑Layer.
|
||
|
||
### 4.5 Sicherheit/Robustheit
|
||
|
||
- **Input‑Limits**: Max‑Bytes pro Upload (z. B. 32 MB), Max‑Dimension (z. B. 12k×12k), Max‑Frames.
|
||
- **ZIP/Decompress‑Bomb Schutz**: Ablehnung extrem großer Canvas/Pixelanzahlen; frühzeitiges Abbrechen.
|
||
- **Timeouts**: Konvertierung per `@Async`/Executor mit Timeout (Controller‑Schicht) optional.
|
||
- **Dependency Hygiene**: regelm. Updates (TwelveMonkeys/WebP/JP2), CVE‑Monitoring.
|
||
|
||
### 4.6 Observability & Logging
|
||
|
||
- **Micrometer**: Timer `image.convert`, Tags: `src.detected`, `dst.format`, `converted` (true/false), `exception`.
|
||
- **Structured Logging**: auf INFO: Zusammenfassung (ohne PII/Bilddaten), DEBUG: techn. Details.
|
||
|
||
---
|
||
|
||
## 5. Öffentliche API (Java)
|
||
|
||
### 5.1 Enum `OutputImageFormat`
|
||
|
||
- Felder: `defaultExtension:String`, `mimeType:String`, `imageIoFormatName:String`.
|
||
- Werte (Phase 1): `PNG, JPEG, GIF, BMP, TIFF` (+ optional `WEBP`, `JP2`).
|
||
- Methoden: `extension()`, `mimeType()`, `imageIoFormatName()`, `fromExtension(String)`.
|
||
|
||
### 5.2 `ImageConversionService`
|
||
|
||
```java
|
||
byte[] convert(InputStream in, OutputImageFormat target, Float quality) throws IOException;
|
||
byte[] convert(InputStream in, OutputImageFormat target, int frameIndex, Float quality) throws IOException;
|
||
```
|
||
|
||
### 5.3 `ImageFormatNegotiatorService`
|
||
|
||
```java
|
||
NegotiationResult negotiateAndConvert(byte[] sourceBytes,
|
||
List<OutputImageFormat> allowedTargets,
|
||
OutputImageFormat preferredTarget,
|
||
Float quality) throws IOException;
|
||
|
||
final class NegotiationResult {
|
||
byte[] data();
|
||
OutputImageFormat finalFormat();
|
||
boolean converted();
|
||
String detectedSourceFormat(); // optional
|
||
}
|
||
```
|
||
|
||
### 5.4 REST (optional)
|
||
|
||
- `POST /convert` (multipart: `file`, `target`, optional `quality`)
|
||
- `POST /negotiate` (multipart: `file`, `allowed`=CSV, `preferred`, optional `quality`)
|
||
- Responses: `200 OK` mit `Content-Type` gemäß Zielformat; `415` bei nicht unterstützten Quellen/Zielen.
|
||
|
||
### 5.5 CSV-Parser `ImageFormatParsers`
|
||
|
||
```java
|
||
List<OutputImageFormat> allowed = ImageFormatParsers.parseCsv(params.getAllowed());
|
||
OutputImageFormat preferred = ImageFormatParsers.lookup(params.getPreferred())
|
||
.orElseThrow(() -> new IllegalArgumentException("Unsupported preferred format"));
|
||
if (!allowed.contains(preferred)) {
|
||
throw new IllegalArgumentException("Preferred must be included in allowed");
|
||
}
|
||
List<OutputImageFormat> defaults = ImageFormatParsers.parseCsvLenient(env.getProperty("images.allowed-default"));
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Build & Packaging
|
||
|
||
- **Spring Boot**: `spring-boot-maven-plugin` → `repackage` Fat‑JAR (Nested JARs in `BOOT-INF/lib`).
|
||
- **Ohne Shade** empfohlen: SPI‑Files bleiben automatisch intakt.
|
||
- **Falls Shade unbedingt nötig**: `ServicesResourceTransformer` aktivieren (merge `META-INF/services`).
|
||
- **Container**: Base `eclipse-temurin:21-jre` (Linux) bzw. Windows‑Servercore‑Variante; nur JAR kopieren.
|
||
|
||
---
|
||
|
||
## 7. Abhängigkeiten (Beispiel‑POM, Versionen als Property)
|
||
|
||
- `com.twelvemonkeys.imageio:imageio-core`, `imageio-tiff`, `imageio-psd`, optional `imageio-pnm`, `imageio-tga`, `imageio-iff`, `imageio-icns` …
|
||
- **Optional JP2**: `com.github.jai-imageio:jai-imageio-core`, `jai-imageio-jpeg2000` (Lizenz prüfen)
|
||
- **Optional WebP**: `org.sejda.imageio:webp-imageio`
|
||
|
||
---
|
||
|
||
## 8. Konfiguration
|
||
|
||
`application.yml` (Beispiele):
|
||
|
||
```yaml
|
||
images:
|
||
allowed-default: [PNG, JPEG]
|
||
preferred-default: PNG
|
||
max-bytes: 33554432 # 32 MB
|
||
max-width: 12000
|
||
max-height: 12000
|
||
max-frames: 1
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Teststrategie
|
||
|
||
**Unit‑Tests**
|
||
|
||
- Parametrisierte Tests über alle `OutputImageFormat` Ziele (PNG/JPEG/GIF/BMP/TIFF/[WEBP]/[JP2]).
|
||
- Erkennungstests: Reader/SPI‑Detection für diverse Quellen (jpg/jpeg, tif/tiff, pnm/ppm/pgm, tga, icns, iff/ilbm, psd, png, gif, bmp, webp, jp2) – kleine Testbilder ins Repo (Lizenzfrei!).
|
||
- Negotiation‑Pfad: (a) pass‑through, (b) convert, (c) unsupported.
|
||
- Quality‑Pfad: JPEG/WebP Qualität 0.6/0.9.
|
||
- Fehlerpfade: korruptes Bild, leeres Byte‑Array, zu groß, falscher FrameIndex.
|
||
|
||
**Integration‑Tests**
|
||
|
||
- REST‑Endpoints mit Multipart; Content‑Type und Dateiname (Endung vom Enum) verifizieren.
|
||
|
||
**Performance‑Tests**
|
||
|
||
- Warmlauf + Messung (JMH oder simple Stopwatch) für typische Größen (2MP, 8MP).
|
||
|
||
---
|
||
|
||
## 10. Risiken & Gegenmaßnahmen
|
||
|
||
- **Plugin‑Abdeckung**: WebP/JP2 benötigen Zusatzmodule → klare Optionalität & Feature‑Flags.
|
||
- **Große Bilder/DoS**: harte Limits, frühzeitiger Abbruch.
|
||
- **Metadatenverlust**: dokumentieren; falls relevant, Folge‑Ticket für EXIF/ICC‑Handling.
|
||
- **Lizenzthemen JP2**: JJ2000‑Lizenz prüfen, Compliance aufnehmen.
|
||
|
||
---
|
||
|
||
## 11. Erweiterungen (Phase 2)
|
||
|
||
- **HEIC/HEIF**: via JDeli oder Aspose.Imaging (kommerziell), gleiche API beibehalten.
|
||
- **Mehrseiten‑Handling**: Seitenliste/Range, Animationserhalt für GIF/WebP.
|
||
- **Farbraum**: explizites ICC‑Profile‑Handling & Konvertierung.
|
||
|
||
---
|
||
|
||
## 12. Umsetzungsfahrplan (für Codex)
|
||
|
||
1. **Enum** `OutputImageFormat` anlegen (PNG/JPEG/GIF/BMP/TIFF + optional WEBP/JP2).
|
||
2. **Service** `ImageConversionService` implementieren (Reader→BufferedImage→Writer; Quality nur wenn `canWriteCompressed`).
|
||
3. **Negotiator** `ImageFormatNegotiatorService` implementieren (SPI‑Detection via `ImageReaderSpi`: Namen/Suffixe/MIMEs; Pass‑Through vs. Convert).
|
||
4. **Controller** (optional) + DTOs + Validation.
|
||
5. **POM**: TwelveMonkeys‑Module + optional WebP/JP2.
|
||
6. **Tests**: Unit + Integration + Beispielbilder.
|
||
7. **Observability**: Micrometer‑Timer & Logs.
|
||
8. **Dockerfile** (Linux), optional Windows‑Image; K8s‑Manifest‑Snippets (Resources/Limits/Probes).
|
||
|
||
---
|
||
|
||
## 13. Akzeptanz‑Checkliste (DoD)
|
||
|
||
-
|
||
|
||
---
|
||
|
||
## 14. Anhang – Code‑Skeletons (Kurz)
|
||
|
||
**Enum** (Auszug)
|
||
|
||
```java
|
||
enum OutputImageFormat {
|
||
PNG("png", "image/png", "png"),
|
||
JPEG("jpg", "image/jpeg", "jpeg"),
|
||
GIF("gif", "image/gif", "gif"),
|
||
BMP("bmp", "image/bmp", "bmp"),
|
||
TIFF("tiff", "image/tiff", "tiff"),
|
||
WEBP("webp", "image/webp", "webp"), // optional
|
||
JP2("jp2", "image/jp2", "jpeg2000"); // optional
|
||
// getter + fromExtension
|
||
}
|
||
```
|
||
|
||
**ImageConversionService** (Signaturen siehe §5.2) – BufferedImage lesen/schreiben; `ImageWriteParam` nur bei `canWriteCompressed()` + `setCompressionQuality(quality)`.
|
||
|
||
**Negotiator** – `ImageReaderSpi` inspizieren (`getFormatNames()`, `getFileSuffixes()`, `getMIMETypes()`), tolerante Matches für jpg/jpeg & tif/tiff.
|
||
|
||
**POM Snippets**
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||
<artifactId>imageio-core</artifactId>
|
||
<version>${twelvemonkeys.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||
<artifactId>imageio-tiff</artifactId>
|
||
<version>${twelvemonkeys.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||
<artifactId>imageio-psd</artifactId>
|
||
<version>${twelvemonkeys.version}</version>
|
||
</dependency>
|
||
<!-- optional: PNM/TGA/IFF/ICNS ... -->
|
||
<!-- optional JP2 -->
|
||
<dependency>
|
||
<groupId>com.github.jai-imageio</groupId>
|
||
<artifactId>jai-imageio-core</artifactId>
|
||
<version>${jai.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.github.jai-imageio</groupId>
|
||
<artifactId>jai-imageio-jpeg2000</artifactId>
|
||
<version>${jai.version}</version>
|
||
</dependency>
|
||
<!-- optional WebP -->
|
||
<dependency>
|
||
<groupId>org.sejda.imageio</groupId>
|
||
<artifactId>webp-imageio</artifactId>
|
||
<version>${webp.version}</version>
|
||
</dependency>
|
||
```
|
||
|
||
**Build**
|
||
|
||
- `spring-boot-maven-plugin` → `repackage` Fat‑JAR.
|
||
- (Nur falls Shade) `ServicesResourceTransformer` zum Mergen der `META-INF/services`.
|
||
|
||
**Docker (Linux, Beispiel)**
|
||
|
||
```dockerfile
|
||
FROM eclipse-temurin:21-jre
|
||
WORKDIR /opt/app
|
||
COPY target/app.jar app.jar
|
||
ENTRYPOINT ["java","-jar","/opt/app/app.jar"]
|
||
```
|
||
|
||
— Ende —
|
||
|