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