Pictures and videos consume the most space in the redirected folders of my users. While I don’t have a great solution for videos yet, I cobbled together a few PowerShell scripts into a single solution. The script below will search a root folder (\\Test\FR\Staff\) and work through each user’s subfolder (\\Test\FR\Staff\jmoody\). It will find any picture greater than your specified resolution and reduce it to a more appropriate size. Even small environments could save several 100 GBs!
I wanted to see how big of a difference this re-sizing could make. I modified the script output to keep the old photo for comparison. To show you the size difference, look at the screenshot below. The new photo is almost 30x smaller than the original photo! On my 1920×1080 monitor – I can see zero difference in quality.
In the very first line of the PowerShell script, you will need to set your domain name. The next three $UserFolders lines are examples searches. The first will convert pictures only in the Jmoody subfolder. The second will convert the first 10 folders in the root folder. The final example will convert all folders. As you implement this script in your environment, I recommend a roll-out using those three examples. For safety, keep extending the select -first example until you are comfortable converting every picture that is in your folder redirection share.
Here is the script – I’ll meet you with some more information below it. If your mouse wheel breaks while scrolling down, remember that you can still use the Page Down button on your keyboard. 🙂
$Domain = "Test.local" #$UserFolders = Get-ChildItem -Path \\Test\FR\Staff\ | where Fullname -Like \\Test\FR\Staff\Jmoody #$UserFolders = Get-ChildItem -Path \\Test\FR\Staff\ | select -First 10 #$UserFolders = Get-ChildItem -Path \\Test\FR\Staff\ Function Set-Owner { <# .SYNOPSIS Changes owner of a file or folder to another user or group. .DESCRIPTION Changes owner of a file or folder to another user or group. .PARAMETER Path The folder or file that will have the owner changed. .PARAMETER Account Optional parameter to change owner of a file or folder to specified account. Default value is 'Builtin\Administrators' .PARAMETER Recurse Recursively set ownership on subfolders and files beneath given folder. .NOTES Name: Set-Owner Author: Boe Prox Version History: 1.0 - Boe Prox - Initial Version .EXAMPLE Set-Owner -Path C:\temp\test.txt Description ----------- Changes the owner of test.txt to Builtin\Administrators .EXAMPLE Set-Owner -Path C:\temp\test.txt -Account 'Domain\bprox Description ----------- Changes the owner of test.txt to Domain\bprox .EXAMPLE Set-Owner -Path C:\temp -Recurse Description ----------- Changes the owner of all files and folders under C:\Temp to Builtin\Administrators .EXAMPLE Get-ChildItem C:\Temp | Set-Owner -Recurse -Account 'Domain\bprox' Description ----------- Changes the owner of all files and folders under C:\Temp to Domain\bprox #> [cmdletbinding( SupportsShouldProcess = $True )] Param ( [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [Alias('FullName')] [string[]]$Path, [parameter()] [string]$Account = 'Builtin\Administrators', [parameter()] [switch]$Recurse ) Begin { #Prevent Confirmation on each Write-Debug command when using -Debug If ($PSBoundParameters['Debug']) { $DebugPreference = 'Continue' } Try { [void][TokenAdjuster] } Catch { $AdjustTokenPrivileges = @" using System; using System.Runtime.InteropServices; public class TokenAdjuster { [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); [DllImport("kernel32.dll", ExactSpelling = true)] internal static extern IntPtr GetCurrentProcess(); [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct TokPriv1Luid { public int Count; public long Luid; public int Attr; } internal const int SE_PRIVILEGE_DISABLED = 0x00000000; internal const int SE_PRIVILEGE_ENABLED = 0x00000002; internal const int TOKEN_QUERY = 0x00000008; internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; public static bool AddPrivilege(string privilege) { try { bool retVal; TokPriv1Luid tp; IntPtr hproc = GetCurrentProcess(); IntPtr htok = IntPtr.Zero; retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_ENABLED; retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); return retVal; } catch (Exception ex) { throw ex; } } public static bool RemovePrivilege(string privilege) { try { bool retVal; TokPriv1Luid tp; IntPtr hproc = GetCurrentProcess(); IntPtr htok = IntPtr.Zero; retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_DISABLED; retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); return retVal; } catch (Exception ex) { throw ex; } } } "@ Add-Type $AdjustTokenPrivileges } #Activate necessary admin privileges to make changes without NTFS perms [void][TokenAdjuster]::AddPrivilege("SeRestorePrivilege") #Necessary to set Owner Permissions [void][TokenAdjuster]::AddPrivilege("SeBackupPrivilege") #Necessary to bypass Traverse Checking [void][TokenAdjuster]::AddPrivilege("SeTakeOwnershipPrivilege") #Necessary to override FilePermissions } Process { ForEach ($Item in $Path) { Write-Verbose "FullName: $Item" #The ACL objects do not like being used more than once, so re-create them on the Process block $DirOwner = New-Object System.Security.AccessControl.DirectorySecurity $DirOwner.SetOwner([System.Security.Principal.NTAccount]$Account) $FileOwner = New-Object System.Security.AccessControl.FileSecurity $FileOwner.SetOwner([System.Security.Principal.NTAccount]$Account) $DirAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity $FileAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity $AdminACL = New-Object System.Security.AccessControl.FileSystemAccessRule('Builtin\Administrators','FullControl','ContainerInherit,ObjectInherit','InheritOnly','Allow') $FileAdminAcl.AddAccessRule($AdminACL) $DirAdminAcl.AddAccessRule($AdminACL) Try { $Item = Get-Item -LiteralPath $Item -Force -ErrorAction Stop If (-NOT $Item.PSIsContainer) { If ($PSCmdlet.ShouldProcess($Item, 'Set File Owner')) { Try { $Item.SetAccessControl($FileOwner) } Catch { Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Directory.FullName)" $Item.Directory.SetAccessControl($FileAdminAcl) $Item.SetAccessControl($FileOwner) } } } Else { If ($PSCmdlet.ShouldProcess($Item, 'Set Directory Owner')) { Try { $Item.SetAccessControl($DirOwner) } Catch { Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Parent.FullName)" $Item.Parent.SetAccessControl($DirAdminAcl) $Item.SetAccessControl($DirOwner) } } If ($Recurse) { [void]$PSBoundParameters.Remove('Path') Get-ChildItem $Item -Force | Set-Owner @PSBoundParameters } } } Catch { Write-Warning "$($Item): $($_.Exception.Message)" } } } End { #Remove priviledges that had been granted [void][TokenAdjuster]::RemovePrivilege("SeRestorePrivilege") [void][TokenAdjuster]::RemovePrivilege("SeBackupPrivilege") [void][TokenAdjuster]::RemovePrivilege("SeTakeOwnershipPrivilege") } } foreach ($UserFolder in $UserFolders){ $FullName = $UserFolder.FullName [reflection.assembly]::LoadWithPartialName("System.Drawing") $SizeLimit=1400 # required size of picture's long side $logfile="resizelog.txt" # log file for errors $toresize=$args[0] # list of directories to find and resize images. can be empty $toresize=@($FullName+"\") echo $toresize # visual control #Write-Host -NoNewLine ("Press any key.") # Optional "Press any key" #$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") #Write-Host "" $error.clear() # first part. find and resize jpgs Get-ChildItem -recurse $toresize -include *.jpg | foreach { $OldBitmap = new-object System.Drawing.Bitmap $_.FullName # open found jpg if ($error.count -ne 0) { # error handling $error | out-file $logfile -append -encoding default $error[($error.count-1)].TargetObject | out-file $logfile -append -encoding default echo $_>>$logfile $error.clear() } $LongSide=$OldBitmap.Width # locating long side of picture if ($OldBitmap.Width -lt $OldBitmap.Height) { $LongSide=$OldBitmap.Height } if ($LongSide -gt $SizeLimit) { # if long side is greater than our limit, process jpg if ($OldBitmap.Width -lt $OldBitmap.Height) { # calculate dimensions of picture resize to $newH=$SizeLimit $newW=[int]($OldBitmap.Width*$SizeLimit/$OldBitmap.Height) } else { $newW=$SizeLimit $newH=[int]($OldBitmap.Height*$newW/$OldBitmap.Width) } $NewBitmap = new-object System.Drawing.Bitmap $newW,$newH # create new bitmap $g=[System.Drawing.Graphics]::FromImage($NewBitmap) $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic # use high quality resize algorythm $g.DrawImage($OldBitmap, 0, 0, $newW, $newH) # resize our picture $name=$_.DirectoryName+"\"+$_.name+".new" # generating name for new picture $NewBitmap.Save($name, ([system.drawing.imaging.imageformat]::jpeg)) # save newly created resized jpg $NewBitmap.Dispose() # cleaning up our mess $OldBitmap.Dispose() $n=get-childitem $name if ($n.length -ge $_.length) { # if resized jpg has greater size than original Write-host -NoNewLine "+" # draw "+" $n.delete() # and delete it } else { # if resized jpg is smaller than original if ($n.Exists -and $_.Exists) { $name=$_.FullName $_.Delete() # delete original $n.MoveTo($name) # rename new file to original name (replace old file with new) echo ($Name + " " + $LongSide) # write its name for visual control } } } else { # if long side is smaller than limit, draw dot for visual Write-host -NoNewLine "." $OldBitmap.Dispose() } } #second part. process other than jpgs bitmaps Get-ChildItem -recurse $toresize -include *.bmp, *.tif, *.gif | foreach { $OldBitmap = new-object System.Drawing.Bitmap $_.FullName # open picture if ($error.count -ne 0) { # handle errors if any $error | out-file $logfile -append -encoding default $error[($error.count-1)].TargetObject | out-file $logfile -append -encoding default $error.clear() } $LongSide=$OldBitmap.Width # find picture's long side if ($OldBitmap.Width -lt $OldBitmap.Height) { $LongSide=$OldBitmap.Height } if ($LongSide -gt $SizeLimit) { # if long side is greater than our limit, process picture if ($OldBitmap.Width -lt $OldBitmap.Height) { # calculating new dimensions $newH=$SizeLimit $newW=[int]($OldBitmap.Width*$SizeLimit/$OldBitmap.Height) } else { $newW=$SizeLimit $newH=[int]($OldBitmap.Height*$newW/$OldBitmap.Width) } $NewBitmap = new-object System.Drawing.Bitmap $newW,$newH # create new bitmap $g=[System.Drawing.Graphics]::FromImage($NewBitmap) $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $g.DrawImage($OldBitmap, 0, 0, $newW, $newH) # copy resized image to it $name=$_.DirectoryName+"\"+$_.name.substring(0, $_.name.indexof($_.extension))+".jpg" # generating new name, jpg extension $NewBitmap.Save($name, ([system.drawing.imaging.imageformat]::jpeg)) # save new file $NewBitmap.Dispose() $OldBitmap.Dispose() $n=get-childitem $name if ($n.length -ge $_.length) { # if new file is longer than original, $n.delete() # delete new file Write-host -NoNewLine "+" # and draw "+" for beauty } else { # if new file is shorter than original (as we expect) echo ($Name + " " + $LongSide) # draw its name $_.Delete() # delete oroginal } } else { # if long side is less than our limit, try to recompress it to jpg $name=$_.DirectoryName+"\"+$_.name.substring(0, $_.name.indexof($_.extension))+".jpg" $OldBitmap.Save($name, ([system.drawing.imaging.imageformat]::jpeg)) $OldBitmap.Dispose() $n=get-childitem $name if ($n.length -ge $_.length) { # if new jpg is greater than original $n.delete() # delete new jpg Write-host -NoNewLine "-" # draw "-" for visual control } else { echo ($Name + " " + $LongSide) # draw new file name $_.Delete() # delete original } } } $Account = $Domain + "\" + $UserFolder.Name Set-Owner -Path $FullName -Recurse -Account "$Account" icacls.exe $FullName /q /c /t /reset Clear-Variable Account,Fullname }
This script is configured to find pictures with a long side greater than 1440. Search for $SizeLimit=1400 to change this value.
- If you wish to keep higher resolution pictures, increase this to a value like: 1680 or 1920
- If you wish to keep lower resolution pictures, decrease this to a value like: 1280 or 1152
Though it should go without saying, have a tested backup before doing any large scale modifications like this. If you are the kind of person who prefers not to test a backup, at least keep your resume on another server. 🙂
The only issue that I have had with this script is the converted picture has a new created/modified date (because it is a new file). If you use File Management Tasks for stale data cleanup, older pictures that would have been deleted soon are suddenly new again. Not a big deal as the 30x space savings more than makes up for that!
We have reached the credits portion of this broadcast. The Set-Owner function was written by Boe Prox. The re-sizing picture portion of the script was written by Oleg Medvedev. The remaining ten lines were written by yours truly.
If you run into issues, have improvements, or just like the script – I would love to hear from you in the comments. And while you are saving space, have you configured data duplication for your file shares yet?
Hey !
Thank you for that cool script.
Unfortunaly it creates a new image and we loose metadata (like camera model, shot date, etc.)…
Do you think it can be copied ?
Regads,
I’m not sure of a way… if you do figure it out, I would love to know about it though!
Hi,
Thanks for this script Joseph 🙂
But it creates a new picture, it keeps NTFS ACL, but we loose jpg metadata and it’s very sad !
Do you think you can add this feature to your script ?
Regards
Dav
Hi Joseph/David. Good tips on the script and the imageresizer app. Another option is to use the app photosizer (the free version is all you need). You point it at the root folder, specify the options such as reduce picture quality or resize to a %, and it works away. Saved me nearly 100GB on a shared drive used by project managers on a building site thousands of 5MB photos reduced to 1MB.
I’ll give the script and imageresizer app a try next time for a change.
Thanks for the tip John! Let me know if you have any issues with the resizing script.
Nice, i have used ImageMagick to do much the same using a batch file run as a scheduled task.
Net use P: \\PATH.TO.SHARE
Forfiles /p P:\002\ /m *.jpg /s /C “cmd /c if @fsize GTR 512000 mogrify -resize 1024×1024 @path && Echo @path”
Net use P: /d
Notes:
– As provided, it will resize any jpgs of file size > 512kb to fit into a 1024px x 1024px image.
– ‘mogrify’ is part of the ImageMagick suite (which needs to be in the system path).
– Forfiles is a handy command which can walk down the subdirectories of a given starting point.
Your solution looks a bit easier to implement than mine! 🙂
True, but it relies on 3rd party software which may not always be an option, even if open source.
For the video side of things, I don’t see why you couldn’t do something similar, but instead of invoking ImageMagick, use Handbrake. (https://trac.handbrake.fr/wiki/CLIGuide). I would however want to make sure that you run this on a separate server as processing the video would take time and a lot of CPU cycles.
This may annoy users though if they aren’t expecting their 4k cat videos to be compressed.
On a separate note, I am looking to implement folder redirection soon, so have been enjoying this series.
Now that is a very interesting idea Adam! If you ever want to work up a proof of concept script, I would love to see it.
And I have three more articles planned for this series – just no time to write them. 🙁
Nice blog, and I like the script, and will give a try in my environment. For the last few years I’ve been using the Fotosizer application. It’s free and does a good job with a lot of resize options (go easy on the install – iirc the default options add other apps). I have been confident to let it run for 3-4000 pics at a time with good results. Resizing and overwriting original to 1024×768 is great for me – Most of the users are heavy on photo storage but have their cameras defaulting to 10Mpixel or similar and rarely print off more than a 2 inch thumbnail. The fact that file properties change means that any folders set to offline use are quicker to replicate afterwards on user laptops 🙂
Unfortunately the task of resizing seems to be getting more and more futile as they’re all now using the cameras for taking video and those things are massive. 🙁
Thank you Joolz! I feel the same way – we solve one problem just to discover a dozen more!
A picture says a thousand words… yes.
Unfortunately as network admins, a video says a thousand pictures 😀
I installed it on my Windows 7 pc. No need to install on server. I search for pictures via mapped drive or unc path for pictures of desired size. From the result I select all pictures, right click then click resize pictures. Very simple to use and I like the fact that it built right into your context menu. Give it a try.
The Microsoft image resizer tool was part of the Windows XP power toys add on. I noticed that it only works on Windows XP and Microsoft have remove the utility. However, on my Windows 7 pc I’m using an “exact clone” of this tool which you can fine here http://imageresizer.codeplex.com/
Been using this tool for years without problem. The resizer feature is built right into your context menu. All you need to do is perform a search of the pictures, select all pictures, right click then click on resize. Once you click on resize there are sizing options that you can seletc.
Hi David,
To use the clone tool on Windows 7 or later, do we need to install it on each computer or can we install only in the server?
Thanks for the tip David! Do you have a link?
Microsoft has a free picture resizer that I’ve been using for years. Works wonderful and it even lets you what size to reduce to.