Home

Syncing Profile Pictures with Office 365 and Active Directory

%3CLINGO-SUB%20id%3D%22lingo-sub-644726%22%20slang%3D%22en-US%22%3ESyncing%20Profile%20Pictures%20with%20Office%20365%20and%20Active%20Directory%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-644726%22%20slang%3D%22en-US%22%3E%3CP%3EGood%20Afternoon%20All%2C%3C%2FP%3E%3CP%3EI%20wanted%20to%20share%20a%20script%20I%20came%20up%20with%20to%20help%20keep%20our%20active%20directory%20profile%20photos%20in%20sync%20with%20Office%20365%20and%20SharePoint%20Online%2C%20but%20first%20a%20little%20background%20on%20WHY%20I%20did%20this%20and%20how%20the%20script%20works%20along%20with%20a%20few%20caveats.%20I%20attached%20the%20script%20to%20this%20post%2C%20the%20code%20in%20the%20post%20is%20only%20for%20reference.%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3EGood%20Luck%2C%26nbsp%3B%3C%2FP%3E%3CP%3EAndrew%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3E%3CSTRONG%3EBackground%3A%3C%2FSTRONG%3E%3C%2FP%3E%3CP%3EFor%20some%20unknown%20reason%2C%20user%20profile%20photos%20were%20missing%20from%20Delve%20and%20SharePoint%20Online.%20They%20may%20have%20removed%20them%20on%20their%20own%2C%20but%20they%20still%20existed%20in%20our%20on-prem%20AD%20and%20we%20wanted%20to%20make%20sure%20they%20were%20in%20sync.%20We%20are%20all%20aware%20that%20the%20photo%20sync%20process%20is%20messy%20and%20rarely%20works%20well.%20One%20issue%20is%20that%20the%20photo%20sync%20only%20works%20on%20the%20initial%20sync%20of%20the%20photo%2C%20if%20we%20update%20an%20AD%20photo%20the%20changed%20photo%20never%20reflected%20in%20SharePoint%20(unsure%20if%20this%20changed%20yet).%20An%20admin%20could%20use%20the%20Set-UserPhoto%20cmd%20to%20update%20the%20photo%20but%20it%20would%20take%20anywhere%20from%2024-72%20hours%20to%20update%20in%20SharePoint%2C%20if%20it%20even%20did%20it%20at%20all.%20I%20create%20this%20script%20to%20synchronize%20the%20pictures%20from%20our%20Active%20Directory%20on%20prem%20to%20SPO%20and%20EXO%20(this%20covers%20most%20of%20the%20services%20we%20use%20atm%20that%20pull%20the%20profile%20picture).%3C%2FP%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3E%3CSTRONG%3EHow%20it%20works%3A%3C%2FSTRONG%3E%3C%2FP%3E%3CUL%3E%3CLI%3ERather%20than%20rely%20on%20365%20sync%20process%2C%20this%20script%20uses%20on%20prem%20AD%20as%20the%20source%20and%20manually%20updates%20the%20picture%20in%20both%20EXO%20and%20SPO.%3C%2FLI%3E%3CLI%3EThe%20script%20targets%20the%203%20main%20OU's%20we%20are%20concerned%20with%20and%20creates%20an%20array%20of%20users%20that%20have%20the%20thumbnailPhoto%20attribute%20set%20in%20AD.%3C%2FLI%3E%3CLI%3EIt%20then%20exports%20the%20photo%20to%20a%20local%20drive%3C%2FLI%3E%3CLI%3EUsing%20an%20Image-Resize%20script%2C%20it%20resizes%20and%20properly%20names%20the%203%20SPO%20thumbnails%3C%2FLI%3E%3CLI%3ERuns%20the%20Set-UserPhoto%20cmd%20to%20for%20each%20user%20to%20set%20the%20photo%20in%20EXO%20(Delve%2C%20etc.)%3C%2FLI%3E%3CLI%3EUploads%20the%203%20SPO%20thumbnails%20to%20the%20tenant%20User%20Profiles%20Library.%3C%2FLI%3E%3C%2FUL%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3E%3CSTRONG%3ECaveats%3A%3C%2FSTRONG%3E%3C%2FP%3E%3CUL%3E%3CLI%3EThe%20SharePoint%20user%20profile%20should%20have%20a%20link%20defined%20in%20the%20%22picture%22%20attribute%2C%20I%20believe%20this%20is%20created%20by%20default%20regardless%20of%20whether%20the%20user%20had%20a%20picture%20synced%20previously%20or%20not.%3C%2FLI%3E%3CLI%3EWe%20kept%20the%20%3CSTRONG%3EPicture%20Exchange%20Sync%20State%3C%2FSTRONG%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3Eattribute%20set%20to%201%20just%20because%2C%20not%20sure%20if%20this%20is%20necessary.%3C%2FLI%3E%3CLI%3EThe%20script%20is%20NOT%20efficient%20but%20it%20works%2C%20I%20am%20SURE%20there%20are%20ways%20to%20make%20it%20a%20little%20smaller%20and%20more%20friendly%20to%20pop%20in%20some%20variables%2C%20I%20may%20update%20it%20later%2C%20but%20I%20wanted%20something%20that%20worked.%20There%20are%20definitely%20redundancies%20in%20repeating%20parts%20the%20script%20for%20each%20OU.%3C%2FLI%3E%3CLI%3EI%20am%20going%20to%20post%20a%20sample%20for%20a%20single%20searchbase%20so%20it%20is%20easier%20to%20look%20at.%20This%20will%20be%20replaced%20with%20a%20variable%20in%20a%20future%20update%20so%20it%20is%20easier%20to%20manage.%3C%2FLI%3E%3CLI%3EScript%20should%20be%20run%20using%20a%20global%20admin%20(although%20if%20you%20want%20you%20CAN%20individually%20permission%20the%20rights%20in%20EXO%2C%20SPO%2C%20and%20local%20AD.)%3C%2FLI%3E%3CLI%3EIt%20is%20EXTREMELY%20slow.%20Unfortunately%20the%20Set-UserPhoto%20cmd%20and%20really%20slow%2C%20nothing%20we%20can%20do%20about%20it.%20The%20SPO%20photo%20upload%20can%20be%20slow%20at%20times%20as%20well.%20This%20is%20not%20something%20I%20would%20do%20daily%20depending%20on%20the%20number%20of%20user%20accounts%20you%20have%20to%20sync.%3C%2FLI%3E%3CLI%3EThe%20local%20path%20to%20manage%20the%20images%20is%20hard%20coded%20atm%20to%20%22C%3A%5CScripts%5CPics%22%2C%20create%20that%20directory%20or%20replace%20all%20instances%20of%20it%20in%20the%20script%20with%20your%20path%20(this%20will%20be%20fixed%20in%20an%20update%20to%20the%20script).%3C%2FLI%3E%3CLI%3EPortions%20of%20the%20script%20were%20borrowed%20from%20the%20following.%3CUL%3E%3CLI%3EResize-Image%3A%26nbsp%3B%3CA%20href%3D%22https%3A%2F%2Fgallery.technet.microsoft.com%2Fscriptcenter%2FResize-Image-A-PowerShell-3d26ef68%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noopener%20noreferrer%20noopener%20noreferrer%22%3Ehttps%3A%2F%2Fgallery.technet.microsoft.com%2Fscriptcenter%2FResize-Image-A-PowerShell-3d26ef68%3C%2FA%3E%3C%2FLI%3E%3CLI%3EUpload%20Files%20to%20SP%3A%26nbsp%3B%3CA%20href%3D%22https%3A%2F%2Fwww.c-sharpcorner.com%2Farticle%2Fsharepoint-online-automation-o365-sharepoint-online-how-to-upload-your-files-r%2F%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noopener%20noreferrer%20noopener%20noreferrer%22%3Ehttps%3A%2F%2Fwww.c-sharpcorner.com%2Farticle%2Fsharepoint-online-automation-o365-sharepoint-online-how-to-upload-your-files-r%2F%3C%2FA%3E%3C%2FLI%3E%3C%2FUL%3E%3C%2FLI%3E%3C%2FUL%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%3CP%3Efunction%20Resize-Image%3CBR%20%2F%3E%7B%3CBR%20%2F%3E%26lt%3B%23%3CBR%20%2F%3E.SYNOPSIS%3CBR%20%2F%3EResize-Image%20resizes%20an%20image%20file%3C%2FP%3E%3CP%3E.DESCRIPTION%3CBR%20%2F%3EThis%20function%20uses%20the%20native%20.NET%20API%20to%20resize%20an%20image%20file%2C%20and%20optionally%20save%20it%20to%20a%20file%20or%20display%20it%20on%20the%20screen.%20You%20can%20specify%20a%20scale%20or%20a%20new%20resolution%20for%20the%20new%20image.%3CBR%20%2F%3E%3CBR%20%2F%3EIt%20supports%20the%20following%20image%20formats%3A%20BMP%2C%20GIF%2C%20JPEG%2C%20PNG%2C%20TIFF%3CBR%20%2F%3E%3CBR%20%2F%3E.EXAMPLE%3CBR%20%2F%3EResize-Image%20-InputFile%20%22C%3A%5Ckitten.jpg%22%20-Display%3C%2FP%3E%3CP%3EResize%20the%20image%20by%2050%25%20and%20display%20it%20on%20the%20screen.%3C%2FP%3E%3CP%3E.EXAMPLE%3CBR%20%2F%3EResize-Image%20-InputFile%20%22C%3A%5Ckitten.jpg%22%20-Width%20200%20-Height%20400%20-Display%3C%2FP%3E%3CP%3EResize%20the%20image%20to%20a%20specific%20size%20and%20display%20it%20on%20the%20screen.%3C%2FP%3E%3CP%3E.EXAMPLE%3CBR%20%2F%3EResize-Image%20-InputFile%20%22C%3A%5Ckitten.jpg%22%20-Scale%2030%20-OutputFile%20%22C%3A%5Ckitten2.jpg%22%3C%2FP%3E%3CP%3EResize%20the%20image%20to%2030%25%20of%20its%20original%20size%20and%20save%20it%20to%20a%20new%20file.%3C%2FP%3E%3CP%3E.LINK%3CBR%20%2F%3EAuthor%3A%20Patrick%20Lambert%20-%20%3CA%20href%3D%22http%3A%2F%2Fdendory.net%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noopener%20noreferrer%20noopener%20noreferrer%22%3Ehttp%3A%2F%2Fdendory.net%3C%2FA%3E%3CBR%20%2F%3E%23%26gt%3B%3CBR%20%2F%3EParam(%5BParameter(Mandatory%3D%24true)%5D%5Bstring%5D%24InputFile%2C%20%5Bstring%5D%24OutputFile%2C%20%5Bint32%5D%24Width%2C%20%5Bint32%5D%24Height%2C%20%5Bint32%5D%24Scale%2C%20%5BSwitch%5D%24Display)%3C%2FP%3E%3CP%3E%23%20Add%20System.Drawing%20assembly%3CBR%20%2F%3EAdd-Type%20-AssemblyName%20System.Drawing%3C%2FP%3E%3CP%3E%23%20Open%20image%20file%3CBR%20%2F%3E%24img%20%3D%20%5BSystem.Drawing.Image%5D%3A%3AFromFile((Get-Item%20%24InputFile))%3C%2FP%3E%3CP%3E%23%20Define%20new%20resolution%3CBR%20%2F%3Eif(%24Width%20-gt%200)%20%7B%20%5Bint32%5D%24new_width%20%3D%20%24Width%20%7D%3CBR%20%2F%3Eelseif(%24Scale%20-gt%200)%20%7B%20%5Bint32%5D%24new_width%20%3D%20%24img.Width%20*%20(%24Scale%20%2F%20100)%20%7D%3CBR%20%2F%3Eelse%20%7B%20%5Bint32%5D%24new_width%20%3D%20%24img.Width%20%2F%202%20%7D%3CBR%20%2F%3Eif(%24Height%20-gt%200)%20%7B%20%5Bint32%5D%24new_height%20%3D%20%24Height%20%7D%3CBR%20%2F%3Eelseif(%24Scale%20-gt%200)%20%7B%20%5Bint32%5D%24new_height%20%3D%20%24img.Height%20*%20(%24Scale%20%2F%20100)%20%7D%3CBR%20%2F%3Eelse%20%7B%20%5Bint32%5D%24new_height%20%3D%20%24img.Height%20%2F%202%20%7D%3C%2FP%3E%3CP%3E%23%20Create%20empty%20canvas%20for%20the%20new%20image%3CBR%20%2F%3E%24img2%20%3D%20New-Object%20System.Drawing.Bitmap(%24new_width%2C%20%24new_height)%3C%2FP%3E%3CP%3E%23%20Draw%20new%20image%20on%20the%20empty%20canvas%3CBR%20%2F%3E%24graph%20%3D%20%5BSystem.Drawing.Graphics%5D%3A%3AFromImage(%24img2)%3CBR%20%2F%3E%24graph.DrawImage(%24img%2C%200%2C%200%2C%20%24new_width%2C%20%24new_height)%3C%2FP%3E%3CP%3E%23%20Create%20window%20to%20display%20the%20new%20image%3CBR%20%2F%3Eif(%24Display)%3CBR%20%2F%3E%7B%3CBR%20%2F%3EAdd-Type%20-AssemblyName%20System.Windows.Forms%3CBR%20%2F%3E%24win%20%3D%20New-Object%20Windows.Forms.Form%3CBR%20%2F%3E%24box%20%3D%20New-Object%20Windows.Forms.PictureBox%3CBR%20%2F%3E%24box.Width%20%3D%20%24new_width%3CBR%20%2F%3E%24box.Height%20%3D%20%24new_height%3CBR%20%2F%3E%24box.Image%20%3D%20%24img2%3CBR%20%2F%3E%24win.Controls.Add(%24box)%3CBR%20%2F%3E%24win.AutoSize%20%3D%20%24true%3CBR%20%2F%3E%24win.ShowDialog()%3CBR%20%2F%3E%7D%3C%2FP%3E%3CP%3E%23%20Save%20the%20image%3CBR%20%2F%3Eif(%24OutputFile%20-ne%20%22%22)%3CBR%20%2F%3E%7B%3CBR%20%2F%3E%24img2.Save(%24OutputFile)%3B%3CBR%20%2F%3E%7D%3CBR%20%2F%3E%7D%3C%2FP%3E%3CP%3E%23%20Load%20SharePoint%20CSOM%20Assemblies%3CBR%20%2F%3EAdd-Type%20-Path%20%22C%3A%5CProgram%20Files%5CCommon%20Files%5CMicrosoft%20Shared%5CWeb%20Server%20Extensions%5C16%5CISAPI%5CMicrosoft.SharePoint.Client.dll%22%3CBR%20%2F%3EAdd-Type%20-Path%20%22C%3A%5CProgram%20Files%5CCommon%20Files%5CMicrosoft%20Shared%5CWeb%20Server%20Extensions%5C16%5CISAPI%5CMicrosoft.SharePoint.Client.Runtime.dll%22%3CBR%20%2F%3EAdd-Type%20-Path%20%22C%3A%5CProgram%20Files%5CCommon%20Files%5Cmicrosoft%20shared%5CWeb%20Server%20Extensions%5C16%5CISAPI%5CMicrosoft.SharePoint.Client.UserProfiles.dll%22%3C%2FP%3E%3CP%3E%23%20Connect%20to%20AD%3CBR%20%2F%3EImport-Module%20ActiveDirectory%3C%2FP%3E%3CP%3E%23%20Connect%20to%20EXO%3CBR%20%2F%3E%24UserCredential%20%3D%20Get-Credential%3CBR%20%2F%3E%24Session%20%3D%20New-PSSession%20-ConfigurationName%20Microsoft.Exchange%20-ConnectionUri%20%3CA%20href%3D%22https%3A%2F%2Foutlook.office365.com%2Fpowershell-liveid%2F%3Fproxymethod%3Drps%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noopener%20noreferrer%20noopener%20noreferrer%22%3Ehttps%3A%2F%2Foutlook.office365.com%2Fpowershell-liveid%2F%3Fproxymethod%3Drps%3C%2FA%3E%20-Credential%20%24UserCredential%20-Authentication%20Basic%20-AllowRedirection%3CBR%20%2F%3EImport-PSSession%20%24Session%20-DisableNameChecking%3C%2FP%3E%3CP%3E%23%20Connect%20to%20SPO%3CBR%20%2F%3E%24Credentials%20%3D%20New-Object%20Microsoft.SharePoint.Client.SharePointOnlineCredentials(%24UserCredential.Username%2C%20%24UserCredential.Password)%3C%2FP%3E%3CP%3E%23%20Bind%20to%20SPO%20site%20collection%20and%20Library%3CBR%20%2F%3E%24SiteURL%20%3D%20%22%3CA%20href%3D%22https%3A%2F%2FYOURTENANT-my.sharepoint.com%2F%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noopener%20noreferrer%20noopener%20noreferrer%22%3Ehttps%3A%2F%2FYOURTENANT-my.sharepoint.com%2F%3C%2FA%3E%22%3CBR%20%2F%3E%24DocLibName%20%3D%20%22User%20Photos%22%3CBR%20%2F%3E%24SubFolderName%20%3D%20%22Profile%20Pictures%22%3CBR%20%2F%3E%24Context%20%3D%20New-Object%20Microsoft.SharePoint.Client.ClientContext(%24SiteURL)%3CBR%20%2F%3E%24Context.Credentials%20%3D%20%24Credentials%3CBR%20%2F%3E%24List%20%3D%20%24Context.Web.Lists.GetByTitle(%24DocLibName)%3CBR%20%2F%3E%24FolderToBindTo%20%3D%20%24List.RootFolder.Folders%3CBR%20%2F%3E%24Context.Load(%24List)%3CBR%20%2F%3E%24Context.Load(%24FolderToBindTo)%3CBR%20%2F%3E%24Context.ExecuteQuery()%3C%2FP%3E%3CP%3E%23%20Get%20all%20the%20employees%20with%20AD%20Pictures%3CBR%20%2F%3E%24user%20%3D%20Get-ADUser%20-filter%20*%20-Properties%20*%20-SearchBase%20'DC%3Dyourdomain%2CDC%3Dcom'%20%7C%20Select%20UserPrincipalName%2C%20thumbnailPhoto%20%7C%20Where-Object%20%7B%24_.thumbnailPhoto%20-ne%20%24NULL%7D%3C%2FP%3E%3CP%3E%23%20Export%20each%20users%20AD%20Pictures%3CBR%20%2F%3EWrite-Host%20%22%60nExporting%20User%20AD%20Pictures%22%3CBR%20%2F%3Eforeach%20(%24line%20in%20%24user)%20%7B%3CBR%20%2F%3E%24upn%20%3D%20%24line.UserPrincipalName%3CBR%20%2F%3EWrite-Host%20%22%60nProcessing%22%20%24line.UserPrincipalName%20-ForegroundColor%20Green%3CBR%20%2F%3E%24FileName%20%3D%20%22C%3A%5Cscripts%5CPics%5C%22%2B%24line.UserPrincipalName%2B%22.jpg%22%3CBR%20%2F%3EWrite-Host%20%22Exporting%20AD%20Picture%20to%22%20%24FileName%20-ForegroundColor%20Yellow%3CBR%20%2F%3E%5BSystem.Io.File%5D%3A%3AWriteAllBytes(%24Filename%2C%20%24line.thumbnailphoto)%3CBR%20%2F%3EWrite-Host%20%22Creating%20SharePoint%20Photo%20Folder%22%20-ForegroundColor%20Yellow%3CBR%20%2F%3ENew-Item%20-Path%20%22C%3A%5CScripts%5CPics%5C%22%20-Name%20%24line.UserPrincipalName%20-ItemType%20%22directory%22%20-Force%20%7C%20Out-Null%3CBR%20%2F%3E%24FormattedName%20%3D%20%24line.UserPrincipalName%20-replace%20%22%5C.%22%2C%22_%22%20-replace%20%22%40%22%2C%22_%22%3CBR%20%2F%3EWrite-Host%20%22Resizing%20Image%20for%20SharePoint%22%20-ForegroundColor%20Yellow%3CBR%20%2F%3E%24NewFilePath%20%3D%20%22C%3A%5CScripts%5CPics%5C%22%20%2B%20%24line.UserPrincipalName%20%2B%20%22%5C%22%20%2B%20%24FormattedName%20%2B%20%22_SThumb.jpg%22%3CBR%20%2F%3EResize-Image%20-InputFile%20%24FileName%20-Width%2048%20-Height%2048%20-Outputfile%20%24NewFilePath%3CBR%20%2F%3EWrite-Host%20%22Small%20Thumbnail%20Created%22%20-ForegroundColor%20Cyan%3CBR%20%2F%3E%24NewFilePath%20%3D%20%22C%3A%5CScripts%5CPics%5C%22%20%2B%20%24line.UserPrincipalName%20%2B%20%22%5C%22%20%2B%20%24FormattedName%20%2B%20%22_MThumb.jpg%22%3CBR%20%2F%3EResize-Image%20-InputFile%20%24FileName%20-Width%2072%20-Height%2072%20-Outputfile%20%24NewFilePath%3CBR%20%2F%3EWrite-Host%20%22Medium%20Thumbnail%20Created%22%20-ForegroundColor%20Cyan%3CBR%20%2F%3E%24NewFilePath%20%3D%20%22C%3A%5CScripts%5CPics%5C%22%20%2B%20%24line.UserPrincipalName%20%2B%20%22%5C%22%20%2B%20%24FormattedName%20%2B%20%22_LThumb.jpg%22%3CBR%20%2F%3EResize-Image%20-InputFile%20%24FileName%20-Width%2096%20-Height%2096%20-Outputfile%20%24NewFilePath%3CBR%20%2F%3EWrite-Host%20%22Large%20Thumbnail%20Created%22%20-ForegroundColor%20Cyan%3CBR%20%2F%3E%7D%3C%2FP%3E%3CP%3E%23%20Set%20each%20users%20365%20Photo%20using%20their%20exported%20AD%20photo%3CBR%20%2F%3EWrite-Host%20%22%60nSetting%20User%20365%20Photos%22%3CBR%20%2F%3Eforeach%20(%24line%20in%20%24user)%20%7B%3CBR%20%2F%3EWrite-Host%20%22%60nProcessing%22%20%24line.UserPrincipalName%20-ForegroundColor%20Green%3CBR%20%2F%3EWrite-Host%20%22Setting%20User%20Photo%20for%22%20%24line.UserPrincipalName%20-ForegroundColor%20yellow%3CBR%20%2F%3ESet-UserPhoto%20-Identity%20%24line.UserPrincipalName%20-PictureData%20(%5BSystem.IO.File%5D%3A%3AReadAllBytes(%22C%3A%5Cscripts%5CPics%5C%22%2B%24line.UserPrincipalName%2B%22.jpg%22))%20-Confirm%3A%24false%3CBR%20%2F%3Ewrite-host%20%22Operation%20complete%20for%22%20%24line.UserPrincipalName%20-ForegroundColor%20green%3C%2FP%3E%3CP%3E%23%20Upload%20file%20to%20SPO%3CBR%20%2F%3EWrite-Host%20%22Uploading%20pictures%20to%20SharePoint%22%20-ForegroundColor%20Yellow%3CBR%20%2F%3E%24Folder%20%3D%20%22C%3A%5CScripts%5CPics%5C%22%20%2B%20%24line.UserPrincipalName%3CBR%20%2F%3EWrite-Host%20%22Source%20Picture%20Path%20is%22%20%24Folder%20-ForegroundColor%20Yellow%3CBR%20%2F%3E%24FolderToUpload%20%3D%20%24FolderToBindTo%20%7C%20Where%20%7B%24_.Name%20-eq%20%24SubFolderName%7D%3C%2FP%3E%3CP%3EForeach(%24File%20in%20(dir%20%24Folder%20-File))%3CBR%20%2F%3E%7B%3CBR%20%2F%3E%24FileStream%20%3D%20New-Object%20IO.FileStream(%24File.FullName%2C%20%5BSystem.IO.FileMode%5D%3A%3AOpen)%3CBR%20%2F%3E%24FileCreationInfo%20%3D%20New-Object%20Microsoft.SharePoint.Client.FileCreationInformation%3CBR%20%2F%3E%24FileCreationInfo.Overwrite%20%3D%20%24true%3CBR%20%2F%3E%24FileCreationInfo.ContentStream%20%3D%20%24FileStream%3CBR%20%2F%3E%24FileCreationInfo.URL%20%3D%20%24File%3CBR%20%2F%3E%24Upload%20%3D%20%24FolderToUpload.Files.Add(%24FileCreationInfo)%3CBR%20%2F%3E%24Context.Load(%24Upload)%3CBR%20%2F%3E%24Context.ExecuteQuery()%3CBR%20%2F%3E%7D%3CBR%20%2F%3EWrite-Host%20%22SharePoint%20Picture%20Upload%20Complete%22%20-ForegroundColor%20Cyan%3CBR%20%2F%3EWrite-Host%20%24line.UserPrincipalName%20%22Photo%20Sync%20Complete%22%20-ForegroundColor%20Green%3CBR%20%2F%3E%7D%3C%2FP%3E%3CP%3E%23Close%20the%20EXO%20Session%3CBR%20%2F%3ERemove-PSSession%20%24Session%3C%2FP%3E%3C%2FLINGO-BODY%3E%3CLINGO-LABS%20id%3D%22lingo-labs-644726%22%20slang%3D%22en-US%22%3E%3CLINGO-LABEL%3EPowerShell%3C%2FLINGO-LABEL%3E%3CLINGO-LABEL%3ESharePoint%20Online%3C%2FLINGO-LABEL%3E%3C%2FLINGO-LABS%3E
Highlighted
andrewraia
New Contributor

Good Afternoon All,

I wanted to share a script I came up with to help keep our active directory profile photos in sync with Office 365 and SharePoint Online, but first a little background on WHY I did this and how the script works along with a few caveats. I attached the script to this post, the code in the post is only for reference.

 

Good Luck, 

Andrew

 

Background:

For some unknown reason, user profile photos were missing from Delve and SharePoint Online. They may have removed them on their own, but they still existed in our on-prem AD and we wanted to make sure they were in sync. We are all aware that the photo sync process is messy and rarely works well. One issue is that the photo sync only works on the initial sync of the photo, if we update an AD photo the changed photo never reflected in SharePoint (unsure if this changed yet). An admin could use the Set-UserPhoto cmd to update the photo but it would take anywhere from 24-72 hours to update in SharePoint, if it even did it at all. I create this script to synchronize the pictures from our Active Directory on prem to SPO and EXO (this covers most of the services we use atm that pull the profile picture).

 

How it works:

  • Rather than rely on 365 sync process, this script uses on prem AD as the source and manually updates the picture in both EXO and SPO.
  • The script targets the 3 main OU's we are concerned with and creates an array of users that have the thumbnailPhoto attribute set in AD.
  • It then exports the photo to a local drive
  • Using an Image-Resize script, it resizes and properly names the 3 SPO thumbnails
  • Runs the Set-UserPhoto cmd to for each user to set the photo in EXO (Delve, etc.)
  • Uploads the 3 SPO thumbnails to the tenant User Profiles Library.

 

Caveats:

  • The SharePoint user profile should have a link defined in the "picture" attribute, I believe this is created by default regardless of whether the user had a picture synced previously or not.
  • We kept the Picture Exchange Sync State attribute set to 1 just because, not sure if this is necessary.
  • The script is NOT efficient but it works, I am SURE there are ways to make it a little smaller and more friendly to pop in some variables, I may update it later, but I wanted something that worked. There are definitely redundancies in repeating parts the script for each OU.
  • I am going to post a sample for a single searchbase so it is easier to look at. This will be replaced with a variable in a future update so it is easier to manage.
  • Script should be run using a global admin (although if you want you CAN individually permission the rights in EXO, SPO, and local AD.)
  • It is EXTREMELY slow. Unfortunately the Set-UserPhoto cmd and really slow, nothing we can do about it. The SPO photo upload can be slow at times as well. This is not something I would do daily depending on the number of user accounts you have to sync.
  • The local path to manage the images is hard coded atm to "C:\Scripts\Pics", create that directory or replace all instances of it in the script with your path (this will be fixed in an update to the script).
  • Portions of the script were borrowed from the following.

 

function Resize-Image
{
<#
.SYNOPSIS
Resize-Image resizes an image file

.DESCRIPTION
This function uses the native .NET API to resize an image file, and optionally save it to a file or display it on the screen. You can specify a scale or a new resolution for the new image.

It supports the following image formats: BMP, GIF, JPEG, PNG, TIFF

.EXAMPLE
Resize-Image -InputFile "C:\kitten.jpg" -Display

Resize the image by 50% and display it on the screen.

.EXAMPLE
Resize-Image -InputFile "C:\kitten.jpg" -Width 200 -Height 400 -Display

Resize the image to a specific size and display it on the screen.

.EXAMPLE
Resize-Image -InputFile "C:\kitten.jpg" -Scale 30 -OutputFile "C:\kitten2.jpg"

Resize the image to 30% of its original size and save it to a new file.

.LINK
Author: Patrick Lambert - http://dendory.net
#>
Param([Parameter(Mandatory=$true)][string]$InputFile, [string]$OutputFile, [int32]$Width, [int32]$Height, [int32]$Scale, [Switch]$Display)

# Add System.Drawing assembly
Add-Type -AssemblyName System.Drawing

# Open image file
$img = [System.Drawing.Image]::FromFile((Get-Item $InputFile))

# Define new resolution
if($Width -gt 0) { [int32]$new_width = $Width }
elseif($Scale -gt 0) { [int32]$new_width = $img.Width * ($Scale / 100) }
else { [int32]$new_width = $img.Width / 2 }
if($Height -gt 0) { [int32]$new_height = $Height }
elseif($Scale -gt 0) { [int32]$new_height = $img.Height * ($Scale / 100) }
else { [int32]$new_height = $img.Height / 2 }

# Create empty canvas for the new image
$img2 = New-Object System.Drawing.Bitmap($new_width, $new_height)

# Draw new image on the empty canvas
$graph = [System.Drawing.Graphics]::FromImage($img2)
$graph.DrawImage($img, 0, 0, $new_width, $new_height)

# Create window to display the new image
if($Display)
{
Add-Type -AssemblyName System.Windows.Forms
$win = New-Object Windows.Forms.Form
$box = New-Object Windows.Forms.PictureBox
$box.Width = $new_width
$box.Height = $new_height
$box.Image = $img2
$win.Controls.Add($box)
$win.AutoSize = $true
$win.ShowDialog()
}

# Save the image
if($OutputFile -ne "")
{
$img2.Save($OutputFile);
}
}

# Load SharePoint CSOM Assemblies
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.UserProfiles.dll"

# Connect to AD
Import-Module ActiveDirectory

# Connect to EXO
$UserCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/?proxymethod=rps -Credential $UserCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session -DisableNameChecking

# Connect to SPO
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserCredential.Username, $UserCredential.Password)

# Bind to SPO site collection and Library
$SiteURL = "https://YOURTENANT-my.sharepoint.com/"
$DocLibName = "User Photos"
$SubFolderName = "Profile Pictures"
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteURL)
$Context.Credentials = $Credentials
$List = $Context.Web.Lists.GetByTitle($DocLibName)
$FolderToBindTo = $List.RootFolder.Folders
$Context.Load($List)
$Context.Load($FolderToBindTo)
$Context.ExecuteQuery()

# Get all the employees with AD Pictures
$user = Get-ADUser -filter * -Properties * -SearchBase 'DC=yourdomain,DC=com' | Select UserPrincipalName, thumbnailPhoto | Where-Object {$_.thumbnailPhoto -ne $NULL}

# Export each users AD Pictures
Write-Host "`nExporting User AD Pictures"
foreach ($line in $user) {
$upn = $line.UserPrincipalName
Write-Host "`nProcessing" $line.UserPrincipalName -ForegroundColor Green
$FileName = "C:\scripts\Pics\"+$line.UserPrincipalName+".jpg"
Write-Host "Exporting AD Picture to" $FileName -ForegroundColor Yellow
[System.Io.File]::WriteAllBytes($Filename, $line.thumbnailphoto)
Write-Host "Creating SharePoint Photo Folder" -ForegroundColor Yellow
New-Item -Path "C:\Scripts\Pics\" -Name $line.UserPrincipalName -ItemType "directory" -Force | Out-Null
$FormattedName = $line.UserPrincipalName -replace "\.","_" -replace "@","_"
Write-Host "Resizing Image for SharePoint" -ForegroundColor Yellow
$NewFilePath = "C:\Scripts\Pics\" + $line.UserPrincipalName + "\" + $FormattedName + "_SThumb.jpg"
Resize-Image -InputFile $FileName -Width 48 -Height 48 -Outputfile $NewFilePath
Write-Host "Small Thumbnail Created" -ForegroundColor Cyan
$NewFilePath = "C:\Scripts\Pics\" + $line.UserPrincipalName + "\" + $FormattedName + "_MThumb.jpg"
Resize-Image -InputFile $FileName -Width 72 -Height 72 -Outputfile $NewFilePath
Write-Host "Medium Thumbnail Created" -ForegroundColor Cyan
$NewFilePath = "C:\Scripts\Pics\" + $line.UserPrincipalName + "\" + $FormattedName + "_LThumb.jpg"
Resize-Image -InputFile $FileName -Width 96 -Height 96 -Outputfile $NewFilePath
Write-Host "Large Thumbnail Created" -ForegroundColor Cyan
}

# Set each users 365 Photo using their exported AD photo
Write-Host "`nSetting User 365 Photos"
foreach ($line in $user) {
Write-Host "`nProcessing" $line.UserPrincipalName -ForegroundColor Green
Write-Host "Setting User Photo for" $line.UserPrincipalName -ForegroundColor yellow
Set-UserPhoto -Identity $line.UserPrincipalName -PictureData ([System.IO.File]::ReadAllBytes("C:\scripts\Pics\"+$line.UserPrincipalName+".jpg")) -Confirm:$false
write-host "Operation complete for" $line.UserPrincipalName -ForegroundColor green

# Upload file to SPO
Write-Host "Uploading pictures to SharePoint" -ForegroundColor Yellow
$Folder = "C:\Scripts\Pics\" + $line.UserPrincipalName
Write-Host "Source Picture Path is" $Folder -ForegroundColor Yellow
$FolderToUpload = $FolderToBindTo | Where {$_.Name -eq $SubFolderName}

Foreach($File in (dir $Folder -File))
{
$FileStream = New-Object IO.FileStream($File.FullName, [System.IO.FileMode]::Open)
$FileCreationInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation
$FileCreationInfo.Overwrite = $true
$FileCreationInfo.ContentStream = $FileStream
$FileCreationInfo.URL = $File
$Upload = $FolderToUpload.Files.Add($FileCreationInfo)
$Context.Load($Upload)
$Context.ExecuteQuery()
}
Write-Host "SharePoint Picture Upload Complete" -ForegroundColor Cyan
Write-Host $line.UserPrincipalName "Photo Sync Complete" -ForegroundColor Green
}

#Close the EXO Session
Remove-PSSession $Session