Search code examples
vb.netgraphicspngmetadata

Extra characters/bytes in metadata after saving a PNG image


I am trying to create metadata to be stored in a PNG Image file, depending on the value of a parameter called sc_status.

The code is as follows:

Dim qualityParam As Object
Dim encoderParams As Object = New Imaging.EncoderParameters(1)
Dim ImgCodec As Imaging.ImageCodecInfo

ImgCodec = GetEncoderInfo("image/png")
qualityParam = New Imaging.EncoderParameter(Imaging.Encoder.ColorDepth, 32L)
encoderParams.Param(0) = qualityParam

'---
' img_src Image is created here
' file_name String is created here
'---

' Creating the PropertyItem
Dim propit As Imaging.PropertyItem = CType(System.Runtime.Serialization.FormatterServices.GetUninitializedObject(GetType(Imaging.PropertyItem)), Imaging.PropertyItem)

propit.Id = 270 '0x010E = Image description
propit.Type = 2

If sc_status = 3 Then
    propit.Value = System.Text.Encoding.UTF8.GetBytes("HQ")
ElseIf sc_status = 5 Then
    propit.Value = System.Text.Encoding.UTF8.GetBytes("LQ")
Else
    propit.Value = System.Text.Encoding.UTF8.GetBytes("UQ")
End If

' Storing the PropertyItem
img_src.SetPropertyItem(propit)

' Saving png
img_src.Save(file_name, ImgCodec, encoderParams)

When I have a look at what is stored in the PNG chunks, I expect to have in byte sequence [72, 81, 0] or [76, 81, 0] or [85, 81, 0], corresponding to the string "LQ","HQ","UQ" plus the vbNullChar which is automatically added at the end of the PNG chunk.

But for a reason I ignore, I sometimes have a longer byte sequence, e.g. [72, 81, 28, 8, 1, 0] which gives - after using System.Text.Encoding.UTF8.GetString() - the string:

HQ & ChrW(28) & vbBack & ChrW(1) & vbNullChar

or sometimes [72, 81, 22, 8, 1, 0], or sometimes [72, 81, 19, 8, 1, 0] or sometimes [72, 81, 23, 8, 1, 0].

I don't understand why sometimes extra bytes are added in the metadata during the img_src.Save() procedure.
What am I doing wrong? Any help is very welcome!


Solution

  • PropertyTagImageDescription is defined as a null-terminated ASCII string (PropertyTagTypeASCII).
    As described in the Documentation, PropertyItem.Type PropertyTagTypeASCII:

    Specifies that Value is a null-terminated ASCII string. If you set the type data member to ASCII type, you should set the Len property to the length of the string including the null terminator. For example, the string "Hello" would have a length of 6.

    A few details:

    • The PropertyItem.Value property stores data as a byte array. Even though the Docs refer to an ASCII string, storing the bytes of a UTF-8 encoded strings, retrieved calling Encoding.UTF8.GetBytes(), is no forbidden though. Just terminate the string and set the correct number of bytes stored: as mentioned Value store just bytes.
    • The PropertyItem.Len property must be set to the length of the string, so to the length of the byte, thus the length of the Value property.

    ► There's no reason to define the Types as Object, as in:

    Dim qualityParam As Object
    Dim encoderParams As Object = New Imaging.EncoderParameters(1)
    

    Declare these Types for what they are.

    Sample implementation, using UTF-8 encoded strings:

    Imports System.Drawing.Imaging
    Imports System.Text
    
    '0x010E = Image description
    Dim imageDescriptionPropItem = &H10E
    ' Property Type 2: null-terminated string
    Dim PropertyTagTypeASCII As short = 2
    
    Dim encoderParams As New EncoderParameters(1)
    Dim ImgCodec = ImageCodecInfo.GetImageEncoders().
                   FirstOrDefault(Function(enc) enc.FormatID = ImageFormat.Png.Guid)
    If ImgCodec Is Nothing Then
        Throw New FormatException("Invalid format")
    End If
    encoderParams.Param(0) = New EncoderParameter(System.Drawing.Imaging.Encoder.ColorDepth, 32L)
    
    Dim imagePath = [Image Source Path]
    Dim imageDestinationPath = [Image Destination Path]
    Dim imageSource = Image.FromStream(
        New MemoryStream(File.ReadAllBytes(imageSourcePath)), False, False)
    
    Dim propItem As PropertyItem = DirectCast(FormatterServices.GetUninitializedObject(
        GetType(PropertyItem)), PropertyItem)
    
    propItem.Id = imageDescriptionPropItem 
    propItem.Type = PropertyTagTypeASCII
    
    Dim description = String.Empty
    Select Case sc_status
        Case 3
            description = "HQ"
        Case 5
            description = "LQ"
        Case 100
            ' Test string, in Russian :)
            description = "Тестовая строка"
        Case Else
            description = "UQ"
    End Select
    
    ' Length of the string including the terminator: mandatory
    propItem.Value = Encoding.UTF8.GetBytes(description & ChrW(0))
    propItem.Len = propItem.Value.Length
    
    imageSource.SetPropertyItem(propItem)
    imageSource.Save(imageDestinationPath, ImgCodec, encoderParams)
    
     
    
    ' Load it back, to check what's what
    Dim imageEncoded = Image.FromStream(New MemoryStream(File.ReadAllBytes(imageDestinationPath)))
    Dim propItemSaved = imageEncoded.GetPropertyItem(imageDescriptionPropItem)
    Dim descr = Encoding.UTF8.GetString(propItemSaved.Value).TrimEnd(ChrW(0))