Merhabalar, bu betiği tamamen kendi ihtiyacımdan geliştirdim.İsteyen kullansın, isteyen kullanmasın, güle güle kullanın..
KOD İÇERİĞİ : Image_Ratio_v7.ps1
KOD İÇERİĞİ : Image_Ratio_v7.ps1
Kod:
# Image Aspect Ratio Analyzer and Safe ZIP Backup Tool V7
# Scans image dimensions, calculates width/height ratio,
# identifies images below the golden ratio threshold,
# creates a ZIP backup, verifies the archive,
# and deletes originals only after successful verification.
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Wait-ForKeyPress {
Write-Host ""
Write-Host "Press any key to continue..." -ForegroundColor Cyan
try {
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
catch {
Read-Host "Press ENTER to continue" | Out-Null
}
}
function Get-FolderSizeBytes {
param (
[Parameter(Mandatory = $true)]
[string]$Path
)
$Total = [int64]0
try {
Get-ChildItem -LiteralPath $Path -File -Recurse -Force -ErrorAction Stop |
ForEach-Object {
$Total += [int64]$_.Length
}
}
catch {
return [int64]0
}
return $Total
}
function Get-FolderFileCount {
param (
[Parameter(Mandatory = $true)]
[string]$Path
)
try {
return [int64]@(
Get-ChildItem -LiteralPath $Path -File -Recurse -Force -ErrorAction Stop
).Count
}
catch {
return [int64]0
}
}
function Convert-BytesToMB {
param (
[Parameter(Mandatory = $true)]
[int64]$Bytes
)
return [math]::Round(($Bytes / 1MB), 3)
}
# Golden ratio threshold
$GoldenRatio = 1.61803398875
# Image file extensions
$ImageExtensions = @(
".jpg",
".jpeg",
".jfif",
".png",
".bmp",
".gif",
".tif",
".tiff",
".webp",
".avif"
)
function Get-WebPDimensions {
param (
[Parameter(Mandatory = $true)]
[System.IO.FileInfo]$File
)
$Stream = $null
$Reader = $null
try {
$Stream = [System.IO.File]::Open(
$File.FullName,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::Read
)
$Reader = New-Object System.IO.BinaryReader($Stream)
$Header = $Reader.ReadBytes(12)
if ($Header.Length -lt 12) {
throw "Invalid WEBP file header."
}
$Riff = [System.Text.Encoding]::ASCII.GetString($Header, 0, 4)
$WebP = [System.Text.Encoding]::ASCII.GetString($Header, 8, 4)
if ($Riff -ne "RIFF" -or $WebP -ne "WEBP") {
throw "Invalid WEBP RIFF header."
}
while ($Stream.Position -lt $Stream.Length) {
$ChunkHeader = $Reader.ReadBytes(8)
if ($ChunkHeader.Length -lt 8) {
break
}
$ChunkType = [System.Text.Encoding]::ASCII.GetString($ChunkHeader, 0, 4)
$ChunkSize = [BitConverter]::ToUInt32($ChunkHeader, 4)
$ChunkDataStart = $Stream.Position
if ($ChunkType -eq "VP8X") {
if ($ChunkSize -lt 10) {
throw "Invalid VP8X chunk."
}
$Data = $Reader.ReadBytes([int]$ChunkSize)
if ($Data.Length -lt 10) {
throw "Invalid VP8X chunk data."
}
$WidthMinusOne = [int]$Data[4] -bor ([int]$Data[5] -shl 8) -bor ([int]$Data[6] -shl 16)
$HeightMinusOne = [int]$Data[7] -bor ([int]$Data[8] -shl 8) -bor ([int]$Data[9] -shl 16)
return [PSCustomObject]@{
Width = $WidthMinusOne + 1
Height = $HeightMinusOne + 1
}
}
if ($ChunkType -eq "VP8 ") {
if ($ChunkSize -lt 10) {
throw "Invalid VP8 chunk."
}
$Data = $Reader.ReadBytes([int][Math]::Min($ChunkSize, 30))
if ($Data.Length -lt 10) {
throw "Invalid VP8 frame data."
}
if ($Data[3] -ne 0x9D -or $Data[4] -ne 0x01 -or $Data[5] -ne 0x2A) {
$StartCodeIndex = -1
for ($i = 0; $i -le $Data.Length - 6; $i++) {
if ($Data[$i] -eq 0x9D -and $Data[$i + 1] -eq 0x01 -and $Data[$i + 2] -eq 0x2A) {
$StartCodeIndex = $i
break
}
}
if ($StartCodeIndex -lt 0 -or $StartCodeIndex + 6 -ge $Data.Length) {
throw "Unable to locate VP8 frame dimensions."
}
$WidthOffset = $StartCodeIndex + 3
}
else {
$WidthOffset = 6
}
$Width = [BitConverter]::ToUInt16($Data, $WidthOffset)
$Height = [BitConverter]::ToUInt16($Data, $WidthOffset + 2)
return [PSCustomObject]@{
Width = $Width -band 0x3FFF
Height = $Height -band 0x3FFF
}
}
if ($ChunkType -eq "VP8L") {
if ($ChunkSize -lt 5) {
throw "Invalid VP8L chunk."
}
$Data = $Reader.ReadBytes(5)
if ($Data.Length -lt 5 -or $Data[0] -ne 0x2F) {
throw "Invalid VP8L frame data."
}
$Width = 1 + (
[int]$Data[1] -bor
(([int]$Data[2] -band 0x3F) -shl 8)
)
$Height = 1 + (
(([int]$Data[2] -shr 6) -band 0x03) -bor
([int]$Data[3] -shl 2) -bor
(([int]$Data[4] -band 0x0F) -shl 10)
)
return [PSCustomObject]@{
Width = $Width
Height = $Height
}
}
$SkipLength = [int64]$ChunkSize
if (($ChunkSize % 2) -ne 0) {
$SkipLength++
}
$Stream.Position = $ChunkDataStart + $SkipLength
}
throw "No supported WEBP image chunk was found."
}
finally {
if ($null -ne $Reader) {
$Reader.Dispose()
}
elseif ($null -ne $Stream) {
$Stream.Dispose()
}
}
}
function Get-AvifDimensions {
param (
[Parameter(Mandatory = $true)]
[System.IO.FileInfo]$File
)
$Stream = $null
$Reader = $null
try {
$Stream = [System.IO.File]::Open(
$File.FullName,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::ReadWrite
)
# AVIF is based on ISO Base Media File Format.
# The image dimensions are stored in an 'ispe' property box:
# version/flags (4 bytes), width (4 bytes), height (4 bytes).
# Scan a bounded prefix to avoid loading a very large image into memory.
$MaxScanBytes = [int64]1MB * 64
$ScanLength = [int][Math]::Min($Stream.Length, $MaxScanBytes)
if ($ScanLength -lt 16) {
throw "AVIF file is too small."
}
$Buffer = New-Object byte[] $ScanLength
$BytesRead = 0
while ($BytesRead -lt $ScanLength) {
$ReadCount = $Stream.Read($Buffer, $BytesRead, $ScanLength - $BytesRead)
if ($ReadCount -le 0) {
break
}
$BytesRead += $ReadCount
}
$Marker = [System.Text.Encoding]::ASCII.GetBytes("ispe")
for ($Offset = 4; $Offset -le ($BytesRead - 16); $Offset++) {
if ($Buffer[$Offset] -ne $Marker[0] -or
$Buffer[$Offset + 1] -ne $Marker[1] -or
$Buffer[$Offset + 2] -ne $Marker[2] -or
$Buffer[$Offset + 3] -ne $Marker[3]) {
continue
}
# The four bytes immediately before the type are the standard box size.
$BoxSize =
(([int64]$Buffer[$Offset - 4]) -shl 24) -bor
(([int64]$Buffer[$Offset - 3]) -shl 16) -bor
(([int64]$Buffer[$Offset - 2]) -shl 8) -bor
[int64]$Buffer[$Offset - 1]
if ($BoxSize -lt 20) {
continue
}
$Width =
(([int64]$Buffer[$Offset + 8]) -shl 24) -bor
(([int64]$Buffer[$Offset + 9]) -shl 16) -bor
(([int64]$Buffer[$Offset + 10]) -shl 8) -bor
[int64]$Buffer[$Offset + 11]
$Height =
(([int64]$Buffer[$Offset + 12]) -shl 24) -bor
(([int64]$Buffer[$Offset + 13]) -shl 16) -bor
(([int64]$Buffer[$Offset + 14]) -shl 8) -bor
[int64]$Buffer[$Offset + 15]
if ($Width -gt 0 -and $Height -gt 0 -and $Width -le 100000 -and $Height -le 100000) {
return [PSCustomObject]@{
Width = [int]$Width
Height = [int]$Height
}
}
}
throw "AVIF image dimensions were not found in an ispe box."
}
finally {
if ($null -ne $Reader) {
$Reader.Dispose()
}
elseif ($null -ne $Stream) {
$Stream.Dispose()
}
}
}
function Get-JpegDimensions {
param (
[Parameter(Mandatory = $true)]
[System.IO.FileInfo]$File
)
$Stream = $null
$Reader = $null
try {
$Stream = [System.IO.File]::Open(
$File.FullName,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::ReadWrite
)
$Reader = New-Object System.IO.BinaryReader($Stream)
if ($Stream.Length -lt 4) {
throw "JPEG file is too small."
}
$Byte1 = $Reader.ReadByte()
$Byte2 = $Reader.ReadByte()
if ($Byte1 -ne 0xFF -or $Byte2 -ne 0xD8) {
throw "Invalid JPEG SOI marker."
}
while ($Stream.Position -lt $Stream.Length) {
# Search for the next marker prefix.
do {
$MarkerPrefix = $Reader.ReadByte()
} while ($MarkerPrefix -ne 0xFF -and $Stream.Position -lt $Stream.Length)
if ($Stream.Position -ge $Stream.Length) {
break
}
# Skip fill bytes 0xFF.
do {
$Marker = $Reader.ReadByte()
} while ($Marker -eq 0xFF -and $Stream.Position -lt $Stream.Length)
if ($Marker -eq 0x00) {
throw "Invalid JPEG marker stream before SOS."
}
# Standalone markers do not have a length field.
if ($Marker -eq 0xD8 -or $Marker -eq 0xD9 -or ($Marker -ge 0xD0 -and $Marker -le 0xD7) -or $Marker -eq 0x01) {
if ($Marker -eq 0xD9) {
break
}
continue
}
$LengthBytes = $Reader.ReadBytes(2)
if ($LengthBytes.Length -ne 2) {
throw "Truncated JPEG segment length."
}
$SegmentLength = (([int]$LengthBytes[0] -shl 8) -bor [int]$LengthBytes[1])
if ($SegmentLength -lt 2) {
throw "Invalid JPEG segment length."
}
# SOF markers contain the primary frame dimensions.
$IsSofMarker = (
($Marker -ge 0xC0 -and $Marker -le 0xC3) -or
($Marker -ge 0xC5 -and $Marker -le 0xC7) -or
($Marker -ge 0xC9 -and $Marker -le 0xCB) -or
($Marker -ge 0xCD -and $Marker -le 0xCF)
)
if ($IsSofMarker) {
if ($SegmentLength -lt 7) {
throw "Invalid JPEG SOF segment."
}
$Segment = $Reader.ReadBytes($SegmentLength - 2)
if ($Segment.Length -ne ($SegmentLength - 2) -or $Segment.Length -lt 5) {
throw "Truncated JPEG SOF segment."
}
$Height = (([int]$Segment[1] -shl 8) -bor [int]$Segment[2])
$Width = (([int]$Segment[3] -shl 8) -bor [int]$Segment[4])
if ($Width -le 0 -or $Height -le 0) {
throw "Invalid JPEG dimensions."
}
return [PSCustomObject]@{
Width = $Width
Height = $Height
}
}
# SOS begins entropy-coded image data. If no SOF was found before
# SOS, this parser cannot safely determine the primary frame size.
if ($Marker -eq 0xDA) {
break
}
$SkipBytes = $SegmentLength - 2
if (($Stream.Position + $SkipBytes) -gt $Stream.Length) {
throw "Truncated JPEG segment data."
}
$Stream.Seek($SkipBytes, [System.IO.SeekOrigin]::Current) | Out-Null
}
throw "JPEG primary frame dimensions were not found."
}
finally {
if ($null -ne $Reader) {
$Reader.Dispose()
}
elseif ($null -ne $Stream) {
$Stream.Dispose()
}
}
}
function Get-WicDimensions {
param (
[Parameter(Mandatory = $true)]
[System.IO.FileInfo]$File
)
$Stream = $null
$Bitmap = $null
try {
$Stream = [System.IO.File]::Open(
$File.FullName,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::ReadWrite
)
$Bitmap = New-Object System.Windows.Media.Imaging.BitmapImage
$Bitmap.BeginInit()
$Bitmap.CacheOption = [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$Bitmap.CreateOptions = [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat
$Bitmap.StreamSource = $Stream
$Bitmap.EndInit()
$Bitmap.Freeze()
if ($Bitmap.PixelWidth -le 0 -or $Bitmap.PixelHeight -le 0) {
throw "WIC returned invalid image dimensions."
}
return [PSCustomObject]@{
Width = [int]$Bitmap.PixelWidth
Height = [int]$Bitmap.PixelHeight
}
}
finally {
if ($null -ne $Bitmap) {
$Bitmap = $null
}
if ($null -ne $Stream) {
$Stream.Dispose()
}
}
}
function Get-ImageInformation {
param (
[Parameter(Mandatory = $true)]
[System.IO.FileInfo]$File
)
$Image = $null
$ReadMethod = ""
try {
$Extension = $File.Extension.ToLowerInvariant()
$Dimensions = $null
# JPEG and JFIF: prefer WIC for the primary image frame.
# The JPEG header parser is used only as a fallback because JPEG files
# can contain embedded thumbnail images inside APP segments.
if ($Extension -eq ".jpg" -or $Extension -eq ".jpeg" -or $Extension -eq ".jfif") {
try {
$Dimensions = Get-WicDimensions -File $File
$ReadMethod = "WIC"
}
catch {
$WicError = $_.Exception.Message
try {
$Dimensions = Get-JpegDimensions -File $File
$ReadMethod = "JPEG header parser fallback"
}
catch {
$JpegError = $_.Exception.Message
try {
$Image = [System.Drawing.Image]::FromFile($File.FullName)
$Dimensions = [PSCustomObject]@{
Width = [int]$Image.Width
Height = [int]$Image.Height
}
$ReadMethod = "System.Drawing fallback"
}
catch {
$DrawingError = $_.Exception.Message
throw "WIC failed: $WicError | JPEG header parser failed: $JpegError | System.Drawing failed: $DrawingError"
}
}
}
}
# AVIF: prefer WIC, then use the ISO BMFF ispe parser as a codec-independent fallback.
elseif ($Extension -eq ".avif") {
try {
$Dimensions = Get-WicDimensions -File $File
$ReadMethod = "WIC"
}
catch {
$WicError = $_.Exception.Message
try {
$Dimensions = Get-AvifDimensions -File $File
$ReadMethod = "AVIF header parser fallback"
}
catch {
$AvifError = $_.Exception.Message
try {
$Image = [System.Drawing.Image]::FromFile($File.FullName)
$Dimensions = [PSCustomObject]@{
Width = [int]$Image.Width
Height = [int]$Image.Height
}
$ReadMethod = "System.Drawing fallback"
}
catch {
$DrawingError = $_.Exception.Message
throw "WIC failed: $WicError | AVIF header parser failed: $AvifError | System.Drawing failed: $DrawingError"
}
}
}
}
# WEBP: use the dedicated parser first.
elseif ($Extension -eq ".webp") {
try {
$Dimensions = Get-WebPDimensions -File $File
$ReadMethod = "WEBP parser"
}
catch {
try {
$Dimensions = Get-WicDimensions -File $File
$ReadMethod = "WIC fallback"
}
catch {
try {
$Image = [System.Drawing.Image]::FromFile($File.FullName)
$Dimensions = [PSCustomObject]@{
Width = [int]$Image.Width
Height = [int]$Image.Height
}
$ReadMethod = "System.Drawing fallback"
}
catch {
throw "WEBP parser failed: $($_.Exception.Message) | WIC failed: $($_.Exception.Message) | System.Drawing failed: $($_.Exception.Message)"
}
}
}
}
# Other supported formats: WIC first, System.Drawing second.
else {
try {
$Dimensions = Get-WicDimensions -File $File
$ReadMethod = "WIC"
}
catch {
$WicError = $_.Exception.Message
try {
$Image = [System.Drawing.Image]::FromFile($File.FullName)
$Dimensions = [PSCustomObject]@{
Width = [int]$Image.Width
Height = [int]$Image.Height
}
$ReadMethod = "System.Drawing fallback"
}
catch {
throw "WIC failed: $WicError | System.Drawing failed: $($_.Exception.Message)"
}
}
}
$Width = [int]$Dimensions.Width
$Height = [int]$Dimensions.Height
if ($Width -le 0 -or $Height -le 0) {
throw "Invalid image dimensions."
}
$Ratio = [math]::Round(($Width / $Height), 6)
[PSCustomObject]@{
File = $File
Name = $File.Name
FileSize = [int64]$File.Length
SizeMB = [math]::Round(($File.Length / 1MB), 3)
Width = $Width
Height = $Height
Resolution = "$Width x $Height"
Ratio = $Ratio
IsGolden = ($Ratio -ge $GoldenRatio)
Readable = $true
ReadMethod = $ReadMethod
Error = ""
}
}
catch {
[PSCustomObject]@{
File = $File
Name = $File.Name
FileSize = [int64]$File.Length
SizeMB = [math]::Round(($File.Length / 1MB), 3)
Width = 0
Height = 0
Resolution = "Unreadable"
Ratio = 0
IsGolden = $false
Readable = $false
ReadMethod = ""
Error = $_.Exception.Message
}
}
finally {
if ($null -ne $Image) {
$Image.Dispose()
}
}
}
# Load image libraries
$SystemDrawingAvailable = $true
$WicAvailable = $true
try {
Add-Type -AssemblyName System.Drawing
}
catch {
$SystemDrawingAvailable = $false
}
try {
Add-Type -AssemblyName PresentationCore
}
catch {
$WicAvailable = $false
}
if (-not $SystemDrawingAvailable -and -not $WicAvailable) {
Write-Host "No supported image decoding library is available." -ForegroundColor Red
Wait-ForKeyPress
exit 1
}
Write-Host ""
Write-Host "Image Aspect Ratio Analyzer" -ForegroundColor Cyan
Write-Host "----------------------------------------"
Write-Host ""
$TargetFolder = Read-Host "Enter the image folder path"
if ([string]::IsNullOrWhiteSpace($TargetFolder)) {
Write-Host "No folder path was provided." -ForegroundColor Red
Wait-ForKeyPress
exit 1
}
if (-not (Test-Path -LiteralPath $TargetFolder -PathType Container)) {
Write-Host "The specified folder does not exist." -ForegroundColor Red
Wait-ForKeyPress
exit 1
}
$TargetFolder = (Resolve-Path -LiteralPath $TargetFolder).Path
# Capture the complete folder footprint before any changes are made.
$FolderSizeBeforeBytes = Get-FolderSizeBytes -Path $TargetFolder
$FolderFileCountBefore = Get-FolderFileCount -Path $TargetFolder
# Get image files from the selected folder
$ImageFiles = Get-ChildItem -LiteralPath $TargetFolder -File |
Where-Object {
$ImageExtensions -contains $_.Extension.ToLowerInvariant()
} |
Sort-Object Name
if ($ImageFiles.Count -eq 0) {
Write-Host ""
Write-Host "No supported image files were found." -ForegroundColor Yellow
Wait-ForKeyPress
exit 0
}
Write-Host ""
Write-Host "Scanning images..." -ForegroundColor Cyan
Write-Host ""
$Results = New-Object System.Collections.Generic.List[object]
foreach ($ImageFile in $ImageFiles) {
$Info = Get-ImageInformation -File $ImageFile
$Results.Add($Info)
}
# Display numbered report
Write-Host ""
Write-Host "Image Analysis Results" -ForegroundColor Cyan
Write-Host "=============================================="
$Index = 1
foreach ($Item in $Results) {
$Number = "{0:D2}" -f $Index
if (-not $Item.Readable) {
Write-Host "$Number - $($Item.Name) | Size: $($Item.SizeMB) MB | UNREADABLE | $($Item.Error)" -ForegroundColor Red
}
elseif ($Item.IsGolden) {
Write-Host (
"$Number - $($Item.Name) | $($Item.Resolution) | Size: $($Item.SizeMB) MB | Ratio: $($Item.Ratio) | " +
"GOLDEN RATIO OR ABOVE"
) -ForegroundColor Green
}
else {
Write-Host (
"$Number - $($Item.Name) | $($Item.Resolution) | Size: $($Item.SizeMB) MB | Ratio: $($Item.Ratio) | " +
"BELOW GOLDEN RATIO"
) -ForegroundColor Yellow
}
$Index++
}
# Select files below the golden ratio
$DeleteCandidates = @(
$Results |
Where-Object {
$_.Readable -and
-not $_.IsGolden
}
)
$GoldenCount = @(
$Results |
Where-Object {
$_.Readable -and
$_.IsGolden
}
).Count
$UnreadableCount = @(
$Results |
Where-Object {
-not $_.Readable
}
).Count
$AnalyzedImageSizeBytes = [int64]0
foreach ($Item in $Results) {
$AnalyzedImageSizeBytes += [int64]$Item.FileSize
}
$DeleteSizeBytes = [int64]0
foreach ($Item in $DeleteCandidates) {
$DeleteSizeBytes += [int64]$Item.FileSize
}
$ProtectedCount = $GoldenCount
$ProtectedImageSizeBytes = [int64]0
foreach ($Item in $Results) {
if ($Item.Readable -and $Item.IsGolden) {
$ProtectedImageSizeBytes += [int64]$Item.FileSize
}
}
if ($Results.Count -gt 0) {
$ProtectedFilePercent = [math]::Round((($ProtectedCount / $Results.Count) * 100), 2)
$DeleteFilePercent = [math]::Round((($DeleteCandidates.Count / $Results.Count) * 100), 2)
$UnreadableFilePercent = [math]::Round((($UnreadableCount / $Results.Count) * 100), 2)
}
else {
$ProtectedFilePercent = 0
$DeleteFilePercent = 0
$UnreadableFilePercent = 0
}
if ($AnalyzedImageSizeBytes -gt 0) {
$ProtectedImagePercent = [math]::Round((($ProtectedImageSizeBytes / $AnalyzedImageSizeBytes) * 100), 2)
$SelectedDeleteImagePercent = [math]::Round((($DeleteSizeBytes / $AnalyzedImageSizeBytes) * 100), 2)
}
else {
$ProtectedImagePercent = 0
$SelectedDeleteImagePercent = 0
}
Write-Host ""
Write-Host "Summary" -ForegroundColor Cyan
Write-Host "=============================================="
Write-Host "Total images : $($Results.Count)"
Write-Host "Protected total : $ProtectedCount ($ProtectedFilePercent %)"
Write-Host "Golden or above : $GoldenCount"
Write-Host "Below golden ratio : $($DeleteCandidates.Count) ($DeleteFilePercent %)"
Write-Host "Unreadable : $UnreadableCount ($UnreadableFilePercent %)"
Write-Host "Total image size : $(Convert-BytesToMB -Bytes $AnalyzedImageSizeBytes) MB"
Write-Host "Protected image size : $(Convert-BytesToMB -Bytes $ProtectedImageSizeBytes) MB ($ProtectedImagePercent %)"
Write-Host "Selected delete size : $(Convert-BytesToMB -Bytes $DeleteSizeBytes) MB ($SelectedDeleteImagePercent %)"
Write-Host "Folder size before : $(Convert-BytesToMB -Bytes $FolderSizeBeforeBytes) MB"
Write-Host "Folder files before : $FolderFileCountBefore"
Write-Host "Golden ratio : $GoldenRatio"
Write-Host ""
if ($DeleteCandidates.Count -eq 0) {
Write-Host "No images require backup or deletion." -ForegroundColor Green
Wait-ForKeyPress
exit 0
}
Write-Host "Files below the golden ratio:" -ForegroundColor Yellow
Write-Host ""
$DeleteIndex = 1
foreach ($Item in $DeleteCandidates) {
$Number = "{0:D2}" -f $DeleteIndex
Write-Host (
"$Number - $($Item.Name) | $($Item.Resolution) | Size: $($Item.SizeMB) MB | Ratio: $($Item.Ratio)"
) -ForegroundColor Yellow
$DeleteIndex++
}
Write-Host ""
Write-Host "The files listed above will be backed up to a ZIP archive." -ForegroundColor Yellow
Write-Host "Original files will be deleted only after ZIP verification." -ForegroundColor Yellow
Write-Host ""
$Confirmation = Read-Host "Type YES to continue"
if ($Confirmation -cne "YES") {
Write-Host ""
Write-Host "Operation cancelled. No files were modified." -ForegroundColor Cyan
Wait-ForKeyPress
exit 0
}
# Generate unique ZIP name
$TimeStamp = Get-Date -Format "yyyyMMdd_HHmmss"
$ZipFileName = "Image_Backup_Below_Golden_Ratio_$TimeStamp.zip"
$ZipPath = Join-Path $TargetFolder $ZipFileName
# Prevent accidental overwrite
if (Test-Path -LiteralPath $ZipPath) {
Write-Host "Backup ZIP already exists." -ForegroundColor Red
Wait-ForKeyPress
exit 1
}
Write-Host ""
Write-Host "Creating ZIP backup..." -ForegroundColor Cyan
try {
$FilesToArchive = @(
$DeleteCandidates |
ForEach-Object {
$_.File.FullName
}
)
Compress-Archive `
-LiteralPath $FilesToArchive `
-DestinationPath $ZipPath `
-CompressionLevel Optimal `
-Force
if (-not (Test-Path -LiteralPath $ZipPath -PathType Leaf)) {
throw "ZIP archive was not created."
}
}
catch {
Write-Host ""
Write-Host "ZIP creation failed." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host "No original files were deleted." -ForegroundColor Green
Wait-ForKeyPress
exit 1
}
# Verify ZIP contents before deletion
Write-Host "Verifying ZIP archive..." -ForegroundColor Cyan
try {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
$ArchiveEntryNames = @(
$Archive.Entries |
ForEach-Object {
[System.IO.Path]::GetFileName($_.FullName)
}
)
$MissingFiles = New-Object System.Collections.Generic.List[string]
foreach ($Item in $DeleteCandidates) {
if ($ArchiveEntryNames -notcontains $Item.Name) {
$MissingFiles.Add($Item.Name)
}
}
$Archive.Dispose()
if ($MissingFiles.Count -gt 0) {
throw "ZIP verification failed. Missing files: $($MissingFiles -join ', ')"
}
Write-Host "ZIP verification successful." -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host "ZIP verification failed." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host "Original files were NOT deleted." -ForegroundColor Green
Wait-ForKeyPress
exit 1
}
$ZipSizeBytes = [int64](Get-Item -LiteralPath $ZipPath -ErrorAction Stop).Length
# Delete originals only after successful verification
Write-Host ""
Write-Host "Deleting original files..." -ForegroundColor Cyan
$DeletedCount = 0
$DeleteFailedCount = 0
$ActualDeletedSizeBytes = [int64]0
foreach ($Item in $DeleteCandidates) {
try {
Remove-Item -LiteralPath $Item.File.FullName -Force
$DeletedCount++
$ActualDeletedSizeBytes += [int64]$Item.FileSize
Write-Host "Deleted: $($Item.Name)" -ForegroundColor DarkYellow
}
catch {
$DeleteFailedCount++
Write-Host "Failed: $($Item.Name)" -ForegroundColor Red
Write-Host "Reason: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Operation completed." -ForegroundColor Cyan
Write-Host "=============================================="
Write-Host "ZIP backup : $ZipPath"
Write-Host "ZIP backup size : $(Convert-BytesToMB -Bytes $ZipSizeBytes) MB"
Write-Host "Files deleted : $DeletedCount"
Write-Host "Files protected : $ProtectedCount"
Write-Host "Deletion failures : $DeleteFailedCount"
$FolderSizeAfterBytes = Get-FolderSizeBytes -Path $TargetFolder
$FolderFileCountAfter = Get-FolderFileCount -Path $TargetFolder
$FolderSizeBeforeMB = Convert-BytesToMB -Bytes $FolderSizeBeforeBytes
$FolderSizeAfterMB = Convert-BytesToMB -Bytes $FolderSizeAfterBytes
$ActualDeletedSizeMB = Convert-BytesToMB -Bytes $ActualDeletedSizeBytes
$ZipSizeMB = Convert-BytesToMB -Bytes $ZipSizeBytes
if ($AnalyzedImageSizeBytes -gt 0) {
$ActualImageReductionPercent = [math]::Round((($ActualDeletedSizeBytes / $AnalyzedImageSizeBytes) * 100), 2)
}
else {
$ActualImageReductionPercent = 0
}
if ($Results.Count -gt 0) {
$ActualDeletedFilePercent = [math]::Round((($DeletedCount / $Results.Count) * 100), 2)
$ActualProtectedFilePercent = [math]::Round((($ProtectedCount / $Results.Count) * 100), 2)
}
else {
$ActualDeletedFilePercent = 0
$ActualProtectedFilePercent = 0
}
$NetReclaimedBytes = $FolderSizeBeforeBytes - $FolderSizeAfterBytes
if ($FolderSizeBeforeBytes -gt 0) {
$NetFolderReductionPercent = [math]::Round((($NetReclaimedBytes / $FolderSizeBeforeBytes) * 100), 2)
$GrossFolderReductionPercent = [math]::Round((($ActualDeletedSizeBytes / $FolderSizeBeforeBytes) * 100), 2)
}
else {
$NetReclaimedBytes = [int64]0
$NetFolderReductionPercent = 0
$GrossFolderReductionPercent = 0
}
if ($ActualDeletedSizeBytes -gt 0) {
$ZipCompressionPercent = [math]::Round(((1 - ($ZipSizeBytes / $ActualDeletedSizeBytes)) * 100), 2)
$ZipToDeletedRatioPercent = [math]::Round((($ZipSizeBytes / $ActualDeletedSizeBytes) * 100), 2)
}
else {
$ZipCompressionPercent = 0
$ZipToDeletedRatioPercent = 0
}
Write-Host ""
Write-Host "Operation completed." -ForegroundColor Cyan
Write-Host "=============================================="
Write-Host "ZIP backup : $ZipPath"
Write-Host "ZIP backup size : $ZipSizeMB MB"
Write-Host "Files deleted : $DeletedCount ($ActualDeletedFilePercent %)"
Write-Host "Files protected : $ProtectedCount ($ActualProtectedFilePercent %)"
Write-Host "Deletion failures : $DeleteFailedCount"
Write-Host "Folder files before : $FolderFileCountBefore"
Write-Host "Folder files after : $FolderFileCountAfter"
Write-Host ""
Write-Host "Size and reduction statistics" -ForegroundColor Cyan
Write-Host "----------------------------------------------"
Write-Host "Folder size before : $FolderSizeBeforeMB MB"
Write-Host "Actual deleted size : $ActualDeletedSizeMB MB"
Write-Host "ZIP backup size : $ZipSizeMB MB"
Write-Host "Folder size after : $FolderSizeAfterMB MB"
Write-Host "Net space reclaimed : $(Convert-BytesToMB -Bytes $NetReclaimedBytes) MB"
Write-Host "Image data removed : $ActualImageReductionPercent %"
Write-Host "Gross folder reduction : $GrossFolderReductionPercent %"
Write-Host "Net folder reduction : $NetFolderReductionPercent %"
Write-Host "ZIP size / deleted data: $ZipToDeletedRatioPercent %"
Write-Host "ZIP compression saving : $ZipCompressionPercent %"
if ($DeleteFailedCount -eq 0) {
Write-Host "All selected files were backed up and deleted successfully." -ForegroundColor Green
}
else {
Write-Host "Backup completed, but some original files could not be deleted." -ForegroundColor Yellow
}
Write-Host ""
Wait-ForKeyPress