Olaf Lischke
.NET Allgemein Powershell
Powershell: EXIF-Daten aus Bildern auslesen

Powershell: EXIF-Daten aus Bildern auslesen

Digitale Kameras (ja, auch die in Handys!) schreiben beim Speichern einer Aufnahme eine ganze Reihe fotografisch relevanter Daten (Belichtung, ISO-Zahl, Brennweite, Blende, Kameramodell) als sog. EXIF-Daten in die Bilddatei – übrigens nahezu unabhängig vom Zielformat, sowohl RAW als auch JPG enthalten diese Daten. Und auch unabhängig vom Kamerahersteller, denn EXIF ist ein weltweiter Standard.

Allerdings, da das Speicherformat von Bildern immer ein binäres ist, werden die EXIF-Daten ebenfalls dort binär abgelegt, was das Auslesen etwas aufwändiger macht. Jedem EXIF-Tag (Belichtung, ISO-Zahl, Brennweite,..) ist eine hexadezimale Tag-ID zugeordnet, und jeder EXIF-Tag hat einen spezifischen Datentyp, ggf. sogar eine Auswahlliste fest vorgegebener Werte. Wer sich reinnerden will: https://exiftool.org/TagNames/EXIF.html.

Aber egal, ob für eine Bilddatenbank oder RAG für eine lokale KI, man kann den Job mit Powershell erledigen, wir bauen uns ein CmdLet Get-ExifData, das für ein gegebenes Bild die EXIF-Daten ausliest.

Zwei kleine Helper-Funktionen

Ich kann mich an eine Auftragsarbeit in C# vor Jahren erinnern, wo das Auslesen der EXIF-Daten in eine ziemliche Konvertierungsorgie eskalierte. Powershell ist da freundlicher, weil es viele Konvertierungen implizit vornimmt. Trotzdem erstellen wir uns eine kleine Konvertierungsfunktion für die wichtigsten Datentypen, auf die wir treffen werden – Byte, ASCII, short, long, rational (Fließkomma):

function Get-ExifValue {
      param ($prop)
      switch ($prop.Type) {
        1 { return $prop.Value[0] } # BYTE
        2 { return ([System.Text.Encoding]::ASCII.GetString($prop.Value)).Trim([char]0) } # ASCII
        3 { return [BitConverter]::ToUInt16($prop.Value, 0) } # SHORT
        4 { return [BitConverter]::ToUInt32($prop.Value, 0) } # LONG
        5 { return Convert-Rational $prop.Value } # RATIONAL
        7 { return $prop.Value } # UNDEFINED
        9 { return [BitConverter]::ToInt32($prop.Value, 0) } # SLONG
        10 { return Convert-Rational $prop.Value } # SRATIONAL
        default { return $prop.Value }
      }
    }

Und weil beim rational-Datenformat jemand ganz kreativ war (es stellt eigentlich einen ganzzahligen Bruch dar: Die ersten 4 Bytes sind der Zähler, die zweiten 4 Bytes der Nenner), hier noch eine zusätzliche Konvertierung, damit daraus ein in Powershell verwendbarer double wird:

function Convert-Rational {
      param ($bytes)
      $num = [BitConverter]::ToUInt32($bytes, 0)
      $den = [BitConverter]::ToUInt32($bytes, 4)
      if ($den -eq 0) { return $num }
      return [math]::Round($num / $den, 4)
    }

Außerdem holen wir uns von https://exiftool.org/TagNames/EXIF.html die IDs der Tags, die uns interessieren, und legen sie in ein Array:

$exifTags = @{
      0x0100 = "ImageWidth"
      0x0101 = "ImageHeight"
      0x010F = "Make"
      0x0110 = "Model"
      0x0112 = "Orientation"
      0x0132 = "DateTime"
      0x829A = "ExposureTime"
      0x829D = "FNumber"
      0x8833 = "ISOSpeed"
      0x9003 = "DateTimeOriginal"
      0x9201 = "ShutterSpeedValue"
      0x9202 = "ApertureValue"
      0x9204 = "ExposureBiasValue"
      0x9209 = "Flash"
      0x920A = "FocalLength"
      0xA002 = "PixelXDimension"
      0xA003 = "PixelYDimension"
      0xA405 = "FocalLengthIn35mmFilm"
      0xA432 = "LensSpecification"
      0xA434 = "LensModel"
      0xA420 = "ImageUniqueID"
    }

Diese Liste kann natürlich beliebig erweitert werden, hier wurden nur die wichtigsten der gut 80 EXIF-Tags ausgewählt.

Die eigentliche Skript-Logik

Und dann kann es auch schon losgehen: $Image enthält den vollständigen Pfad zu einer Bilddatei, aus der wir ein System.Drawing.Image machen. Diese .NET-Klasse stellt uns die EXIF-Daten als PropertyItems zur Verfügung, über die wir iterieren und nach „unseren“ TagIDs suchen. Bei einem Treffer kommt unsere Get-ExifValue Funktion zum Einsatz, und wir schreiben den nun für Powershell lesbaren Wert in unsere Ergebnisdaten $exifData.

if (-not (Test-Path $Image)) {
      Write-Error "File not found: $Image"
      return
    }
    try {
      $imageObj = [System.Drawing.Image]::FromFile($Image)
      # Hashtabelle für EXIF-Daten
      $exifData = [ordered]@{}
      # Die EXIF-Daten sind PropertyItems des Image-Objekts
      foreach ($prop in $imageObj.PropertyItems) {
        # Konvertiere die numerische Property-ID in einen 4-stelligen Hexadezimal-String (z.B. 0x829A)
        $id = '{0:X4}' -f $prop.Id
        # Verwende einen sprechenden Namen aus $exifTags, falls verfügbar, sonst einen generischen Namen
        $name = $exifTags[$prop.Id] ? $exifTags[$prop.Id] : "Unknown_0x$id"
        # Dekodiere den Property-Wert mit der Hilfsfunktion, abhängig vom Typ
        $value = Get-ExifValue $prop
        # Speichere den Property-Namen und den dekodierten Wert in der Hashtabelle
        $exifData[$name] = $value
      }
      $imageObj.Dispose()
      # Gib alle EXIF-Daten als PowerShell-Objekt aus
      [PSCustomObject]$exifData
    }
    catch {
      Write-Error "Could not read EXIF data from $Image. $_"
    }

Weil Speicher begrenzt ist, wird am Ende noch das Image entsorgt (Dispose()), und weil Dateizugriffe kleine Zicken sind, liegt das Ganze in einem robusten Try...Catch Block.

Das fertige CmdLet im Ganzen

Und hier nochmal das ganze Skript verpackt in ein CmdLet Get-ExifData:

function Get-ExifData {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias("FullName")]
    [string]$Image
  )

  begin {
    $exifTags = @{
      0x0100 = "ImageWidth"
      0x0101 = "ImageHeight"
      0x010F = "Make"
      0x0110 = "Model"
      0x0112 = "Orientation"
      0x0132 = "DateTime"
      0x829A = "ExposureTime"
      0x829D = "FNumber"
      0x8833 = "ISOSpeed"
      0x9003 = "DateTimeOriginal"
      0x9201 = "ShutterSpeedValue"
      0x9202 = "ApertureValue"
      0x9204 = "ExposureBiasValue"
      0x9209 = "Flash"
      0x920A = "FocalLength"
      0xA002 = "PixelXDimension"
      0xA003 = "PixelYDimension"
      0xA405 = "FocalLengthIn35mmFilm"
      0xA432 = "LensSpecification"
      0xA434 = "LensModel"
      0xA420 = "ImageUniqueID"
    }

    function Convert-Rational {
      param ($bytes)
      $num = [BitConverter]::ToUInt32($bytes, 0)
      $den = [BitConverter]::ToUInt32($bytes, 4)
      if ($den -eq 0) { return $num }
      return [math]::Round($num / $den, 4)
    }
    
    function Get-ExifValue {
      param ($prop)
      switch ($prop.Type) {
        1 { return $prop.Value[0] } # BYTE
        2 { return ([System.Text.Encoding]::ASCII.GetString($prop.Value)).Trim([char]0) } # ASCII
        3 { return [BitConverter]::ToUInt16($prop.Value, 0) } # SHORT
        4 { return [BitConverter]::ToUInt32($prop.Value, 0) } # LONG
        5 { return Convert-Rational $prop.Value } # RATIONAL
        7 { return $prop.Value } # UNDEFINED
        9 { return [BitConverter]::ToInt32($prop.Value, 0) } # SLONG
        10 { return Convert-Rational $prop.Value } # SRATIONAL
        default { return $prop.Value }
      }
    }
  }
  process {
    if (-not (Test-Path $Image)) {
      Write-Error "File not found: $Image"
      return
    }
    try {
      $imageObj = [System.Drawing.Image]::FromFile($Image)
      # Hashtabelle für EXIF-Daten
      $exifData = [ordered]@{}
      # Die EXIF-Daten sind PropertyItems des Image-Objekts
      foreach ($prop in $imageObj.PropertyItems) {
        # Konvertiere die numerische Property-ID in einen 4-stelligen Hexadezimal-String (z.B. 0x829A)
        $id = '{0:X4}' -f $prop.Id
        # Verwende einen sprechenden Namen aus $exifTags, falls verfügbar, sonst einen generischen Namen
        $name = $exifTags[$prop.Id] ? $exifTags[$prop.Id] : "Unknown_0x$id"
        # Dekodiere den Property-Wert mit der Hilfsfunktion, abhängig vom Typ
        $value = Get-ExifValue $prop
        # Speichere den Property-Namen und den dekodierten Wert in der Hashtabelle
        $exifData[$name] = $value
      }
      $imageObj.Dispose()
      # Gib alle EXIF-Daten als PowerShell-Objekt aus
      [PSCustomObject]$exifData
    }
    catch {
      Write-Error "Could not read EXIF data from $Image. $_"
    }
  }
}

Export-ModuleMember -Function Get-ExifData

# Anwendung:
# Import-Module ".\Get-ExifData.psm1"
# Get-ExifData -Image "C:\Path\To\Your\image.jpg"
Powershell
Powershell: Wakeup-on-LAN

Powershell: Wakeup-on-LAN

Neulich hatte ich das Vergnügen, mich mit dem guten alten Wakeup-On-LAN (WOL) auseinandersetzen zu dürfen. Die Doku schreibt etwas von einem „Magic Paket“, das gesendet wird, um den Rechner aufzuwecken.

Und weil Magie und Powershell sich irgendwie ganz gut vertragen, kurz mal tiefer eingelesen und gelernt, dass es sich dabei um ein simples UDP-Paket handelt, das an die MAC-Adresse des zu weckenden Rechners gesendet wird.

In Powershell sieht das Ganze dann so aus:

function Send-WolPacket {
    param (
        [Parameter(Mandatory=$true)]
        [string]$Mac,
        [string]$Broadcast = "255.255.255.255",
        [int]$Port = 9
    )
    $macBytes = ($mac -split '[:-]') | % { [byte]('0x' + $_) }
    $packet = ([byte[]](,0xFF * 6 + ($macBytes * 16)))
    $udp = new-Object System.Net.Sockets.UdpClient
    $udp.Connect($broadcast, $port)
    $udp.Send($packet, $packet.Length)
    $udp.Close()
}

Beispielaufruf (MAC-Adresse natürlich anpassen!):

Send-WolPacket -Mac "12-34-56-78-9A-BC"

Voraussetzung ist, dass der zu weckende Rechner entsprechend für Wakeup-On-LAN konfiguriert ist (siehe BIOS-Einstellungen). Außerdem funktioniert die Wakeup-On-LAN nicht, wenn der Rechner im Hibernate-Zustand ist. Komplett heruntergefahren oder Standby sind Zustände, aus denen er aufwachen kann.