💾 Archived View for g.mikf.pl › gemlog › 2022-12-13-winforms-jpeg.gmi captured on 2023-04-26 at 13:00:24. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

Writing a JPEG compressor GUI in .NET Visual Basic quickly

2022-12-13

Private valueChangeSource As Object = Nothing

Private Sub TrackBar1_ValueChanged(sender As Object, e As EventArgs) Handles TrackBar1.ValueChanged
    If valueChangeSource Is Nothing Then
        valueChangeSource = TrackBar1
    ElseIf valueChangeSource Is TrackBar1 Then
        valueChangeSource = Nothing
        Return
    End If
    NumericUpDown1.Value = TrackBar1.Value
End Sub

Private Sub NumericUpDown1_ValueChanged(sender As Object, e As EventArgs) Handles NumericUpDown1.ValueChanged
    If valueChangeSource Is Nothing Then
        valueChangeSource = NumericUpDown1
    ElseIf valueChangeSource Is NumericUpDown1 Then
        valueChangeSource = Nothing
        Return
    End If
    TrackBar1.Value = NumericUpDown1.Value
End Sub

`Nothing` is actually a value of Nullable, don't do this.

I could also probably use the `sender` parameter to deduplicate this code more or less neatly.

This is just the code to keep them in sync.

Dim original As Image = New System.Drawing.Bitmap(100, 100)
Private Sub OpenFileDialog1_FileOk(sender As OpenFileDialog, e As System.ComponentModel.CancelEventArgs) Handles OpenFileDialog1.FileOk
    original = Image.FromFile(sender.FileName)
End Sub

Yes it creates a dummy bitmap first, that maybe is not needed but is neat.

It now turns out that the Image.Save method, to accept EncoderParameters so that we can pick the quality instead of the default, requires us to not use ImageFormat but ImageCodecInfo. Help came from the documentation example of EncoderParameter class usage, specifically the GetEncoder function from there, although I wrote it out differently:

Function Codec() As Imaging.ImageCodecInfo
    'If ComboBox1.SelectedValue = "" Then
    '    ComboBox1.SelectedValue = "JPG"
    'End If
    'If Not ComboBox1.SelectedValue = "JPG" Then
    '    Throw New ArgumentException
    'End If
    Dim format As ImageFormat = ImageFormat.Jpeg
    Dim codecs = Imaging.ImageCodecInfo.GetImageEncoders
    Dim result = (From approached In codecs
                    Select approached
                    Where approached.FormatID = format.Guid
                        ).First
    Return result
End Function

This is supposed to handle the ComboBox selection. It's so far non-functional. Would be for picking other lossy formats someday. Such as `ImageFormat.Webp` or `ImageFormat.Heif`.

If you want, remove the ComboBox and resize&anchor the TrackBar to the top of the window.

The other parameter to our `Image.Save` will be EncoderParameters:

Function Params() As EncoderParameters
    Dim obj = New EncoderParameters(1)
    Dim quality As Long = NumericUpDown1.Value
    Dim param = New EncoderParameter(
        Encoder.Quality, quality)
    obj.Param(0) = param
    Return obj
End Function

Now what's left is to generate and preview the compressed image:

Dim compressed As Byte()

Sub Render()
    Dim stream = New MemoryStream
    original.Save(stream, Codec, Params)
    compressed = stream.ToArray
    Dim img = Image.FromStream(stream)
    PictureBox1.Image = img
    stream.Dispose()
End Sub

And add a call to `Render()` at the end of the both ValueChanged and FileOk handlers.

The `compressed` field is so far unused but this is our original compression output, as lossy compression could be non-deterministic and come out differently each time. We are to be saving it to file to achieve full functionality.

And for the last thing, we need our application to start with asking for a file:

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    OpenFileDialog1.ShowDialog()
End Sub

I really hope I hadn't omitted anything. Many things need yet to be done, including drag move around, scaling. PictureBox image ("client controllers") can be drag-moved by making handlers to set a variable on MouseUp and MouseDown, and a MouseMove handler to add `e.x` to PictureBox.Left and so on. Scaling could be handled with MouseWheel.

Saving the byte array to a file will be trivial, can be triggered by a click to the image, or by handling window close event (and making the closing procedure be to cancel the file save dialog, for example), or by a keyboard shortcut.

Many things need to be polished, like the default filenames in file chooser, filetype filters in the chooser... But we wanted a thing quick to hack and we got it. Many optimization can be made but are not needed. TabStop of TrackBar could be set to one or a different value, but not necessarily, as it reduces the wastefulness of resources. Asynchronicity could be introduced, especially considering large images, would probably be needed for responsiveness.

And one of the most needed features that I only now remembered from the Android app JPEG Optimizer (com.mixaimaging.jpeg.optimizerfree) would be to have resizing of images as part of the compressing. Having live preview of a selection of downscaling algorithms would be so so cool.

Oh, and yeah, the StatusStrip is to be for showing the file sizes and original resolution.

EOF