Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Camera to DropBox
-
Is there a way to take a picture from my picture script:
import photos x=photos.capture_image() photos.save_image(x)
And save that photo directly to Dropbox?
-
Pythonista has the Dropbox API (
dropbox
module) built-in, but Dropbox interaction is much more complicated than writing to a regular file. In order to be able to write to your own Dropbox your script needs to authenticate with your account data and an access token that you can get from your account settings. This thread explains the process in more detail.Once you have a usable
DropboxClient
object you can use theput_file
method to upload a file. Note thatput_file
expects a "file-like" object, so you need to either first save the image to a temporary location in the script library, or store the raw image data in aStringIO
object to simulate a file object. -
I would like to know how to save a camera roll image to the Pythonista script directory. Right now I copy the image to a buffer and then, using put_file, I transfer it to dropbox. I would like to be able to save it to the script directory first, then use put_file to get it to the dropbox at a later time. I can't get the buffer technique to save it to the script directory. Any help on this would be much appreciated.
-
What kind of "buffer" are you using? If it supports normal file methods, you can easily change your code to write to a file instead:
with open("my_file_name.jpg", "wb") as myfile: myfile.write(a_string_of_bytes)
Reading from a file is similar:
with open("my_file_name.jpg", "rb") as myfile: a_string_of_bytes = myfile.read()
In both cases it is important that you access the file in binary mode (
"rb"
or"wb"
) - the default is text mode, which will not work with non-text files like images. Any code that uses themyfile
object (the variable can of course have any name you want) needs to be inside thewith
block as well. Once thewith
block ends, the file is closed and can no longer be read from or written to. -
@coomlata1, this allows you to select a photo from the camera roll and save it into a local file.
import photos assert photos.get_count(), 'Sorry no access or no pictures.' img, metadata = photos.pick_image(include_metadata=True, raw_data=True) filename = metadata.get('filename', 'my_photo.png') with open('my_photo.png', 'wb') as out_file: out_file.write(img) print('Your photo was written to the file {}.'.format(filename))
-
Thanks for your comments and code...much appreciated. Here is the code I was working with:
import photos from io import BytesIO import PIL from DropboxLogin import get_client from pexif import JpegFile choose=photos.pick_image(show_albums=True, multi=True, include_metadata=True) for photo in choose: resized=photo[0].resize((1600, 1200), Image.ANTIALIAS) buffer=BytesIO() resized.save(buffer,'JPEG') buffer.seek(0) # Upload to dropbox folder..."new_filename" contains the path and filename for photo response=drop_client.put_file(new_filename, buffer)
This code works to add the photo to dropbox but with no metadata. When I resize the photo I loose all the metadata. I want to be able to write the metadata back to the photo before uploading to dropbox. The pexif module I imported to Pythonista will do that but it wants a relative reference to the photo.
Trying your code, I created the file my_photo.jpg in the Pythonista scripts directory. I didn't try to resize it, but your code did keep all the media metadata intact. When I tried to access it with the pexif module to look at the metafile output I get a "TypeError:must be string without null bytes" as it dumps the metadata. I think it it is not matching the metadata included in the camera roll. Metadata seems to be a slippery slope.
ef=JpegFile.fromFile('my_photo.jpg') ef.dump
Pexif allows you to read and write metadata so I was hoping I could save the metadata with pexif and then write it back to the photo after resizing it.
-
The dump function should be dumping a complete list of all the JPEG segments. The EXIF data is an APP1 JPEG segment. Assuming the JPEG file is intact the error you are seeing may be due to a bug in Pexif. Maybe it is written in Python3 or something and does not work properly in 2.7. The traceback should show you where the problem is happening and allow you to determine the source of the problem.
-
The code above never calls
buffer.close()
so a buffer is left behind in RAM unused for every image processed. Usesys.getsizeof(buffer)
to see how quickly this might add up to a lot of RAM.I would recommend changing
buffer=BytesIO()
towith BytesIO() as buffer:
and then indenting the lines that follow so thatbuffer.close()
is called automatically for you.See If you don’t use “with”, when does Python close files? The answer is: It depends.
-
Thank you much for the explanation...very helpful and informative!!! That would explain why Pythonista was crashing after processing 10 to 12 photos in the loop. Looping through a significant amount of photos, and processing them in this way, puts a heavy load on the memory resources in an iPhone.
Is it possible, using Pythonista, to resize a camera roll photo and either save it to the Pythonista script dir and/or upload it to dropbox without losing the metadata from the original photo on the camera roll? At this point I can save the photo, untouched, to the script dir and copy or upload it to dropbox and the meta is untouched. Any attempt to resize anywhere in the chain results in wiping the meta.
-
It appears that PiL doesn't respect exif data.
I found a pure python exif writing tool, you might be able to modify it for your purposes... Namely you'd want to read the exif before PILling it, then write it after. -
@coomlat1 - the PIL code in Pythonista is vintage 2009. At that time exif support was "experimental". You will find that JPEG images have a _getexif function which parses the exif if it is there.
I checked the Pillow project to see if they have done any big changes to the JPEG code but don't see much: https://github.com/python-pillow/Pillow/blob/master/PIL/JpegImagePlugin.py
In fact they seem to be attempting to deal with crashes in _getexif only recently (Oct this year):
https://github.com/python-pillow/Pillow/issues/518There was also this Pillow sample:
from PIL import Image img_path = "/tmp/img.jpg" img = Image.open(img_path) exif = img.info['exif'] img.save("output_"+img_path, exif=exif)
Tested in Pillow 2.5.3 - not clear is it works in PIL as well but is worth a try
For what it's worth here is a PIL example function that should autorotate an image based on the exif info it finds. You notice that it handles "exceptions" thrown by _getexif which will probably happen - but WTF its worth a try.
def exif_orientation(im): """ Rotate and/or flip an image to respect the image's EXIF orientation data. """ try: exif = im._getexif() except Exception: # There are many ways that _getexif fails, we're just going to blanket # cover them all. exif = None if exif: orientation = exif.get(0x0112) if orientation == 2: im = im.transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 3: im = im.rotate(180) elif orientation == 4: im = im.transpose(Image.FLIP_TOP_BOTTOM) elif orientation == 5: im = im.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 6: im = im.rotate(-90) elif orientation == 7: im = im.rotate(90).transpose(Image.FLIP_LEFT_RIGHT) elif orientation == 8: im = im.rotate(90) return im
-
Update to my previous post: The Pillow sample:
from PIL import Image img_path = "input_img.jpg" img = Image.open(img_path) exif = img.info['exif'] img.save("output_img.jpg", exif=exif)
Does not work in Pythonista PIL. It does not throw exceptions, but the exif does not appear in the output JPEG. The Pillow guys seem to have added a raw exif buffer into the libraries.
So one possible solution would be for Pythonista to adopt Pillow and replace current PIL with it. PIL seems to be very stable and reliable but that is largely because it is abandoned by the developers. Pillow is still pretty active and supported. This is another Ole call since PIL/Pillow has a lot of C code in it. I wonder why the Pillow guys have not adopted pexiv2/gexiv2?
-
I finally got things working properly. I can now copy photos from the camera roll with it's metadata to the Pythonista script directory. Resize that photo and copy the metadata from the original photo back to the resized one using pexif, and then upload the resized photo with the metadata to Dropbox. Here is the script...Thanks everyone for input, info, and advice.
#coding: utf-8 import photos import time import Image import sys import console import PIL import string from DropboxLogin import get_client import pexif # Global arrays for photos that will require manual processing no_exif=[] no_resize=[] def GetDateTimeInfo(meta): old_filename=str(meta.get('filename')) exif=meta.get('{Exif}') try: if not exif=='None': theDatetime=str(exif.get('DateTimeOriginal')) theDatetime=theDatetime.split(" ") theDate=theDatetime[0] theDate=theDate.split(':') theTime=theDatetime[1] theTime=theTime.replace(':','.')+'.' folder_name=theDate[0]+'/'+theDate[1]+'.'+theDate[2]+'.'+theDate[0] new_filename=theTime+old_filename except: new_filename=old_filename folder_name='None' no_exif.append(old_filename) return old_filename,new_filename,folder_name def GetDimensions(meta,resize,img_name): # Original size exif=meta.get('{Exif}') img_width=exif.get('PixelXDimension') img_height=exif.get('PixelYDimension') if resize=='No Change': x=img_width y=img_height no_resize.append(img_name) return (x,y,img_width,img_height) else: resize=resize.split('x') x=int(resize[0]) y=int(resize[1]) # Don't resize photos smaller than your resize choice. if x*y>img_width*img_height: x=img_width y=img_height no_resize.append(img_name) return (x,y,img_width,img_height) # Don't resize if width or height isn't prportional to resize choice...scaling would be a better approach here. if x>img_width or y>img_height: x=img_width y=img_height no_resize.append(img_name) return (x,y,img_width,img_height) # Landscape if img_width>img_height: new_width=x new_height=y # Square elif img_width==img_height: # Don't resize if smaller than resize size if img_width<y: new_width=img_width new_height=img_height no_resize.append(img_name) else: new_width=y new_height=y # Portrait else: new_width=y new_height=x # Return resize dimensions...new & old return (new_width, new_height,img_width,img_height) def CopyMeta(meta_src,meta_dst): # Copy metadata from original photo to a resized photo that has no media metadata and write the results to a new photo that is resized with the media metadata. # Source photo img_src=pexif.JpegFile.fromFile(meta_src) # Destination photo img_dst=pexif.JpegFile.fromFile(meta_dst) img_dst.import_metadata(img_src) # Results photo img_dst.writeFile('meta_resized.jpg') #img=pexif.JpegFile.fromFile('meta_resized.jpg') #img.exif.primary.ExtendedEXIF.PixelXDimension= 1600 #img.exif.primary.ExtendedEXIF.PixelYDimension= 1200 #img.writeFile('meta_resized.jpg') img_src='' img_dst='' def main(): console.clear() try: # Here we are picking photos from the camera roll which, in Pythonista, allows us access to extra media data in photo's metafile. Because raw data is set to true, the image is a string representing the image object, not the object itself. choose=photos.pick_image(show_albums=True, multi=True,original=True,raw_data=True,include_metadata=True) except: print 'No photos choosen...exiting.' sys.exit() # Create an instance of Dropbox client drop_client=get_client() count=0 dest_dir='/Photos' # When metadata is returned with photo the photo is a tuple, with one the image, and the other the media metadata. for photo in choose: print '' print 'Processing photo...' # Raw data string img=photo[0] # Metadata meta=photo[1] # Get date and time info of photo old_filename,new_filename,folder_name=GetDateTimeInfo(meta) # Use info to rename photo new_filename=dest_dir+'/'+folder_name+'/'+new_filename # Get dimensions for resize based on orientation and size of original photo new_width,new_height,old_width,old_height=GetDimensions(meta,'1600x1200',old_filename) print '' print 'Original Name: '+old_filename print 'New Name: '+new_filename print '' print 'Original Size: '+str(old_width)+'x'+str(old_height) print 'New Size: '+str(new_width)+'x'+str(new_height) # Write string image of original photo to Pythonista script dir with open('meta_with.jpg', 'wb') as out_file: out_file.write(img) # Open image, resize it, and write new image to scripts dir img=Image.open('meta_with.jpg') resized=img.resize((new_width,new_height),Image.ANTIALIAS) resized.save('meta_without.jpg') resized='' # Copy metadata from original photo to resized one CopyMeta('meta_with.jpg','meta_without.jpg') print '' print 'Uploading photo to Dropbox...' # Upload resized photo with original metadata to Dropbox...use with statement to open file so file closes automatically at end of with. with open('meta_resized.jpg','r') as img: response=drop_client.put_file(new_filename,img) # Give Dropbox server time to process time.sleep(5) response='' print '' print 'Upload successful.' count=count+1 print '' print str(count) + ' photos processed.' if len(no_exif)>0: print '' print 'Photos with no DateTimeOriginal tag in their metadata and will need categorizing manually:' print '\n'.join(no_exif) if len(no_resize)>0: print '' print 'Photos that did not get resized because they were smaller than the resized version:' print '\n'.join(no_resize) sys.exit() if __name__ == '__main__': main()