33

I have a script for testing an API which returns a base64 encoded image. My current solution is this.

$e = (curl.exe -H "Content-Type: multipart/form-data" -F "image=@clear.png" localhost:5000)
$decoded = [System.Convert]::FromBase64CharArray($e, 0, $e.Length)
[io.file]::WriteAllBytes('out.png', $decoded) # <--- line in question

Get-Content('out.png') | Format-Hex

This works, but I would like to be able to write the byte array natively in PowerShell, without having to source from [io.file].

My attempts of writing $decoded in PowerShell all resulted in writing a string-encoded list of the integer byte values. (e.g.)

42
125
230
12
34
...

How do you do this properly?

wonea
  • 4,783
  • 17
  • 86
  • 139
Kaelan Fouwels
  • 1,155
  • 1
  • 13
  • 28
  • 10
    You're already doing it properly. `[IO.File]::WriteAllBytes()` is the correct way of writing bytes to a file in PowerShell. – Ansgar Wiechers Aug 06 '15 at 12:46
  • Why exactly do you want not to write "natively" in Powershell ? PS is more or less built on top of .Net, so using .Net methods is perfectly fine and standard. – thomasb Aug 06 '15 at 12:53
  • No real reason, figured it there was a way to do it within powershell, it would be preferable than having to source from a .NET assembly. (note: written prior to the above edit) – Kaelan Fouwels Aug 06 '15 at 12:56
  • 2
    Not sure, so feel free to correct me, but I think your question may have been more properly phrased as something along the lines of `How do you do this with PowerShell Cmdlets?` -- my ~28¢ – Code Jockey Jun 01 '16 at 13:09
  • 2
    I think "idiomatically" is the adjective you're looking for, rather than "natively". – brianary Dec 12 '18 at 23:08
  • 2
    @thomasb I know this is old, but the reason some people want to write without IO.File in powershell is that using IO.File does not work in Powershell restricted mode, which is required in some high security environments. The accepted answer below will work in restricted mode, though. – Shirik Dec 13 '19 at 21:46

3 Answers3

53

The Set-Content cmdlet lets you write raw bytes to a file by using the Byte encoding:

$decoded = [System.Convert]::FromBase64CharArray($e, 0, $e.Length)
Set-Content out.png -Value $decoded -Encoding Byte

(However, BACON points out in a comment that Set-Content is slow: in a test, it took 10 seconds, and 360 MB of RAM, to write 10 MB of data. So, I recommend not using Set-Content if that's too slow for your application.)

Tanner Swett
  • 3,241
  • 1
  • 26
  • 32
  • 3
    An idiomatic (and high-level) solution. – brianary Jul 06 '18 at 23:04
  • 2
    Why is it so slow? It takes more than 2 minutes to write 14 megabytes. – Sasha Feb 03 '20 at 09:18
  • 1
    @Sasha, it's slow because 14 million bytes are being passed via the pipeline, one byte at a time. Good details about pipeline performance at https://stackoverflow.com/questions/34113755/need-to-make-a-powershell-script-faster/34114444#34114444 – jimhark Jul 01 '20 at 16:21
  • 3
    @jimhark With that in mind it seems this answer should include, if not recommend, writing it as `Set-Content out.png -Value $decoded -Encoding Byte`. A quick benchmark of the two invocations with a 10 MB array using `Measure-Command` and Task Manager gives 3 min/+5.5 GB RAM for `$byteArray | Set-Content ... -Encoding Byte` vs. 10 sec/+360 MB RAM for `Set-Content ... -Value $byteArray -Encoding Byte` in PS5.1; a big improvement, but still terrible. Using `-AsByteStream` on PS7 makes the pipeline 45 seconds faster, but _no difference_ with `-Value`. I'd say avoid `Set-Content`, if possible. – Lance U. Matthews Jul 01 '20 at 19:13
  • @BACON Thanks for the suggestion! I edited that into my answer; does what I have now look correct? – Tanner Swett Jul 01 '20 at 19:42
  • @TannerSwett That looks good, yes. I'm trying to think of scenarios where one might want to use a pipeline here, but considering that adding pipeline elements between the array and `Set-Content` could only make it _even slower_, maybe it is best to not even mention it. – Lance U. Matthews Jul 01 '20 at 20:00
  • @TannerSwett, Nice. That is a big improvement. Keep in mind that for small files (under 10K, maybe even under 100k) the performance hit will be much less noticeable. Idiomatic use of the pipeline might be more valuable then negligible performance improvement for smaller files. But if you know the file is going to be big (MBytes) or you notice a performance problem, then yeah, [io.file]::WriteAllBytes is a great alternative. – jimhark Jul 02 '20 at 15:39
  • @jimhark, thanks! But 14 million is not so much. I still don't see a reason why pipelining should be so slow if implemented properly. – Sasha Jul 02 '20 at 17:02
  • 4
    @TannerSwett, `-Encoding Byte` is [replaced](https://www.jonathanmedd.net/2017/12/powershell-core-does-not-have-encoding-byte-replaced-with-new-parameter-asbytestream.html) with `-AsByteStream` in PowerShell ≥[6](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-content?view=powershell-6). – Sasha Jul 02 '20 at 17:07
17

Running C# assemblies is native to PowerShell, therefore you are already writing bytes to a file "natively".

If you insist, you can use a construction like set-content test.jpg -value (([char[]]$decoded) -join ""), this has a drawback of adding #13#10 to the end of written data. With JPEGs it's bearable, but other files may get corrupt from this alteration. So please stick with byte-optimized routines of .NET instead of searching for "native" approaches - these are already native.

wonea
  • 4,783
  • 17
  • 86
  • 139
Vesper
  • 18,599
  • 6
  • 39
  • 61
15

Powershell Core (v6 and above) no longer have -Encoding byte as an option, so you'll need to use -AsByteStream, e.g:

Set-Content -Path C:\temp\test.jpg -AsByteStream
Mark Henderson
  • 2,586
  • 1
  • 33
  • 44
  • 1
    -Value parameter is missed. The correct command is: `Set-Content -Path 'out.png' -Value $decoded -AsByteStream;` – elshev Jan 04 '22 at 09:18