locked
Shell.Application.Namespace(0x14).CopyHere(FontLocation) not creating registry entries in first time runs. RRS feed

  • Question

  • Hello,

    I have recieved a request last week on installing 251 fonts on 7 different computers and may see similar request on new computers. Me being me, is lazy. :) so i tried to ease my way of doing it.

    I have reated a custom powershell script that installs fonts from a remote shared location to the local computer. I have checked that the appropriate permissions required are set so that the computer can access the fonts shared path. I have attached my powershell script as a COMPUTER startup script.

    I have noticed that the command at line "$computerFontsFolder.CopyHere($copiedFont)" does not create a registry entry when importing the font file when the font was not in somehow imported manually. Cannot see the font using explorer in C:\Windows\Fonts but can see it when querried in CMD or Powershell. It also does not show in applications that uses fonts.

    However, if I Install the font manually using Windows Explorer (Right cliking the font, then clicking install), it will create the registry entry (in HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts) and will extract the font to the C:\Windows\Fonts of the computer executing the script. If I delete the font and registry entry, and reboot the computer, both are regenerated unexpectedly when the computer executes the powershell scripts, and can see the font in my applications that uses them.

    Any ideas why the "$computerFontsFolder.CopyHere($copiedFont)" does register the fonts appropriately the first time? I have also changed this line to "$computerFontsFolder.CopyHere($copiedFont, 16)" and "$computerFontsFolder.CopyHere($copiedFont,10)" but they seem to not make any changes.

    Code Below:

    # Author: Sheen Ismhael Lim
    # Email: sheenismhael.lim@outlook.ph
    # Created: 7/15/2017 2:18 PM
    # NOTES: This PowerShell script will import fonts from a specific folder to the computer executing the script.
    # Usage: Add as a login script in group policy.
    
    $eventSourceID = "Import-Fontsv2.ps1 PS Script"
    
    function CheckAndCreateEventLogSource() {
        # Get the Application Event file.
        $logTypes = Get-WmiObject WIN32_NTEventLogFile -Filter "filename='Application'"
    
        $isPSEventStoreCreated = $false
    
        # Scan the Application Event File and check if the store for this PS Script is already existing.
        foreach ($source in $logTypes.Sources) {
            if ($source -eq $eventSourceID) {
                $isPSEventStoreCreated = $true
                break
            }
        }
    
        if ($isPSEventStoreCreated -eq $false) {
            # Create a Source Event Log category
            New-EventLog –LogName Application –Source $eventSourceID
        }
    }
    
    function CopyToLocalComputer([System.IO.FileInfo] $remoteFontFile) {
        if ((Test-Path "c:\TempFonts") -eq $false) {
            New-Item -ItemType Directory -Path C:\ -Name "TempFonts"
        }
    
        else {
            foreach ($file in [array](Get-ChildItem -Path "C:\TempFonts" -Force)) {
                Remove-Item "c:\TempFonts\$($file)" -Force
            }
        }
    
        Copy-Item $remoteFontFile.FullName -Destination "C:\TempFonts"
         
        return "C:\TempFonts\$($remoteFontFile.Name)"
    }
    
    function IsThisFontExisting([System.String] $currentFontFile) {
        if ([System.IO.File]::Exists("c:\Windows\Fonts\" + $currentFontFile)) {
            return $true
        }
    
        return $false
    }
    
    function CreateRegistryEntryForFont([System.String] $copiedFont) {
        $fileInfo = New-Object System.IO.FileInfo $copiedFont
        
        $regLocation = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
        New-ItemProperty -Path $regLocation -Name $fileInfo.Name -Value $file.Name
    }
    
    function Install-Fonts() {
        $shellApplication = New-Object -ComObject Shell.Application
        $CSIDL = 0x14
        $computerFontsFolder = $shellApplication.NameSpace($CSIDL)
    
        $evenID = 8000
    
        CheckAndCreateEventLogSource
    
        [array]$sourceFonts = Get-ChildItem -Path "\\SRV1\SharedFonts" -Recurse | `
            Where-Object { $_.Extension -eq ".otf" -or $_.Extension -eq ".ttf" } 
    
        $isAnyFontImported = $false
        $eventMessage = "ImportFontsV2.ps1 Script - Import Log. `n`n"
    
        foreach ($fontFile in $sourceFonts) {
            if (IsThisFontExisting($fontFile.Name)) {
                $evenID = 8001
                
                $eventMessage += "Font file: " + $fontFile.Name + " is existing. Replaced Font with the same Font. `n" 
    
                continue
            }
    
            else {
                $isAnyFontImported = $true
                $evenID = 8000
                $copiedFont = CopyToLocalComputer($fontFile)
                
                $computerFontsFolder.CopyHere($copiedFont)
                #CreateRegistryEntryForFont($copiedFont)
    
                $eventMessage += "Font file: " + $fontFile.Name + " installed. `n"    
            }
        }
    
        if ($isAnyFontImported -eq $false) {
            $eventMessage += "No Fonts were imported."
        }
    
        $eventMessage += "`n`n This event was written by ImportFontsV2.ps1 Script."
        Write-EventLog -LogName Application -EventId $evenID -Source "Import-Fontsv2.ps1 PS Script" -EntryType Information `
            -Message $eventMessage
    }
    
    Install-Fonts


    For God, and Country.



    Sunday, July 16, 2017 10:30 AM

All replies

  • You can change this line and it might work.

    CHANGE

    return "C:\TempFonts\$($remoteFontFile.Name)"

    TO

    return "C:\TempFonts\$($remoteFontFile.FullName)"


    \_(ツ)_/

    Sunday, July 16, 2017 10:43 AM
  • Here is all you need to do this.  The registry will be updated with the installed font results automatically.  You do not need to create a custom source or event. 

    There is no need to overcomplicate this simple operation.

    $shell = New-Object -ComObject Shell.Application
    $fontFolder = $shell.NameSpace(0x14)
    
    $localfolder = New-Item c:\fonttemp -Force
    $fontsCopied = @()
    Get-ChildItem -Path \\SRV1\SharedFonts -Include *.ttf -Recurse |
    	ForEach-Object{
    		if(-not (Test-Path ($fontFolder.Self.Path+'\'+$_.Name))){ #'
    			$fontsCopied += Copy-Item $_ $localfolder -PassThru
    			$fontFolder.CopyHere($fontCopied.Fullname)
    		}
    	}
    
    if($fontsCopied){
    	Write-Host "$($fontsCopied.Count) fonts installed"
    }else{
    	Write-Host 'No fonts copied'
    }


    \_(ツ)_/






    • Edited by jrv Sunday, July 16, 2017 4:06 PM
    Sunday, July 16, 2017 3:58 PM
  • @jrv,


    The remoteFontFile.Name returns the filename.ext of the font. The reason why return "C:\TempFonts\$($remoteFontFile.Name)" is called is that "

    Copy-Item $remoteFontFile.FullName -Destination "C:\TempFonts"

    " is called first.

    If I call return "C:\TempFonts\$($remoteFontFile.FullName)" is will return something like

    C:\TempFonts\\\srv1\sharedfonts\teamviewer12.otf"


    For God, and Country.

    Sunday, July 16, 2017 9:55 PM
  • Hi jrv

    $fontFolder.CopyHere($fontCopied.Fullname)

    does not work on a UNC path, thats the problem of the CopyHere function, in addition to that it does not return any result, error or success. Starting in Windows 7, CopyHere no longer functions when feed with UNC path.

    I tried both UNC and local sources, on Win7, Win8, Win8.1 and Win10 (all updated). only local works. But it does not register the Fonts in the registry.

    Event logs are included because I have to have visibility of the fonts that was imported when it is time to diagnose the issue.

    Again, the entire scripts works as expected as I see fonts being extracted on the C:\Windows\Fonts but it just dont register the fonts, it only re-register the fonts when I install the font manually, delete the registry entry and font, reboot the computer.

    I need the script copyhere to register registry entry event if the fonts were never imported in the past.


    For God, and Country.

    Sunday, July 16, 2017 10:03 PM
  • That is why I posted a full solution.  You are trying to code like you think a program should be written but your assumptions about program design are incorrect and impossible to mage.  Forget about programming.  Write a simple script that does what you need.  There is no need for functions here even if they do look oh so official.

    Once you understand how to write simple scripts then you can consider why and how to use functions.


    \_(ツ)_/

    Sunday, July 16, 2017 10:09 PM
  • @jrv,

    Have you even tried running your proposed script on a test environment? Because I did, and its not working.

    And don't you worry about how I use functions, I am very comfortable with it and understand how they work and behave.

    Let me just emphasize that the only line I am having problem with is the CopyHere function, both your proposed answer and my script are behaving the same, they just copy the font file and not create the registry entry.

    I know, because I have a Virtual Machine that I test that is freshly installed template with came from sysprep. Both recreate and copy the font file, IF THE font was manually installed (using Windows Explorer, right clicking the font and cliking install) at some point during the lifetime of the computer.

    If I restore to a earlier snapshot, connect it to the domain, and again, it no longer re-register the appropriate registry entry for the font. it only copies it, both your script and mine are behaving the same thing.


    For God, and Country.

    Monday, July 17, 2017 5:12 PM
  • @jrv,

    Have you even tried running your proposed script on a test environment? Because I did, and its not working.

    And don't you worry about how I use functions, I am very comfortable with it and understand how they work and behave.

    Let me just emphasize that the only line I am having problem with is the CopyHere function, both your proposed answer and my script are behaving the same, they just copy the font file and not create the registry entry.

    I know, because I have a Virtual Machine that I test that is freshly installed template with came from sysprep. Both recreate and copy the font file, IF THE font was manually installed (using Windows Explorer, right clicking the font and cliking install) at some point during the lifetime of the computer.

    If I restore to a earlier snapshot, connect it to the domain, and again, it no longer re-register the appropriate registry entry for the font. it only copies it, both your script and mine are behaving the same thing.


    For God, and Country.

    The CopyHere function requires the full path to the file.  Your works second time because of something in your environment.

    Using the shell to put a font file into the fonts folder automatically registers the font.  There is no need to decorate the registry.  Fonts that are in older formats may have issues.

    The purpose of using the "shell" to copy the fonts is because the shell knows how to register a font. 

    Just saying something doesn't work is not helpful. You need to tell  us the exact error.

    You might also consider if Group Policy is blocking you font installs.

    It would take me some time to set up a system to match yours.  I cannot do that right now but the code I posted is the Microsoft example of how to install fonts converted to PowerShell.  It has to be adjusted to your environment.  You can run it in the debugger or use trace statements to determine where your mismatch is.


    \_(ツ)_/

    Monday, July 17, 2017 5:21 PM
  • @Jrv,

    Your help is greatly appreciated, but the problem with copyhere function is it does not return any error even if you deliberately pass an invalid path making it very hard to pinpoint what other references or variable it is having problem.

    Anyone got any ideas?


    For God, and Country.

    Saturday, July 22, 2017 12:40 AM
  • @Jrv,

    Your help is greatly appreciated, but the problem with copyhere function is it does not return any error even if you deliberately pass an invalid path making it very hard to pinpoint what other references or variable it is having problem.

    Anyone got any ideas?


    For God, and Country.

    Saturday, July 22, 2017 12:42 AM
  • It returns an error but that error is not visible in a script.  The shell is not a scriptable object as we have been saying over-and-over.  You cannot use it in a script. Period! Punto final!


    \_(ツ)_/

    Saturday, July 22, 2017 1:00 AM
  • Hi, not sure if this helps or not but i wrote the following and havent seemd to have had any problems with it so far - cheers!

    #<#
        #=======================================================================================================
        # ADD or REMOVE MULTIPLE FONT FILES [Using ComObjects] - WORKING - Only Tested TTF Files thus Far
        #=======================================================================================================
        # This code will install or uninstall a font using ComObject
        # You Must Modify the following variables in order to work
        # (A) $dirFiles                ==>  This is the source folder path that contains all of your font files
        # (B) $InstallOrUninstall      ==>  $true = Install Font ...  $false = UnInstall Font
        #=======================================================================================================
            # Define Working Variables
                $dirFiles = "C:\Temp\Fonts"
                $InstallOrUninstall = $true  # $true = Install = 1  ...or...  $false = UnInstall = 0
                $srcFontFiles = Get-ChildItem "$($dirFiles)\Fonts"
                $Fonts = (New-Object -ComObject Shell.Application).Namespace(0x14)
            # Copy each file into the Font Folder or Delete it - Depends on the $InstallOrUninstall variable setting
                ForEach($srcFontFile in $srcFontFiles) 
                {
                    $srcFontFileName = $srcFontFile.name
                    $srcFontFileFullPath = $srcFontFile.fullname
                    $targFonts = "C:\Windows\Fonts\$($srcFontFileName)"
                    If (Test-Path $targFonts -PathType any) { Remove-Item $targFonts -Recurse -Force } # UnInstall Font
                    If ((-not(Test-Path $targFonts -PathType container)) -and ($InstallOrUninstall -eq $true)) { $fonts.CopyHere($srcFontFileFullPath, 16) } # Install Font
                }
        #>



    • Edited by djma72 Tuesday, December 19, 2017 5:48 PM corrected code error
    Tuesday, December 19, 2017 5:31 PM
  • Hi there. I am working on this as well. What do you mean by:

    "The shell is not a scriptable object as we have been saying over-and-over.  You cannot use it in a script."

    Your example above is a script, is that correct? Do you mind if I show you my work (basically mirrors yours), and perhaps try to answer why everything works except registering the font? (Doesn't appear in registry or applications, but does appear in C:\Windows\Fonts)

    We do not have any GPO's blocking font installation. I have pretty much mirrored your example and the font does not register.

    Thank you!

    Monday, January 22, 2018 9:34 PM
  • Hi there. I am working on this as well. What do you mean by:

    "The shell is not a scriptable object as we have been saying over-and-over.  You cannot use it in a script."

    Your example above is a script, is that correct? Do you mind if I show you my work (basically mirrors yours), and perhaps try to answer why everything works except registering the font? (Doesn't appear in registry or applications, but does appear in C:\Windows\Fonts)

    We do not have any GPO's blocking font installation. I have pretty much mirrored your example and the font does not register.

    Thank you!

    According to Microsoft the "shell" object was not designed to support scripting and will fail frequently.  Most of the reason for this iis that ALL shell calls are asynchronous by design and will fail or be unreliable because the script will proceed or exit before the operation has completed.

    Can you get away with some things in a script?  Yes but there is no guarantee that a shell method call will complete correctly when no callback has been registered.

    For more detailed information on the shell object and how it works and is intended to be used review the full Microsoft documentation on the shell object.

    An example of a failure is to "CopyHere" a very large number of files and exit the script immediately after the copy returns.  Most of the files will not be copied.  If the target is a zip file then the zip file will be incomplete.  There are some work-around techniques to prevent for this.


    \_(ツ)_/

    Monday, January 22, 2018 9:46 PM
  • Thank you for the follow up!

    I got this to work by copying with the shell application and then registering the font later in the script, literally with reg add. Seems to work just fine.

    Thanks again

    Tuesday, January 23, 2018 2:48 PM
  • Note also that not all font types can be automatically registered with "CopyHere".  Only TTF type fonts will register.  Others have to be registered with the API or, as noted, direct registry update.


    \_(ツ)_/

    Tuesday, January 23, 2018 3:02 PM