Dynamische Bildverarbeitung in Scala mit ImageMagick
Rufen Sie ImageMagick aus Scala auf, um die auf der Befehlszeile angegebenen Bilder zu skalieren und mit Wasserzeichen zu versehen. Das folgende Programm schreibt pro Eingabe eine PNG-Datei in ein neues Verzeichnis, behält Ihre Originale und gibt einen Status ungleich null zurück, wenn ein Bild fehlschlägt. Erfolgreiche Ergebnisse bleiben verfügbar, auch wenn andere Bilder im Stapel fehlschlagen.
Diese Anleitung konzentriert sich auf die Prozessintegration in Scala und die Fehlerbehandlung bei der Stapelverarbeitung. Um mehrere Bildbreiten und ein Ausweichformat für eine Website zu erzeugen, folgen Sie der separaten Anleitung für responsive Bilder.
Ein kleines Scala-Projekt einrichten
Verwenden Sie Linux, Bash, cURL, eine JDK-21-Installation und
ImageMagick 7.1.2-31 mit Unterstützung für JPEG, PNG und FreeType.
Dieses Beispiel wurde mit OpenJDK 21.0.12.1, Scala 2.13.18 und ImageMagick 7.1.2-31 Q16-HDRI getestet.
Scala 2.13.18 ist die aktuelle Scala-2.13-Version und
unterstützt JDK 21.
Der Einstiegspunkt convert von ImageMagick 6 und native Windows-Shells
werden in dieser Anleitung nicht behandelt.
Sie benötigen außerdem eine lesbare TrueType-Schriftdatei. Der Befehl zum Ausführen verwendet
/usr/share/fonts/liberation/LiberationSans-Regular.ttf; ersetzen Sie diesen Pfad durch den vollständigen Pfad Ihrer
Schriftdatei, falls Ihre Distribution sie an anderer Stelle installiert. Fügen Sie die Shell-Blöcke
bei deaktiviertem errexit in Bash ein, damit Sie bei einem fehlgeschlagenen
Befehl zur Eingabeaufforderung zurückkehren. Jeder Block lässt das Verzeichnis und die
Shell-Optionen des Aufrufers unverändert.
java -version && magick -version && magick -list format
Prüfen Sie, ob JPEG und PNG in der Formatliste
Lese- und Schreibunterstützung haben. Beginnen Sie in einem beschreibbaren Arbeitsverzeichnis.
Dieser Block erstellt ein neues Projekt namens scala-images und lädt die drei
Scala-JAR-Dateien in den festgelegten Versionen von Maven Central herunter. Ein vorhandenes
Projektverzeichnis wird abgelehnt; wählen Sie ein anderes Arbeitsverzeichnis, wenn dieser Name
bereits belegt ist.
(
mkdir scala-images || exit 1
cd scala-images || exit 1
mkdir lib classes inputs || exit 1
for artifact in scala-library scala-reflect scala-compiler; do
curl -fsSLo "lib/$artifact-2.13.18.jar" \
"https://repo.maven.apache.org/maven2/org/scala-lang/$artifact/2.13.18/$artifact-2.13.18.jar" || exit 1
done
java -cp 'lib/*' scala.tools.nsc.Main -version
)
Dies sind der Compiler, die Laufzeitbibliothek und die Reflection-Bibliothek für Scala 2.13.18. Das Projekt verwendet weder einen sbt-Build noch Scala-Abhängigkeiten von Drittanbietern. Seine expliziten Klassenpfade funktionieren auch innerhalb eines bestehenden sbt-Projekts, ohne dessen Build-Einstellungen zu lesen oder ihm Abhängigkeiten hinzuzufügen.
Das vollständige Programm zur Stapelverarbeitung speichern
Speichern Sie dies als scala-images/ImageBatch.scala. Die Argumente sind ein neues
Ausgabeverzeichnis, ein Schriftdateipfad, ein Wasserzeichentext und ein oder mehrere lokale Pfade
zu nicht animierten JPEG- oder PNG-Bildern. Der Text darf bis zu 40 ASCII-Buchstaben, Ziffern,
Leerzeichen, Punkte, Unterstriche und Bindestriche enthalten und muss mit einem Buchstaben oder
einer Ziffer beginnen. So bleiben die Text-Escapesequenzen und die Dateilesesyntax von ImageMagick
aus dem Text ausgeschlossen.
Der Ausgabename enthält den vollständigen Eingabedateinamen: Aus photo.jpg
wird photo.jpg.png.
Zwei Eingaben mit demselben Basisnamen werden abgelehnt, bevor das Ausgabeverzeichnis erstellt wird.
Die Eingaben werden in der Reihenfolge der Argumente verarbeitet, auch mit den Erweiterungen
.jpg, .jpeg und .png
in gemischter Groß- und Kleinschreibung.
import java.io.IOException
import java.nio.file.{Files, Path, Paths}
import java.util.Locale
import scala.sys.process.{Process, ProcessLogger}
import scala.util.control.NonFatal
object ImageBatch {
private def command(arguments: Seq[String], directory: Path): String = {
val output = new StringBuilder
val errors = new StringBuilder
val status = Process(arguments, directory.toFile).!(ProcessLogger(
line => { output.append(line).append('\n'); () },
line => { errors.append(line).append('\n'); () }
))
if (status != 0 || errors.nonEmpty) {
throw new IOException(s"ImageMagick status $status: ${errors.toString.trim}")
}
output.toString
}
private def processImage(input: Path, output: Path, font: Path, label: String): Boolean = {
try {
val name = input.getFileName.toString.toLowerCase(Locale.ROOT)
if (!Seq(".jpg", ".jpeg", ".png").exists(name.endsWith)) {
throw new IOException("Expected a .jpg, .jpeg, or .png filename")
}
if (!Files.isRegularFile(input) || !Files.isReadable(input)) {
throw new IOException("Not a readable regular file")
}
val stage = Files.createTempDirectory(output, ".work-")
try {
// Ordinary private names prevent ImageMagick from interpreting the reader's paths.
Files.copy(input, stage.resolve("source"))
Files.copy(font, stage.resolve("font.ttf"))
val formats = command(Seq("magick", "identify", "-regard-warnings",
"-limit", "thread", "1", "-format", "%m\n", "source"), stage)
.linesIterator.toVector
if (formats != Vector("JPEG") && formats != Vector("PNG")) {
throw new IOException("Expected one still JPEG or PNG image")
}
command(Seq(
"magick", "-regard-warnings", "-limit", "thread", "1", "source",
"-auto-orient", "-colorspace", "sRGB", "-resize", "800x600>",
"-background", "white", "-alpha", "remove", "-alpha", "off",
"-font", "font.ttf", "-pointsize", "24", "-gravity", "southeast",
"-fill", "white", "-stroke", "black", "-strokewidth", "1",
"-annotate", "+12+12", label, "-strip", "PNG24:result.png"
), stage)
Files.move(stage.resolve("result.png"), output.resolve(input.getFileName.toString + ".png"))
} finally {
val children = Files.list(stage)
try children.forEach(path => { Files.delete(path); () })
finally children.close()
Files.delete(stage)
}
println(s"OK $input")
true
} catch {
case NonFatal(error) =>
Console.err.println(s"FAILED $input: ${error.getMessage}")
false
}
}
private def run(arguments: Array[String]): Int = {
if (arguments.length < 4) {
Console.err.println("Usage: ImageBatch NEW_OUTPUT_DIR FONT.ttf LABEL INPUT [INPUT ...]")
return 2
}
val output = Paths.get(arguments(0)).toAbsolutePath
val font = Paths.get(arguments(1)).toAbsolutePath
val label = arguments(2)
val inputs = arguments.drop(3).map(value => Paths.get(value).toAbsolutePath).toVector
require(Files.isRegularFile(font) && Files.isReadable(font), "Font must be readable")
require(label.matches("[A-Za-z0-9][A-Za-z0-9 ._-]{0,39}"), "Invalid watermark label")
require(inputs.map(_.getFileName.toString).distinct.size == inputs.size,
"Input basenames must be distinct")
// Creating a new directory reserves this batch; an existing directory is never reused.
Files.createDirectory(output)
var succeeded = 0
var failed = 0
for (input <- inputs) {
if (processImage(input, output, font, label)) succeeded += 1
else failed += 1
}
println(s"Batch: $succeeded succeeded, $failed failed")
if (failed == 0) 0 else 1
}
def main(arguments: Array[String]): Unit = {
val status = try run(arguments) catch {
case NonFatal(error) =>
Console.err.println(s"Cannot start batch: ${error.getMessage}")
2
}
sys.exit(status)
}
}
Process(Seq(...), directory)
übergibt separate Argumente an die ausführbare Datei. Leerzeichen in Pfaden erfordern keinen Aufbau
eines Shell-Befehls. ImageMagick hat jedoch eine eigene
Interpretation von Dateinamen, einschließlich Frame-Selektoren
und Ausgabemustern. Die Java-Dateioperationen kopieren jede Quelldatei nach
source, führen ImageMagick in diesem privaten Verzeichnis aus und verschieben
result.png unter den wörtlichen Ausgabenamen. Dadurch bleiben auch eckige
Klammern, Prozentzeichen und führende Bindestriche in Ihren Dateinamen wörtlich erhalten.
Die Ausrichtung wird vor der Größenänderung und dem Entfernen der Metadaten angewendet. Die
Größenänderung passt das Bild in 800×600 ein, ohne es zuzuschneiden, zu verzerren oder kleinere
Eingaben zu vergrößern. Transparente Pixel werden mit Weiß hinterlegt, und
PNG24: schreibt eine deckende 8-Bit-RGB-PNG-Datei. Der weiße Text mit
schwarzer Kontur wird unten rechts platziert; verwenden Sie für kleine Bilder einen kurzen Text,
damit er hineinpasst.
Die sRGB-Konvertierung ist generisch. Profilabhängige Bilder oder solche mit großem Farbraum, insbesondere CMYK-Fotos, benötigen einen geeigneten Workflow mit ICC-Profilen für einen präzisen Farbabgleich. Bewahren Sie Ihre Originale auf. Verwenden Sie nicht animierte PNG-Dateien: Der PNG-Decoder liest nur den ersten Frame einer APNG-Datei, daher kann die Formatprüfung nicht jede Animation erkennen. Dieses Programm erhält keine Animationen.
Ein konkretes Beispiel kompilieren und ausführen
Erstellen Sie zwei Beispielbilder. Diese Befehle werden aus Ihrem ursprünglichen Arbeitsverzeichnis ausgeführt und ersetzen keines der Beispielbilder, falls es bereits existiert.
(
cd scala-images &&
test ! -e inputs/landscape.jpg && test ! -L inputs/landscape.jpg &&
test ! -e 'inputs/portrait photo.png' && test ! -L 'inputs/portrait photo.png' &&
magick -size 1600x900 xc:steelblue inputs/landscape.jpg &&
magick -size 600x1200 xc:seagreen 'inputs/portrait photo.png'
)
Kompilieren Sie das gespeicherte Programm und rufen Sie dann seine Hauptklasse mit Java auf.
&& verhindert, dass ein fehlgeschlagener Build alte Klassendateien
startet. Das übergeordnete Verzeichnis des Ausgabeverzeichnisses muss existieren;
output selbst darf noch nicht existieren.
(
cd scala-images &&
java -cp 'lib/*' scala.tools.nsc.Main -usejavacp -d classes ImageBatch.scala &&
java -cp 'classes:lib/scala-library-2.13.18.jar' ImageBatch \
output /usr/share/fonts/liberation/LiberationSans-Regular.ttf 'Scala batch' \
inputs/landscape.jpg 'inputs/portrait photo.png'
)
Ein erfolgreicher Durchlauf gibt pro Eingabe eine Zeile mit OK sowie
Batch: 2 succeeded, 0 failed aus und endet mit Status null.
Prüfen Sie die Ausgabedateien unter ihren wörtlichen Namen:
(
cd scala-images &&
magick identify -format '%f: %m %wx%h\n' \
output/landscape.jpg.png 'output/portrait photo.png.png'
)
Das Querformat ist eine PNG-Datei mit 800×450, das Hochformat eine PNG-Datei mit 300×600. Öffnen Sie
beide Dateien in einem Bildbetrachter und prüfen Sie den Text Scala batch unten
rechts. Ersetzen Sie für Ihren eigenen Stapel die beiden Eingabeargumente durch Pfade in
Anführungszeichen und wählen Sie ein neues Ausgabeverzeichnis. Java übergibt jedes Argument nach
ImageBatch an das Programm, einschließlich Dateinamen, die mit
- beginnen.
Die Stapelverarbeitung begrenzen
Die Scala-Schleife verarbeitet jeweils ein Bild. Jeder ImageMagick-Aufruf enthält
-limit thread 1; dies begrenzt die Verarbeitungsthreads von ImageMagick, nicht den
gesamten Speicherbedarf oder externe Delegates. Das Decodieren einer großen Quelldatei kann die
Ressourcenrichtlinie Ihrer Installation bereits überschreiten, bevor die Größenänderung das Bild
verkleinert. Ändern Sie die globale Richtlinie nicht nur, um diese Beispiele auszuführen. Die
Dokumentation zur Ressourcenrichtlinie erläutert diese Grenzen.
Jeder Auftrag legt eine Eingabedatei, eine Schriftdatei und eine Ausgabedatei temporär ab und entfernt
anschließend sein temporäres Verzeichnis. Erfolgreiche PNG-Dateien sammeln sich im Stapelverzeichnis.
Dieses Beispiel hat weder ein Zeitlimit pro Auftrag noch eine Bereinigung bei Unterbrechungen.
Wenn die JVM gestoppt wird, kann ein Verzeichnis namens .work-* zurückbleiben,
das Sie nach dem Ende der Prozesse prüfen und entfernen können. Verwenden Sie dieses Programm als
lokales Werkzeug zur Stapelverarbeitung vertrauenswürdiger Dateien, nicht als Upload-Dienst.
Fehler in einem fehlgeschlagenen Stapel diagnostizieren
Exit-Status 1 bedeutet, dass mindestens ein ausgewähltes Bild fehlgeschlagen ist. Die Zeile mit
FAILED nennt diese Eingabe. Die abschließenden Zähler berücksichtigen alle
Bilder, auch wenn ein späteres Bild erfolgreich verarbeitet wird. Leere, fehlende, unlesbare und
nicht erkannte Dateien schlagen fehl, ohne die übrigen Aufträge zu stoppen. Die temporäre Ausgabe
einer fehlgeschlagenen Konvertierung wird entfernt; erfolgreiche Ergebnisse bleiben im
Ausgabeverzeichnis.
Exit-Status 2 bedeutet, dass die Stapelverarbeitung nicht starten konnte, etwa weil die Schriftdatei unlesbar ist, der Wasserzeichentext ungültig ist, Eingabebasisnamen kollidieren oder das Ausgabeverzeichnis bereits existiert. Ein erneuter Durchlauf lehnt das vorhandene Verzeichnis ab und bewahrt so dessen Dateien. Wählen Sie nach der Fehlerbehebung ein neues Ziel. Lassen Sie die Eingabedateien und die Schriftdatei während der Verarbeitung unverändert. Lassen Sie keinen anderen Prozess ein Stapelverzeichnis bearbeiten, solange dieses Programm es verwaltet.
-regard-warnings stuft einige Decoder-Warnungen
als Fehler ein. Das Programm lehnt außerdem einen Befehl ab, der trotz Rückgabestatus null
Diagnosemeldungen ausgibt. Dies ist keine Integritätsprüfung für Bilder. Ein Decoder kann beschädigte
Nutzdaten ohne Meldung wiederherstellen. Prüfen Sie bei möglichen Beschädigungen die gesamte Quelle
und Ausgabe. Ein gültiger Header, die erwarteten Abmessungen oder ein erfolgreicher Prozessstatus
allein können keine intakten Pixel belegen.
Für eine Anwendung, die verwaltete Verarbeitung statt einer lokalen ImageMagick-Installation benötigt, sehen Sie sich die Bildverarbeitungs-API von Transloadit an.
