NikonExif.psm1
|
function get-exif { param( [Parameter(Mandatory = $true, HelpMessage = 'Путь к файлу JPG или NEF')] [string]$Path ) # Снимаем пробелы и кавычки, если путь введён вручную в приглашении $Path = $Path.Trim().Trim('"').Trim("'").Trim() # Проверка расширения $Allowed = '.jpg', '.jpeg', '.nef' $ext = [IO.Path]::GetExtension($Path).ToLowerInvariant() if ($ext -notin $Allowed) { Write-Host 'Скрипт поддерживает расширения jpg, jpeg, nef' -ForegroundColor Red return } if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Write-Host "Файл не существует: $Path" -ForegroundColor Red return } # Теги для вывода, в нужном порядке $wanted = @( @{ Id = 0x010F; Name = 'Make' } @{ Id = 0x0110; Name = 'Model' } @{ Id = 0xA433; Name = 'LensMake' } @{ Id = 0x0131; Name = 'Software' } @{ Id = 0x829A; Name = 'ExposureTime' } @{ Id = 0x829D; Name = 'FNumber' } @{ Id = 0x8822; Name = 'ExposureProgram' } @{ Id = 0x8827; Name = 'ISOSpeedRatings' } @{ Id = 0xA405; Name = 'FocalLengthIn35mmFilm' } @{ Id = 0xA432; Name = 'LensSpecification' } @{ Id = 0x9003; Name = 'DateTimeOriginal' } @{ Id = 0x9004; Name = 'DateTimeDigitized' } @{ Id = 0xA431; Name = 'BodySerialNumber' } @{ Id = 0xA435; Name = 'LensSerialNumber' } ) # Точка как десятичный разделитель (3.2, а не 3,2 в русской локали) $inv = [Globalization.CultureInfo]::InvariantCulture # Полный путь нужен, т.к. .NET не знает о текущей папке PowerShell $fullPath = (Get-Item -LiteralPath $Path).FullName try { $bytes = [IO.File]::ReadAllBytes($fullPath) } catch { Write-Host "Не удалось прочитать файл: $($_.Exception.Message)" -ForegroundColor Red return } # --- Поиск начала TIFF-блока с EXIF --- $tiff = -1 if ($bytes.Length -ge 4 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xD8) { # JPEG: перебираем сегменты, ищем APP1 (0xFFE1) с сигнатурой "Exif" $pos = 2 while ($pos + 4 -le $bytes.Length -and $bytes[$pos] -eq 0xFF) { $marker = $bytes[$pos + 1] if ($marker -eq 0xDA -or $marker -eq 0xD9) { break } # дальше данные изображения $segLen = ([int]$bytes[$pos + 2] -shl 8) -bor $bytes[$pos + 3] if ($marker -eq 0xE1 -and $pos + 10 -le $bytes.Length -and [Text.Encoding]::ASCII.GetString($bytes, $pos + 4, 4) -eq 'Exif') { $tiff = $pos + 10 break } $pos += 2 + $segLen } } elseif ($bytes.Length -ge 8 -and (($bytes[0] -eq 0x49 -and $bytes[1] -eq 0x49) -or ($bytes[0] -eq 0x4D -and $bytes[1] -eq 0x4D))) { $tiff = 0 # NEF — TIFF-контейнер, EXIF начинается с первого байта } if ($tiff -lt 0) { Write-Host "EXIF не найден или формат не поддерживается: $Path" -ForegroundColor Red return } # 'II' = little-endian (Intel), 'MM' = big-endian (Motorola) $le = $bytes[$tiff] -eq 0x49 # --- Чтение чисел с учётом порядка байт --- function Get-Chunk([long]$o, [int]$size) { $c = [byte[]]::new($size) [Array]::Copy($bytes, $o, $c, 0, $size) if ($le -ne [BitConverter]::IsLittleEndian) { [Array]::Reverse($c) } , $c } function U16([long]$o) { [BitConverter]::ToUInt16((Get-Chunk $o 2), 0) } function U32([long]$o) { [BitConverter]::ToUInt32((Get-Chunk $o 4), 0) } function I32([long]$o) { [BitConverter]::ToInt32((Get-Chunk $o 4), 0) } # Размер одного элемента для типов TIFF $typeSize = @{ 1 = 1; 2 = 1; 3 = 2; 4 = 4; 5 = 8; 7 = 1; 9 = 4; 10 = 8 } $tags = @{} # --- Разбор каталога тегов (IFD) --- function Read-Ifd([long]$ifdOffset) { $start = $tiff + $ifdOffset if ($start + 2 -gt $bytes.Length) { return } $count = U16 $start for ($i = 0; $i -lt $count; $i++) { $e = $start + 2 + $i * 12 if ($e + 12 -gt $bytes.Length) { break } $tag = [int](U16 $e) $type = [int](U16 ($e + 2)) $n = [long](U32 ($e + 4)) if (-not $typeSize.ContainsKey($type)) { continue } $total = $n * $typeSize[$type] # До 4 байт значение лежит прямо в записи, иначе там смещение $data = if ($total -le 4) { $e + 8 } else { $tiff + (U32 ($e + 8)) } if ($data + $total -gt $bytes.Length) { continue } $tags[$tag] = @{ Type = $type; Count = $n; Offset = $data } } } # --- Форматирование дробей --- function Format-Rational([int]$tagId, [double]$num, [double]$den) { if ($den -eq 0) { return 'n/a' } # Выдержку показываем как 1/100 if ($tagId -eq 0x829A -and $num -gt 0 -and $num -lt $den) { return '1/' + [math]::Round($den / $num).ToString($inv) } [math]::Round($num / $den, 2).ToString($inv) } function Get-TagValue([int]$tagId) { $t = $tags[$tagId] $o = $t.Offset $n = [int]$t.Count if ($n -eq 0) { return '' } switch ($t.Type) { 2 { return [Text.Encoding]::ASCII.GetString($bytes, $o, $n).Trim([char]0, ' ') } 3 { $vals = for ($k = 0; $k -lt $n; $k++) { U16 ($o + 2 * $k) } } 4 { $vals = for ($k = 0; $k -lt $n; $k++) { U32 ($o + 4 * $k) } } 9 { $vals = for ($k = 0; $k -lt $n; $k++) { I32 ($o + 4 * $k) } } 5 { $vals = for ($k = 0; $k -lt $n; $k++) { Format-Rational $tagId (U32 ($o + 8 * $k)) (U32 ($o + 8 * $k + 4)) } } 10 { $vals = for ($k = 0; $k -lt $n; $k++) { Format-Rational $tagId (I32 ($o + 8 * $k)) (I32 ($o + 8 * $k + 4)) } } default { $vals = $bytes[$o..($o + $n - 1)] } } ($vals | ForEach-Object { "$_" }) -join ', ' } # --- ShutterCount из Nikon MakerNote (тег 0x927C -> вложенный тег 0x00A7) --- # Структура MakerNote: "Nikon\0" + версия (4 байта), затем собственный TIFF-заголовок function Get-ShutterCount { if (-not $tags.ContainsKey(0x927C)) { return $null } $mn = $tags[0x927C] $o = [long]$mn.Offset if ($mn.Count -lt 18 -or [Text.Encoding]::ASCII.GetString($bytes, $o, 5) -ne 'Nikon') { return $null } $base = $o + 10 # начало TIFF-заголовка MakerNote $mle = $bytes[$base] -eq 0x49 # свой порядок байт: II или MM function MU([long]$p, [int]$size) { $c = [byte[]]::new($size) [Array]::Copy($bytes, $p, $c, 0, $size) if ($mle -ne [BitConverter]::IsLittleEndian) { [Array]::Reverse($c) } if ($size -eq 2) { [BitConverter]::ToUInt16($c, 0) } else { [BitConverter]::ToUInt32($c, 0) } } $ifd = $base + (MU ($base + 4) 4) if ($ifd + 2 -gt $bytes.Length) { return $null } $count = MU $ifd 2 for ($i = 0; $i -lt $count; $i++) { $e = $ifd + 2 + 12 * $i if ($e + 12 -gt $bytes.Length) { break } if ((MU $e 2) -eq 0x00A7) { return MU ($e + 8) 4 } # LONG, значение внутри записи } return $null } # IFD0: Make, Model, Software + указатель на EXIF-каталог (0x8769) Read-Ifd (U32 ($tiff + 4)) if ($tags.ContainsKey(0x8769)) { Read-Ifd (U32 $tags[0x8769].Offset) } # --- Вывод --- foreach ($w in $wanted) { if ($tags.ContainsKey($w.Id)) { [pscustomobject]@{ Id = '0x{0:X4}' -f $w.Id Name = $w.Name Value = Get-TagValue $w.Id } } } # Последняя строка: количество сделанных кадров $shutter = Get-ShutterCount $shutterValue = if ($null -ne $shutter) { "$shutter" } else { 'не найдено' } [pscustomobject]@{ Id = '0x00A7' Name = 'ShutterCount' Value = $shutterValue } } Export-ModuleMember -Function Get-Exif |