Standort-Wasserzeichen für Bilder mit Transloadit erstellen

Ein Foto kann GPS-Koordinaten in seinen EXIF-Metadaten enthalten. Wir können diese Koordinaten lokal auslesen, einen Reverse-Geocoder nach einer Ortsbezeichnung fragen und diese Bezeichnung dann mit Transloadit in das Bild einfügen. Das Auslesen von EXIF und das Reverse-Geocoding sind getrennte Vorgänge: Ein Geocoder extrahiert keine Bildmetadaten.

Bevor wir beginnen
Verwenden Sie Node.js 24 oder neuer und ein JPEG, das Ihnen gehört und GPS-Metadaten enthält. Dieses Beispiel verspricht bewusst keine Unterstützung für jede Kamera-RAW- oder HEIC-Variante. Installieren Sie die eigentlichen Bibliotheken:
npm install exifr@7.1.3 @transloadit/node@4.12.0
Bewahren Sie die erzeugte Lockfile zusammen mit Ihrem Projekt auf. Der Code ist ein CLI-Werkzeug für ein einzelnes Foto, kein öffentlicher Dienst. Er sendet Koordinaten an den öffentlichen Nominatim-Dienst und lädt das Foto zu Transloadit hoch. Verwenden Sie ihn nicht für sensible Orte, für Fotos ohne Erlaubnis oder für automatisierte Massen-Workloads.
Lesen Sie die Nominatim-Nutzungsrichtlinie. Geben Sie eine identifizierende Kontaktadresse an, betreiben Sie immer nur eine Instanz gleichzeitig, lassen Sie mindestens eine Sekunde zwischen den Abfragen und verwenden Sie frühere Ergebnisse erneut, statt dasselbe Foto wiederholt abzufragen. Für ein Produkt oder eine Batch-Pipeline wählen Sie einen Anbieter, dessen Vertrag diese Last abdeckt, oder betreiben Sie einen eigenen Geocoder.
Code
Legen Sie dieses vollständige Programm in geo-watermarker.mjs ab. Stellen Sie TRANSLOADIT_AUTH_KEY,
TRANSLOADIT_AUTH_SECRET, GEOCODER_CONTACT und PHOTO über Ihre lokale Umgebung bereit.
Bei GEOCODER_CONTACT sollte es sich um eine echte Kontakt-E-Mail-Adresse der betreibenden Person der Anwendung handeln, nicht um ein Secret.
import { open } from 'node:fs/promises'
import { setTimeout as delay } from 'node:timers/promises'
import { Transloadit } from '@transloadit/node'
import exifr from 'exifr'
function required(name) {
const value = process.env[name]
if (!value) throw new Error('Missing configuration.')
return value
}
async function readPhoto(path) {
const handle = await open(path, 'r')
try {
const stat = await handle.stat()
if (!stat.isFile() || stat.size === 0 || stat.size > 8 * 1024 * 1024) {
throw new Error('Use a JPEG no larger than 8 MiB.')
}
const bytes = Buffer.alloc(stat.size)
let offset = 0
while (offset < bytes.length) {
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, null)
if (bytesRead === 0) throw new Error('Photo changed while reading.')
offset += bytesRead
}
if (bytes[0] !== 0xff || bytes[1] !== 0xd8) throw new Error('Use a JPEG photo.')
return bytes
} finally {
await handle.close()
}
}
async function placeFor(latitude, longitude, contact) {
if (!Number.isFinite(latitude) || !Number.isFinite(longitude) ||
Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
throw new Error('Photo has no valid GPS coordinates.')
}
const url = new URL('https://nominatim.openstreetmap.org/reverse')
url.searchParams.set('format', 'jsonv2')
url.searchParams.set('lat', String(latitude))
url.searchParams.set('lon', String(longitude))
url.searchParams.set('zoom', '10')
url.searchParams.set('accept-language', 'en')
await delay(1000)
const response = await fetch(url, {
headers: { 'User-Agent': 'PhotoWatermarker/1.0 (' + contact + ')' },
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) throw new Error('Location lookup failed.')
const data = await response.json()
if (typeof data?.display_name !== 'string') throw new Error('No place was found.')
// Keep the label bounded and plain text before sending it to the image renderer.
const label = data.display_name.replace(/[\u0000-\u001f\u007f]/g, ' ').trim()
if (!label || label.length > 160) throw new Error('Place label needs manual review.')
return label
}
async function main() {
const photo = required('PHOTO')
const authKey = required('TRANSLOADIT_AUTH_KEY')
const authSecret = required('TRANSLOADIT_AUTH_SECRET')
const contact = required('GEOCODER_CONTACT')
if (!/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(contact)) {
throw new Error('Provide a contact email for the geocoder.')
}
const bytes = await readPhoto(photo)
const gps = await exifr.gps(bytes)
const address = await placeFor(gps?.latitude, gps?.longitude, contact)
const transloadit = new Transloadit({ authKey, authSecret })
const result = await transloadit.createAssembly({
waitForCompletion: true,
timeout: 120_000,
signal: AbortSignal.timeout(120_000),
uploads: { photo: bytes },
params: {
fields: { address },
steps: {
':original': { robot: '/upload/handle' },
watermarked: {
robot: '/image/resize',
use: ':original',
result: true,
format: 'jpg',
strip: true,
text: [{
text: '${fields.address}',
size: 24,
font: 'Ubuntu',
color: '#ffffff',
background_color: '#000000',
align: 'center',
valign: 'bottom',
y_offset: -12,
}],
imagemagick_stack: 'v3',
},
},
},
})
const output = result.results?.watermarked?.[0]?.ssl_url
if (result.ok !== 'ASSEMBLY_COMPLETED' || typeof output !== 'string') {
throw new Error('No completed watermarked image was returned.')
}
const url = new URL(output)
if (url.protocol !== 'https:') throw new Error('Unexpected result URL.')
console.log('Result: ' + url.href)
console.log('Place data: OpenStreetMap contributors, https://www.openstreetmap.org/copyright')
}
main().catch(() => {
console.error('Watermarking failed. Check configuration, photo metadata and service availability.')
process.exitCode = 1
})
Informationsextraktion
Exifr liest die GPS-Tags des JPEG aus und wandelt sie in dezimale Breiten- und Längengrade um. Null ist eine gültige Koordinate, deshalb prüft das Programm Zahlenbereiche statt Truthiness. Fehlende Koordinaten stoppen den Ablauf, bevor überhaupt etwas hochgeladen wird.
Der begrenzte Puffer wird sowohl für die Extraktion der Metadaten als auch für den Upload verwendet, wodurch ein zweites Lesen einer Datei entfällt, die sich zwischenzeitlich geändert haben könnte. Halten Sie das lokale Foto während des Auslesens unverändert.
Geocodierung
Reverse-Geocoding ordnet Koordinaten einem nahe gelegenen kartierten Ort zu; es ist kein Beleg dafür, wo ein Foto aufgenommen wurde. EXIF-Daten können fehlen, bearbeitet oder ungenau sein. Das Beispiel fragt eine grobe Ortsbezeichnung ab, sendet dem Anbieter aber dennoch die ursprünglichen Koordinaten.
Das CLI-Werkzeug gibt die OpenStreetMap-Namensnennung aus. Behalten Sie die erforderlichen Hinweise zu Namensnennung und Lizenzierung überall dort bei, wo Sie die abgeleiteten Ortsdaten veröffentlichen. Eine Meldung im Terminal allein ist keine Namensnennung für eine veröffentlichte Bildergalerie. Prüfen Sie die zurückgegebene Bezeichnung und kürzen Sie sie bei Bedarf selbst, bevor Sie dieses Muster für ein echtes Foto einsetzen.
Encoding des Endergebnisses
Der Auth Key und das Auth Secret authentifizieren das serverseitige SDK. Sie sind keine Template-Zugangsdaten für Storage. Legen Sie das Secret niemals in Browser-Code, in einem öffentlichen Repository oder in einem geteilten Terminal-Mitschnitt ab.
Der Robot /image/resize
verwendet die Adresse als Assembly Variable und gibt
ein JPEG zurück. Die Option strip entfernt die Metadaten der Quelle aus der Ausgabe, doch das
hochgeladene Original und die Anfrage an den Geocoder haben den Standort diesen Diensten bereits
offengelegt. Prüfen Sie Ihre Anforderungen an den Umgang mit Daten, bevor Sie das Programm ausführen.
Ergebnisse
Wenn Sie Ihre Umgebung konfiguriert haben, führen Sie Folgendes aus:
node geo-watermarker.mjs
Eine erfolgreiche Assembly gibt eine temporäre Ergebnis-URL und den Hinweis zur Namensnennung aus. Laden Sie das Ergebnis zeitnah herunter oder fügen Sie einen Step für den Export an einen Speicherort hinzu, den Sie selbst kontrollieren. Prüfen Sie bei Ihren Bildabmessungen, ob die gerenderte Bezeichnung abgeschnitten wird; die feste Schriftgröße ist ein Beispiel, kein universelles Layout.

Das zeigt die Grenze der Integration: lokale Extraktion der Metadaten, eine explizite Standortabfrage und ein verwalteter Vorgang zum Setzen des Wasserzeichens. Wenn Sie das für ein Produkt automatisieren, sind zusätzlich Einwilligungen, passende Anbieterbedingungen, aufbewahrte Abfrageergebnisse, betriebliche Limits und ein geprüfter Veröffentlichungsprozess erforderlich.