Pythonista crashes trying to save merged photo
-
I wrote a program (as an extension) to take selected photos from the camera roll, combine them vertically and save the combined photo to the camera roll. This would be used for combining multiple screen shots of say a web page where you capture parts of the page while scrolling through it. I know you can buy an app(s) that do this but this seems like a perfect thing for Pythonista (plus an opportunity to learn).
I have the code merging the photos (at least a few) but Pythonista crashes when more than 3 or so pics are selected and when trying to save to the camera roll. I added the startup code to log crash info but it is always empty. Looking for any help anyone may have.
import appex from PIL import Image from PIL.ExifTags import TAGS import photos # standard get exif code - needed for image size def get_exif(fn): ret = {} i = Image.open(fn) info = i._getexif() for tag, value in info.items(): decoded = TAGS.get(tag, tag) ret[decoded] = value return ret def main(): if not appex.is_running_extension(): print('This script is intended to be run from the sharing extension.') return images = appex.get_images() print(f'len(images) = {len(images)})') # get the widths/heights of the images widths, heights = zip(*(i.size for i in images)) # this is stacking vertically so the width is the max width of all the pics max_width = max(widths) # since stacking vertically the height is the total heights of the pics total_height = sum(heights) print(f'total height = {total_height}') print(f'max width = {max_width}') # create a new blank image in the required size new_im = Image.new('RGBA', (max_width, total_height)) print(f'new_im.size = {new_im.size}') # now assemble the merged pic # y_offset controls the pixel location of where each image is added # to the new image - to add each end-to-end # starting at the top which is y = 0 y_offset = 0 # loop through all passed images for im in images: # paste in the image at the left edge (x=0) and current offset for y new_im.paste(im, (0,y_offset)) print(f'y_offset = {y_offset}') # increase the offset by the height of current pic y_offset += im.size[1] # save the new image to the camera roll photos.save_image(new_im) if __name__ == '__main__': main()
-
I added the del image after it is pasted into the merged image and that part appears to work. The program is now having a problem saving the merged image to the camera roll. If I instead save it to the Pythonista file system it works and is a workaround until the camera roll save is figured out. Any other thoughts on how to fix the camera roll save is greatly appreciated.
-
Do you have a GitHub repo? It is more difficult to debug English prose that it is to debug Python code.
-
Unfortunately I do not.
But if you look at the code in my original post I added one line in the for loop to delete the image (below is the loop from that post with the del at the end)
for im in images: # paste in the image at the left edge (x=0) and current offset for y new_im.paste(im, (0,y_offset)) print(f'y_offset = {y_offset}') # increase the offset by the height of current pic y_offset += im.size[1] del im
As mentioned that seemed to fix the issue pasting more than a few images into the new one.
This line crashes Pythonista:
photos.save_image(new_im)
But I can save it to the Pythonista file system with the following:
new_im.save('combined.jpg')
-
This is long shot but:
del new_img photos.save(Image.open('combined.jpg'))
-
Thanks. I tried that but it crashes trying to save the image to the camera roll. I can open the jpg in Pythonista and manually add it to the camera roll. Here are the lines at the end that I added:
new_im.save('combined.jpg') del new_im photos.save_image(Image.open('combined.jpg'))
-
@jm2466 I'm in holiday, thus perhaps not able to correctly understand all (🍷🍷) but why did you not use
photos.create_image_asset(path)
instead of
photos.save_image(Image.open(path))
-
Um... because you never told me to until now??? :) kidding of course. But honestly have no real answer to that question. Possibly because I wanted to save an in-memory image and not have to save the file locally first (at least that was the initial plan but is not where I am at with it now). Not sure if create image asset supports that (I tried passing the variable and got an error that a str is required).
I just tried your suggestion and it works - I mean it saves the locally saved combined.jpg to the camera roll. Thanks!!!
Any thoughts on how to get this to work without saving combined.jpg first???
-
@jm2466 You could always convert your PIL Image into an ui.Image and use this kind of code to save it in camera roll
# only to have an ui.Image import ui img = ui.Image.named('test:Bridge') # Create a PHAsset from an ui.Image (not from a PIL Image) from objc_util import * import threading NSBundle.bundleWithPath_('/System/Library/Frameworks/Photos.framework').load() PHPhotoLibrary = ObjCClass('PHPhotoLibrary') PHAssetChangeRequest = ObjCClass('PHAssetChangeRequest') lib = PHPhotoLibrary.sharedPhotoLibrary() def change_block(): req = PHAssetChangeRequest.creationRequestForAssetFromImage_(img) def perform_changes(): lib.performChangesAndWait_error_(change_block, None) t = threading.Thread(target=perform_changes) t.start() t.join()
-
@jm2466 this
# 1) convert PIL Image to ui.Image console.hud_alert('convert PIL Image into ui.Image') with io.BytesIO() as bIO: new_im.save(bIO, 'PNG') ui_image = ui.Image.from_data(bIO.getvalue()) del bIO # 2) Create a PHAsset from an ui.Image (not from a PIL Image) console.hud_alert('Create a PHAsset from an ui.Image') lib = PHPhotoLibrary.sharedPhotoLibrary() def change_block(): req = PHAssetChangeRequest.creationRequestForAssetFromImage_(ui_image) def perform_changes(): lib.performChangesAndWait_error_(change_block, None) t = threading.Thread(target=perform_changes) t.start() t.join()
is about (😀) 1000 x slower than
path = 'temp.jpg' new_im.save(path , quality=95) photos.create_image_asset(path) os.remove(path)