Skip to content

Commit 4256c71

Browse files
committed
New framework for automatic plugin updates
Relates to #534 . Thank you to @mizka76 for reporting this problem. PhotoDemon can work with AVIF files, but it doesn't ship the required 3rd-party plugin (libavif) because libavif is *huge* - bigger than PhotoDemon itself! Instead, PD offers to download libavif on-the-fly when a user first interacts with an AVIF file. This works well, but there is no additional mechanism to update libavif if the installed version falls out of date with the latest version PD supports. This commit fixes the problem with a new plugin update feature. PD now checks plugin versions when working with AVIF files, and if it sees that the installed version is less than the latest supported version, PD will now offer to download and configure a new copy of libavif. This of course requires an internet connection, and the user can say NO to keep working with their existing plugin copy. But if they say YES, PD will download the latest libavif, decompress and install it locally (to PD), and continue with the action that initially triggered the update warning (usually loading or saving an AVIF file). As part of this work, I also rewrote libavif's version-checking and download code to use a new generalized set of update functions. I should be able to migrate other on-demand image formats (like JPEG XL) to this same codebase, which would extend ongoing automatic updates to those formats as well. I also updated the automatically downloaded libavif version to the latest 1.0.4 while here (previously PD used 1.0.1). Still TODO is updating localizations for the new update feature - this will happen shortly!
1 parent e8b7662 commit 4256c71

File tree

8 files changed

+273
-167
lines changed

8 files changed

+273
-167
lines changed

Forms/Tools_PluginManager.frm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ End Sub
556556

557557
'Given a plugin's availability, expected version, and index on this form, populate the relevant labels associated with it.
558558
' This function will return TRUE if the plugin is in good status, FALSE if it isn't (for any reason)
559-
Private Function CheckLibraryStateUI(ByVal pluginID As CORE_PLUGINS, Optional ByRef dstStateString As String = vbNullString, Optional ByRef dstStateUIColor As Long = vbBlack) As Boolean
559+
Private Function CheckLibraryStateUI(ByVal pluginID As PD_PluginCore, Optional ByRef dstStateString As String = vbNullString, Optional ByRef dstStateUIColor As Long = vbBlack) As Boolean
560560

561561
'Is this plugin present on the machine?
562562
If PluginManager.IsPluginCurrentlyInstalled(pluginID) Then
@@ -679,7 +679,7 @@ Private Sub LibraryChanged()
679679
picContainer(0).Visible = False
680680
picContainer(1).Visible = True
681681

682-
Dim pluginIndex As CORE_PLUGINS, pluginName As String
682+
Dim pluginIndex As PD_PluginCore, pluginName As String
683683
pluginIndex = lstPlugins.ListIndex - 1
684684
pluginName = PluginManager.GetPluginName(pluginIndex)
685685

Modules/ImageExporter.bas

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,13 @@ Public Function ExportAVIF(ByRef srcPDImage As pdImage, ByVal dstFile As String,
542542
Dim sFileType As String: sFileType = "AVIF"
543543

544544
'If this system is 64-bit capable but libavif doesn't exist, ask if we can download a copy
545-
If OS.OSSupports64bitExe And (Not Plugin_AVIF.IsAVIFExportAvailable()) Then
545+
If OS.OSSupports64bitExe Then
546546

547-
If (Not Plugin_AVIF.PromptForLibraryDownload_AVIF()) Then GoTo ExportAVIFError
547+
If (Not Plugin_AVIF.IsAVIFExportAvailable()) Then
548+
If (Not Plugin_AVIF.PromptForLibraryDownload_AVIF()) Then GoTo ExportAVIFError
549+
Else
550+
Plugin_AVIF.CheckAVIFVersionAndOfferUpdates False
551+
End If
548552

549553
'Downloading the AVIF plugins will raise new messages in the status bar; restore the original
550554
' "saving %1 image" text

Modules/ImageLoader.bas

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,8 +1185,16 @@ Private Function LoadAVIF(ByRef srcFile As String, ByRef dstImage As pdImage, By
11851185
If potentialAVIF Then
11861186

11871187
'If this system is 64-bit capable but libavif doesn't exist, ask if we can download a copy
1188-
If OS.OSSupports64bitExe And (Not Plugin_AVIF.IsAVIFImportAvailable()) Then
1189-
If (Not Plugin_AVIF.PromptForLibraryDownload_AVIF()) Then Exit Function
1188+
If OS.OSSupports64bitExe Then
1189+
1190+
If (Not Plugin_AVIF.IsAVIFImportAvailable()) Then
1191+
If (Not Plugin_AVIF.PromptForLibraryDownload_AVIF()) Then Exit Function
1192+
1193+
'If the library *does* exist, check for updates
1194+
Else
1195+
Plugin_AVIF.CheckAVIFVersionAndOfferUpdates True
1196+
End If
1197+
11901198
End If
11911199

11921200
If Plugin_AVIF.IsAVIFImportAvailable() Then

Modules/Loading.bas

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -696,29 +696,10 @@ Public Function QuickLoadImageToDIB(ByVal imagePath As String, ByRef targetDIB A
696696
If cXCF.IsFileXCF(imagePath) Then loadSuccessful = cXCF.LoadXCF_FromFile(imagePath, tmpPDImage, targetDIB)
697697
If loadSuccessful Then tmpPDImage.GetCompositedImage targetDIB, True
698698

699-
'AVIF support was provisionally added in v9.0. Loading requires 64-bit Windows and manual
700-
' copying of the official libavif exe binaries (for example,
701-
' https://github.com/AOMediaCodec/libavif/releases/tag/v0.9.0)
702-
'...into the /App/PhotoDemon/Plugins subfolder.
699+
'AVIF support was provisionally added in v9.0.
703700
Case "HEIF", "HEIFS", "HEIC", "HEICS", "AVCI", "AVCS", "AVIF", "AVIFS"
704-
If Plugin_AVIF.IsAVIFImportAvailable() Then
705-
706-
'The separate AVIF apps convert AVIF to intermediary formats; we use PNG currently
707-
Dim tmpFile As String
708-
loadSuccessful = Plugin_AVIF.ConvertAVIFtoStandardImage(imagePath, tmpFile)
709-
710-
If loadSuccessful Then
711-
Set cPNG = New pdPNG
712-
loadSuccessful = (cPNG.LoadPNG_Simple(tmpFile, tmpPDImage, targetDIB) < png_Failure)
713-
End If
714-
715-
'Free the intermediary file before continuing
716-
Files.FileDeleteIfExists tmpFile
717-
If (Not targetDIB.GetAlphaPremultiplication) Then targetDIB.SetAlphaPremultiplication True
718-
If loadSuccessful Then tmpPDImage.GetCompositedImage targetDIB, True
719-
720-
End If
721-
701+
loadSuccessful = Plugin_AVIF.QuickLoadPotentialAVIFToDIB(imagePath, targetDIB, tmpPDImage)
702+
722703
'All other formats follow a set pattern: try to load them via FreeImage (if it's available), then GDI+, then finally
723704
' VB's internal LoadPicture function.
724705
Case Else

Modules/Plugin_AVIF.bas

Lines changed: 86 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ Attribute VB_Name = "Plugin_AVIF"
33
'libavif Interface
44
'Copyright 2021-2024 by Tanner Helland
55
'Created: 13/July/21
6-
'Last updated: 13/September/23
7-
'Last update: use the new pdPipeSync class for shell and output capture
6+
'Last updated: 21/February/24
7+
'Last update: new engine that can offer ongoing automatic updates of libavif to users
88
'
99
'Module for handling all libavif interfacing (via avifdec/enc.exe). This module is pointless without
1010
' those exes, which need to be placed in the App/PhotoDemon/Plugins subdirectory. (PD will automatically
@@ -15,7 +15,7 @@ Attribute VB_Name = "Plugin_AVIF"
1515
'
1616
' https://github.com/AOMediaCodec/libavif
1717
'
18-
'PhotoDemon has been designed against v0.10.0 (06 April '22). It may not work with other versions.
18+
'PhotoDemon has been designed against v1.0.4 (08 Feb '24). It may not work with other versions.
1919
' Additional documentation regarding the use of libavif is available as part of the official library,
2020
' downloadable from https://github.com/AOMediaCodec/libavif. You can also run the exe files manually
2121
' with the -h extension for details on how they work.
@@ -414,6 +414,72 @@ Public Function IsAVIFImportAvailable() As Boolean
414414
IsAVIFImportAvailable = m_avifImportAvailable
415415
End Function
416416

417+
Public Function QuickLoadPotentialAVIFToDIB(ByRef srcFile As String, ByRef dstDIB As pdDIB, Optional ByRef tmpPDImage As pdImage = Nothing) As Boolean
418+
419+
If Plugin_AVIF.IsAVIFImportAvailable() Then
420+
421+
'The separate AVIF apps convert AVIF to intermediary formats; we use PNG currently
422+
Dim tmpFile As String
423+
QuickLoadPotentialAVIFToDIB = Plugin_AVIF.ConvertAVIFtoStandardImage(srcFile, tmpFile)
424+
425+
If QuickLoadPotentialAVIFToDIB Then
426+
Dim cPNG As pdPNG
427+
Set cPNG = New pdPNG
428+
If tmpPDImage Is Nothing Then Set tmpPDImage = New pdImage
429+
QuickLoadPotentialAVIFToDIB = (cPNG.LoadPNG_Simple(tmpFile, tmpPDImage, dstDIB) < png_Failure)
430+
Set cPNG = Nothing
431+
End If
432+
433+
'Free the intermediary file before continuing
434+
Files.FileDeleteIfExists tmpFile
435+
If (Not dstDIB.GetAlphaPremultiplication) Then dstDIB.SetAlphaPremultiplication True
436+
437+
End If
438+
439+
End Function
440+
441+
'Returns TRUE if the installed version of libavif is >= the expected version of libavif.
442+
' By design, this function also returns TRUE if libavif is NOT installed - this is purposeful because
443+
' I don't want to raise "library out of date" warnings if the library doesn't even exist (there's a
444+
' separate code pathway for downloading the library for the first time).
445+
Public Function CheckAVIFVersionAndOfferUpdates(Optional ByVal targetIsImportLib As Boolean = True) As Boolean
446+
447+
'By design, this function returns TRUE if libavif doesn't exist.
448+
Dim libavifNotInstalled As Boolean
449+
If targetIsImportLib Then
450+
libavifNotInstalled = (Not IsAVIFImportAvailable)
451+
Else
452+
libavifNotInstalled = (Not IsAVIFExportAvailable)
453+
End If
454+
455+
If libavifNotInstalled Then
456+
CheckAVIFVersionAndOfferUpdates = True
457+
Exit Function
458+
End If
459+
460+
'Still here? libavif exists in this install. Let's pull its version and compare it to the expected version
461+
' (for this build of PhotoDemon).
462+
Dim curVersion As String
463+
curVersion = Plugin_AVIF.GetVersion(targetIsImportLib)
464+
465+
Dim expectedVersion As String
466+
expectedVersion = PluginManager.ExpectedPluginVersion(CCP_libavif)
467+
468+
If Updates.IsNewVersionHigher(curVersion, expectedVersion) Then
469+
470+
'The installed copy of libavif is out-of-date. Offer to download a new copy.
471+
CheckAVIFVersionAndOfferUpdates = False
472+
473+
Dim okToDownload As VbMsgBoxResult
474+
okToDownload = Updates.OfferPluginUpdate("libavif", curVersion, expectedVersion)
475+
If (okToDownload = vbYes) Then CheckAVIFVersionAndOfferUpdates = DownloadLatestLibAVIF()
476+
477+
Else
478+
CheckAVIFVersionAndOfferUpdates = True
479+
End If
480+
481+
End Function
482+
417483
'Notify the user that PD can automatically download and configure AVIF support for them.
418484
'
419485
'Returns TRUE if PD successfully downloaded (and initialized) all required plugins
@@ -458,116 +524,7 @@ Public Function PromptForLibraryDownload_AVIF(Optional ByVal targetIsImportLib A
458524
End If
459525

460526
'The user said YES! Attempt to download the latest libavif release now.
461-
Dim srcURL As String, dstFileTemp As String
462-
463-
'Before downloading anything, ensure we have write access on the plugin folder.
464-
dstFileTemp = PluginManager.GetPluginPath()
465-
If Not Files.PathExists(dstFileTemp, True) Then
466-
PDMsgBox g_Language.TranslateMessage("You have placed PhotoDemon in a restricted system folder. Because PhotoDemon does not have administrator access, it cannot download files for you. Please move PhotoDemon to an unrestricted folder and try again."), vbOKOnly Or vbApplicationModal Or vbCritical, g_Language.TranslateMessage("Error")
467-
PromptForLibraryDownload_AVIF = False
468-
Exit Function
469-
End If
470-
471-
'Previously, PhotoDemon downloaded each .exe as-is. Now we package them into a single pdPackage file
472-
' and extract them post-download. (This cuts download size by ~80%.)
473-
474-
'Grab the .pdz file. This path is hard-coded according to my most recently tested version of avifdec/enc.
475-
srcURL = "https://github.com/tannerhelland/PhotoDemon-Updates-v2/releases/download/libavif-plugins-1.0.1/libavif-1.0.1.pdz"
476-
dstFileTemp = PluginManager.GetPluginPath() & "libavif.tmp"
477-
478-
'If the destination file does exist, kill it (maybe it's broken or bad)
479-
Files.FileDeleteIfExists dstFileTemp
480-
481-
'Download
482-
Dim tmpFile As String
483-
tmpFile = Web.DownloadURLToTempFile(srcURL, False)
484-
485-
If Files.FileExists(tmpFile) Then Files.FileCopyW tmpFile, dstFileTemp
486-
Files.FileDeleteIfExists tmpFile
487-
488-
'With the pdPackage file successfully downloaded, extract avifdec and avifenc and place them in the plugins folder.
489-
PDDebug.LogAction "Extracting latest libavif..."
490-
Dim cPackage As pdPackageChunky
491-
Set cPackage = New pdPackageChunky
492-
493-
Dim dstFilename As String
494-
Dim tmpStream As pdStream, tmpChunkName As String, tmpChunkSize As Long
495-
496-
Dim numSuccessfulFiles As Long, numBytesExtracted As Long
497-
numSuccessfulFiles = 0
498-
numBytesExtracted = 0
499-
500-
'Load the file into a temporary package manager
501-
If cPackage.OpenPackage_File(dstFileTemp) Then
502-
503-
'I use a custom-built tool to assemble pdPackage files; individual files are stored as simple name-value pairs
504-
Do While cPackage.GetNextChunk(tmpChunkName, tmpChunkSize, tmpStream)
505-
506-
'Ensure the chunk name is actually a "NAME" chunk
507-
If (tmpChunkName = "NAME") Then
508-
509-
'Convert the filename to a full path into the user's plugin folder
510-
dstFilename = PluginManager.GetPluginPath() & tmpStream.ReadString_UTF8(tmpChunkSize)
511-
512-
'Next, extract the chunk's data
513-
If cPackage.GetNextChunk(tmpChunkName, tmpChunkSize, tmpStream) Then
514-
515-
'Ensure the chunk data is a "DATA" chunk
516-
If (tmpChunkName = "DATA") Then
517-
518-
'Write the chunk's contents to file
519-
If Files.FileCreateFromPtr(tmpStream.Peek_PointerOnly(0, tmpChunkSize), tmpChunkSize, dstFilename, True) Then
520-
numSuccessfulFiles = numSuccessfulFiles + 1
521-
numBytesExtracted = numBytesExtracted + tmpChunkSize
522-
Else
523-
InternalError FUNC_NAME, "failed to create target file " & dstFilename
524-
End If
525-
526-
'/Validate DATA chunk
527-
End If
528-
529-
'/Unexpected chunk
530-
Else
531-
InternalError FUNC_NAME, "bad data chunk: " & tmpChunkName
532-
End If
533-
534-
'/Unexpected chunk
535-
Else
536-
InternalError FUNC_NAME, "bad name chunk: " & tmpChunkName
537-
End If
538-
539-
'Iterate all remaining package items
540-
Loop
541-
542-
Else
543-
InternalError FUNC_NAME, "download failed! libavif is *not* currently available to this PhotoDemon instance."
544-
End If
545-
546-
'Free the underlying package object
547-
Set cPackage = Nothing
548-
549-
'Double-check expected number of files and total size of extracted bytes.
550-
' Currently we expect three files in the package:
551-
' - avifdec.exe (for decoding)
552-
' - avifenc.exe (for encoding)
553-
' - avif-LICENSE.txt (copyright and license info)
554-
If (numSuccessfulFiles <> 3) Then InternalError FUNC_NAME, "unexpected extraction file count: " & numSuccessfulFiles
555-
556-
'Current libavif build is 1.0.3768, downloaded from https://ci.appveyor.com/project/louquillio/libavif/builds/47660062/artifacts
557-
Const EXPECTED_TOTAL_EXTRACT_SIZE As Long = 24154944
558-
If (numBytesExtracted = EXPECTED_TOTAL_EXTRACT_SIZE) Then
559-
PDDebug.LogAction "Successfully extracted " & numSuccessfulFiles & " files totaling " & numBytesExtracted & " bytes."
560-
Else
561-
InternalError FUNC_NAME, "unexpected extraction size: " & numBytesExtracted & " vs " & EXPECTED_TOTAL_EXTRACT_SIZE
562-
End If
563-
564-
'Delete the temporary package file
565-
Files.FileDeleteIfExists dstFileTemp
566-
567-
'Attempt to initialize both the import and export plugins, and return whatever PD's central plugin manager
568-
' says is the state of these libraries (it may perform multiple initialization steps, including testing OS compatibility)
569-
PluginManager.LoadPluginGroup False
570-
PromptForLibraryDownload_AVIF = PluginManager.IsPluginCurrentlyEnabled(CCP_libavif)
527+
PromptForLibraryDownload_AVIF = DownloadLatestLibAVIF()
571528

572529
End If
573530

@@ -579,6 +536,22 @@ BadDownload:
579536

580537
End Function
581538

539+
'Attempt to download the latest libavif copy to this PC.
540+
Private Function DownloadLatestLibAVIF() As Boolean
541+
542+
' Currently we expect three files in the package:
543+
' - avifdec.exe (for decoding)
544+
' - avifenc.exe (for encoding)
545+
' - avif-LICENSE.txt (copyright and license info)
546+
Const EXPECTED_NUM_FILES As Long = 3
547+
548+
'Current libavif build is 1.0.1, downloaded from https://github.com/AOMediaCodec/libavif/releases/tag/v1.0.1
549+
Const EXPECTED_TOTAL_EXTRACT_SIZE As Long = 23881536
550+
Const UPDATE_URL As String = "https://github.com/tannerhelland/PhotoDemon-Updates-v2/releases/download/libavif-plugins-1.0.4/libavif-1.0.4.pdz"
551+
DownloadLatestLibAVIF = Updates.DownloadPluginUpdate(CCP_libavif, UPDATE_URL, EXPECTED_NUM_FILES, EXPECTED_TOTAL_EXTRACT_SIZE)
552+
553+
End Function
554+
582555
Private Sub InternalError(ByRef funcName As String, ByRef errDescription As String)
583556
PDDebug.LogAction "WARNING! libavif error reported in " & funcName & "(): " & errDescription
584557
End Sub

0 commit comments

Comments
 (0)