windowspowershellbatch-filecommand-line

Is there a way to delete all images in a folder which are less than a certain width via Windows Batch?


I'd like to write a simple program to extract the windows spotlight images from the system directory where they live to a folder where I can more easily get at them. So far I have this:

@ECHO OFF
if not exist "%homedrive%%homepath%\Downloads\Spotlight" mkdir %homedrive%%homepath%\Downloads\Spotlight
xcopy /s %homedrive%%homepath%\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets\ %homedrive%%homepath%\Downloads\Spotlight
ren %homedrive%%homepath%\Downloads\Spotlight\* *.jpg

Which successfully copies the contents of the spotlight directory into a new folder, then renames all the files to make them JPGs. However, a number of miscellaneous icons come too, as well as the 'portrait mode' spotlight images. I'd like to delete them by checking each .jpg in the output folder to see if its width is less than 1920 pixels, and deleting the image if so.

I went to AI to try and solve my problem, but failed:

set "SpotlightFolder=%USERPROFILE%\Downloads\Spotlight"
:: Iterate over all .jpg files and delete those with width smaller than 1920 pixels
for %%F in ("%SpotlightFolder%\*.jpg") do (
    for /f %%W in ('powershell -command "& {(Get-ImageDimensions '%%~F').Width}"') do set "Width=%%W"
    if !Width! lss 1920 (
        echo Deleting: %%~nxF
        del "%%F"
    ) else (
        echo Keeping: %%~nxF
    )
)

:: PowerShell script to get image dimensions
<#
function Get-ImageDimensions {
    param([string]$filePath)
    $image = [System.Drawing.Image]::FromFile($filePath)
    $width = $image.Width
    $image.Dispose()
    $width
}
#>

This returns a nebulous 'The syntax of the command is incorrect' error, and deletes all the images in the folder, regardless of dimension. Any help would be appreciated.

The solution is marked below, but for anyone with the same question, this is the complete script I've created to solve my problem; since the answer ended up being in Powershell script I rewrote the beginning of my code in that language.

$minWidth = 1900
$SpotlightFolder = "${env:USERPROFILE}\Downloads\Spotlight"


If(!(test-path -PathType container $SpotlightFolder))
{
      New-Item -ItemType Directory -Path $SpotlightFolder
}

xcopy /s "$env:USERPROFILE\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets\" $SpotlightFolder

#Get-ChildItem -Path $SpotlightFolder | Rename-Item -NewName { $_.Name -replace '$.*', '.jpg' }
#Rename all items to make them JPGs, accounting for the possibility that they already exist (avoid 'cannot overwrite via rename' error)
Get-ChildItem -Path $SpotlightFolder | ForEach-Object {
    $newName = $_.BaseName + ".jpg"
    $newPath = Join-Path -Path $_.DirectoryName -ChildPath $newName

    if (-not (Test-Path $newPath)) {
        Rename-Item -Path $_.FullName -NewName $newName
    }
}
#Delete all items which do not have an extension (those which already existed and didn't get renamed)
Get-ChildItem -Path $SpotlightFolder | Where-Object { -not $_.Extension } | Remove-Item -Force


$widthColumnNum = 176 # or 165 or some other value, see below for details
$shell = New-Object -COMObject Shell.Application
$shellfolder = $shell.Namespace($SpotlightFolder)


#Uncomment the Write-Output code and run to see Windows' list of attributes. Find the number corresponding to 'Width'. Replace '176' if it is different for you.
$objFolder = (New-Object -ComObject Shell.Application).Namespace('c:\')
for ($columnNumber = 0; $columnNumber -lt 500; ++$columnNumber) 
{ 
    $columnName = $objFolder.GetDetailsOf($objFolder.Items, $columnNumber) 
    if ($columnName)
    {
        #Write-Output "$(([string]$columnNumber).PadLeft(3)) $columnName"
    }
}

Get-ChildItem -Filter *.jpg $SpotlightFolder | Where-Object {
    $shellfile = $shellfolder.ParseName($_.Name)
    $w = $shellfolder.GetDetailsOf($shellfile, $widthColumnNum)
    [int]([regex]::Match($w, "\d+").Value) -lt $minWidth
} | Remove-Item #-WhatIf

Thanks for your help.


Solution

  • Using System.Drawing.Image might not be very efficient because it actually decodes and loads the whole image. The Windows Shell can read those metadata just by reading the header and other related sections so performance should be better. In fact it must be better, otherwise the details view in Windows Explorer will suffer. They probably even cache the info for reuse the next time

    The metadata can be accessed directly using Shell COM object's Folder.GetDetailsOf() method so to get the images' widths cheaply you can use this simple PowerShell script:

    $minWidth = 1920
    $SpotlightFolder = "${env:USERPROFILE}\Downloads\Spotlight"
    
    $widthColumnNum = 176 # or 165 or some other value, see below for details
    $shell = New-Object -COMObject Shell.Application
    $shellfolder = $shell.Namespace($SpotlightFolder)
    
    Get-ChildItem -Filter *.jpg $SpotlightFolder | Where-Object {
        $shellfile = $shellfolder.ParseName($_.Name)
        $w = $shellfolder.GetDetailsOf($shellfile, $widthColumnNum)
        [int]([regex]::Match($w, "\d+").Value) -lt $minWidth
    } | Remove-Item -WhatIf
    

    Basically this lists all *.jpg files in the specified path, filter the ones with Width less than $minWidth to pass to Remove-Item. Remove the -WhatIf option to do actual removing

    The column number 165 is magic which is taken from here, or 176 based on this. You can run the PowerShell script in What options are available for Shell32.Folder.GetDetailsOf(..,..)? to find the actual value on your system in case it's different:

    $objFolder = (New-Object -ComObject Shell.Application).Namespace('c:\')
    $(for ($columnNumber = 0; $columnNumber -lt 500; ++$columnNumber) 
    { 
        $columnName = $objFolder.GetDetailsOf($objFolder.Items, $columnNumber) 
        if ($columnName)
        {
            [PsCustomObject]@{
                 ColumnNumber = ([string]$columnNumber).PadLeft(3);
                 ColumnName = $columnName
            }
        }
    }) | where { $_.ColumnName -like "*width*" -or $_.ColumnName -like "*len*" }
    

    I've already tested it on my PC1 and it works, the image width is field 176 as expected. Of course you can also automate the method to get that magic number but I'm keeping it simple here. You can remove the where {} command to list all the available columns

    For more information about metadata reading check

    Because it uses COM objects, it can be called from any languages that support COM objects, for example a JS, VBS or hybrid VBS/hybrid JS


    1Here's the sample output on my PC:

    ColumnNumber ColumnName
    ------------ ----------
     27          Length
    165          Filename
    176          Width
    262          Focal length
    263          35mm focal length
    265          Lens maker
    266          Lens model
    322          Frame width