AndyMcMars
AndyMcMars

Reputation: 21

Powershell error "A generic error occurred in GDI+." when trying to overwrite the image after changing its properties

i'm trying to batch edit lot of photos that i have saved on an HDD. Basically i need to change the property of the Date Taken so that i can index them. I will use the name of the file to get the acutal date.

I'm writing the script with Powershell and i can't get it to work with the .save() command at the end.

Here what i wrote so far

[reflection.assembly]::LoadWithPartialName("System.Drawing")

$filePath = 'imagePath.jpg'

# Load the image as a copy
$originalPic = New-Object System.Drawing.Bitmap($filePath)
$pic = $originalPic.Clone()

# Dispose the original bitmap object to release the file handle
$originalPic.Dispose()
$originalPic = $null

$props = @("306", "20531", "36867", "36868")

$defaultDateTime = Get-Date "2018-06-28"
$dateString = $defaultDateTime.ToString("yyyy:MM:dd HH:mm:ss`0")
$byteArray = [System.Text.Encoding]::ASCII.GetBytes($dateString)

foreach($prop in $props){
    
    try{
        $propertyItem = $pic.GetPropertyItem($prop)
    }
    catch{
        $propertyItem = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([System.Drawing.Imaging.PropertyItem])
        $propertyItem.Id = $prop
    }
    
    $propertyItem.Type = 2
    $propertyItem.Len = $byteArray.Length
    $propertyItem.Value = $byteArray

    
    $pic.SetPropertyItem($propertyItem)
}

$pic.Save($filePath)  # Save the modified image with the new property

Everything it's working fine except the last step where i overwrite it. I understand that i could give a different name and that does indeed work but i don't want to create a copy for each image.

I thought that with the .clone i would obtain a copy and after the dispose, it would just close the object but it seems like it's always there.

This is what i get as error

Exception calling "Save" with "1" argument(s): "A generic error occurred in GDI+."
At C:\Users\user\Desktop\Random Tools\Powershell\FixMyPic.ps1:40 char:1
+ $pic.Save($filePath)  # Save the modified image with the new property
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ExternalException

Thanks everyone.

I also set it as $null after the dispose because it seems like it's just getting rid of the properties but the object is still there (i guess it's normal) but i thought it would be even better let's say to set it null. Object still loaded after dispose.

I tried to follow on some of the .NET related issues, they make use of the "using" but i don't think it will change much.

I tried opening and closing the ISE as per this question

EDIT:

i can also see that even with the use of dispose the image is still being held by Powershell and that's what's making it stop from overwriting

enter image description here

Upvotes: 2

Views: 233

Answers (2)

mklement0
mklement0

Reputation: 439822

Mathias' helpful answer explains your problem well and offers an effective solution.

Let me provide an alternative that builds on your own approach, using the v5+ intrinsic ::new() method for constructor calls instead of New-Object:

Add-Type -AssemblyName System.Drawing

# Determine the *full* input file path.
# Note: This is important, because .NET's working dir. usually differs from PowerShell's.
$filePath = "$PWD\image.jpg"

# Load the image from disk, then create an independent, in-memory copy:
$pic = & {

  # Load the image from disk; this keeps the file locked and prevents
  # writes to the same file until .Dispose() is called.
  # Note: This calls the following constructor overload:
  #         public Bitmap (string filename);
  $originalPic = [System.Drawing.Bitmap]::new($filePath)

  # Construct a *new, independent instance* from the original [System.Drawing.Bitmap]
  # instance and *output it* (will be captured in $pic).
  # Note: This calls the following constructor overload:
  #         public Bitmap (System.Drawing.Image original);
  [System.Drawing.Bitmap]::new($originalPic)

  # Dispose of the from-disk image, which unlocks the file.
  $originalPic.Dispose() 
}

# ... Now you can work with $pic

# Save the changes an dispose of the in-memory image too.
$pic.Save($filePath); $pic.Dispose()

Note:

Upvotes: 0

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174825

Your current approach - passing a file path to the [Bitmap] constructor and then subsequently cloning the resulting object - appears to also clone the file handle created, resulting in a sharing violation when you call $pic.Save, as $pic itself now has a read-lock on the same file.

$originalPic = New-Object System.Drawing.Bitmap($filePath) # exclusive read handle count: 1
$pic = $originalPic.Clone()                                # exclusive read handle count: 2

$originalPic.Dispose()                                     # exclusive read handle count: 1
$originalPic = $null                                       # still not in the clear...

Under the hood all the file system I/O is handled by Windows' native GDI+ API, so you get a "generic GDI+ error" because it couldn't complete the file write and doesn't know that its because the calling application has an exclusive handle on the file.

To work around this, load the original picture with [Image]::FromFile, then pass the resulting image object to the [Bitmap] constructor - at which point the constructor will copy the image data from the in-memory object rather than recreating the file handle, and you should be free to obtain a write handle to the file after disposing the original:

$originalPic = [System.Drawing.Image]::FromFile($filePath) # exclusive read handle count: 1
$pic = [System.Drawing.Bitmap]::new($originalPic)          # exclusive read handle count: 1
$originalPic.Dispose()                                     # exclusive read handle count: 0

As an aside, I'd recommend wrapping your code in a try/finally statement to ensure file system resources are always disposed of:

# Load assembly
Add-Type -AssemblyName System.Drawing

# Resolve file path
$filePath = Resolve-Path 'imagePath.jpg'

try {
    # Load image from file, copy contents to new image object
    $originalPic = [System.Drawing.Image]::FromFile($filePath)
    $pic = [System.Drawing.Bitmap]::new($originalPic)

    # Dispose the original bitmap object to release the file handle
    $originalPic.Dispose()
    $originalPic = $null

    # Update properties
    $props = @("306", "20531", "36867", "36868")

    $defaultDateTime = Get-Date "2018-06-28"
    $dateString = $defaultDateTime.ToString("yyyy:MM:dd HH:mm:ss`0")
    $byteArray = [System.Text.Encoding]::ASCII.GetBytes($dateString)

    foreach ($prop in $props) {
    
        try {
            $propertyItem = $pic.GetPropertyItem($prop)
        }
        catch {
            $propertyItem = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([System.Drawing.Imaging.PropertyItem])
            $propertyItem.Id = $prop
        }
    
        $propertyItem.Type = 2
        $propertyItem.Len = $byteArray.Length
        $propertyItem.Value = $byteArray
    
        $pic.SetPropertyItem($propertyItem)
    }

    $pic.Save($filePath)  # Save the modified image with the new property
}
finally {
    # clean up regardless of errors
    $originalPic, $pic | Where-Object { $_ -is [IDisposable] } | ForEach-Object Dispose
}

Upvotes: 2

Related Questions