I tried this:

var reader := ZIPReader.new()
var err := reader.open(path_to_zip)
for file_path in reader.get_files():
    if file_path.ends_with("/picture.jpg"):
        var buffer := reader.read_file(file_path)
        img = Image.new()
	img.load_jpg_from_buffer(buffer)
	img.save_jpg("/somewhere/else/picture.jpg")

But, the resulting file size is several times bigger than the original picture.jpg (I checked this by extracting the file with an archive manger and checking its size). I am guessing the intermediate step of converting the file into an Image object is what causes this. Is there a better way?

EDIT: Changed the save_png method to save_jpg as this is what I was actually using.

If the original image is a .jpg, why not use Image.save_jpg()? It has a quality parameter that affects the file size.
https://docs.godotengine.org/en/4.2/classes/class_image.html#class-image-method-save-jpg

Or use FileAccess.store_buffer() to write the file after extracting it from the .zip, instead of converting the file into an Image. That should provide the exact file that was in the .zip.
https://docs.godotengine.org/en/4.2/classes/class_fileaccess.html#class-fileaccess-method-store-buffer

    DaveTheCoder Aha!

    Writing the buffer directly did the trick. I haven't noticed that method before, autocomplete won't show it at first as it is not a static method. My bad.

    I have fixed my example, too: I was using Image.save_jpg() to keep the original file type, not sure how ''png'' got in there.