I'm trying to create a blobstore entry from an image data-uri object, but am getting stuck.
Basically, I'm posting via ajax the data-uri as text, an example of the payload:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPA...
I'm trying to receive this payload with the following handler. I'm assuming I need to convert the data-uri back into an image before storing? So am using the PIL library.
My python handler is as follows:
import os
import urllib
import webapp2
from google.appengine.ext.webapp import template
from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
from google.appengine.api import images
class ImageItem(db.Model):
section = db.StringProperty(required=False)
description = db.StringProperty(required=False)
img_url = db.StringProperty()
blob_info = blobstore.BlobReferenceProperty()
when = db.DateTimeProperty(auto_now_add=True)
#Paste upload handler
class PasteUpload(webapp2.RequestHandler):
def post(self):
from PIL import Image
import io
import base64
data = self.request.body
#file_name = data['file_name']
img_data = data.split('data:image/png;base64,')[1]
#Convert base64 to jpeg bytes
f = Image.open(io.BytesIO(base64.b64decode(img_data)))
img = ImageItem(description=self.request.get('description'), section=self.request.get('section') )
img.blob_info = f.key()
img.img_url = images.get_serving_url( f.key() )
img.put()
This is likely all kinds of wrong. I get the following error when posting:
img.blob_info = f.key()
AttributeError: 'PngImageFile' object has no attribute 'key'
What am I doing wrong here? Is there an easier way to do this? I'm guessing I don't need to convert the data-uri into an image to store as a blob?
I also want this Handler to return the URL of the image created in the blobstore.
There are a couple of ways to view your question and the sample code you posted, and it's a little confusing what you need because you are mixing strategies and technologies.
POST base64 to _ah/upload/...
Your service uses create_upload_url() to make a one-time upload URL/session for your client. Your client makes a POST to that URL and the data never touches your service (no HTTP-request-size restrictions, no CPU-time spent handling the POST). An App Engine internal "blob service" receives that POST and saves the body as a Blob in the Blobstore. App Engine then hands control back to your service in the BlobstoreUploadHandler class you write and then you can determine how you want to respond to the successful POST. In the case of the example/tutorial, PhotoUploadHandler redirects the client to the photo that was just uploaded.
That POST from your client must be encoded as multipart/mixed and use the fields shown in the example HTML <form>.
The multipart form can take the optional parameter, Content-Transfer-Encoding, and the App Engine internal handler will properly decode base64 data. From blob_upload.py:
base64_encoding = (form_item.headers.get('Content-Transfer-Encoding') ==
'base64')
...
if base64_encoding:
blob_file = cStringIO.StringIO(base64.urlsafe_b64decode(blob_file.read()))
...
Here's a complete multipart form I tested with cURL, based on the fields used in the example. I found out how to do this over at Is there a way to pass the content of a file to curl?:
myconfig.txt:
header = "Content-length: 435"
header = "Content-type: multipart/mixed; boundary=XX
data-binary = "#myrequestbody.txt"
myrequestbody.txt:
--XX
Content-Disposition: form-data; name="file"; filename="test.gif"
Content-Type: image/gif
Content-Transfer-Encoding: base64
R0lGODdhDwAPAIEAAAAAzMzM/////wAAACwAAAAADwAPAAAIcQABCBxIsODAAAACAAgAIACAAAAiSgwAIACAAAACAAgAoGPHACBDigwAoKTJkyhTqlwpQACAlwIEAJhJc6YAAQByChAAoKfPn0CDCh1KtKhRAAEAKF0KIACApwACBAAQIACAqwECAAgQAIDXr2DDAggIADs=
--XX
Content-Disposition: form-data; name="submit"
Submit
--XX--
and then run like:
curl --config myconfig.txt "http://127.0.0.1:8080/_ah/upload/..."
You'll need to create/mock-up the multipart form in your client.
Also, as an alternative to Blobstore, you can use Cloud Storage if you want to save a little on storage costs or have some need to share the data without your API. Follow the documentation for Setting Up Google Cloud Storage, and then modify your service to create the upload URL for your bucket of choice:
create_upload_url(gs_bucket_name=...)
It's a little more complicated than just that, but reading the section Using the Blobstore API with Google Cloud Storage in the Blobstore document will get you pointed in the right direction.
POST base64 directly to your service/handler
Kind of like you coded in the original post, your service receives the POST from your client and you then decide if you need to manipulate the image and where you want to store it (Datastore, Blobstore, Cloud Storage).
If you need to manipulate the image, then using PIL is good:
from io import BytesIO
from PIL import Image
from StringIO import StringIO
data = self.request.body
#file_name = data['file_name']
img_data = data.split('data:image/png;base64,')[1]
# Decode base64 and open as Image
img = Image.open(BytesIO(base64.b64decode(img_data)))
# Create thumbnail
img.thumbnail((128, 128))
# Save img output as blob-able string
output = StringIO()
img.save(output, format=img.format)
img_blob = output.getvalue()
# now you choose how to save img_blob
If you don't need to manipulate the image, just stop at b64decode():
img_blob = base64.b64decode(img_data)
An image object (https://cloud.google.com/appengine/docs/standard/python/refdocs/google.appengine.api.images) isn't a Datastore entity, so it has no key. You need to actually save the image to blobstore[2] or Google Cloud Storage[1] then get a serving url for your image.
[1] https://cloud.google.com/appengine/docs/standard/python/googlecloudstorageclient/setting-up-cloud-storage
[2] https://cloud.google.com/appengine/docs/standard/python/blobstore/
Related
Goal: Take/attach pictures in a PhoneGap application and send a public URL for each picture to a Google Cloud SQL database.
Question 1: Is there a way to create a Google Cloud Storage object from a base64 encoded image (in Python), then upload that object to a bucket and return a public link?
I'm looking to use PhoneGap to send images to a Python Google App Engine application, then have that application send the images to a Google Cloud Storage bucket I have set up, then return a public link back to the PhoneGap app. These images can either be taken directly from the app, or attached from existing photo's on the user's device.
I use PhoneGap's FileTransfer plugin to upload the images to GAE, which are sent as base64 encoded images (this isn't something I can control).
Based on what I've found in Google Docs, I can upload the images to Blobstore; however, it requires <input type='file'> elements in a form. I don't have 'file' input elements; I just take the image URI returned from PhoneGap's camera object and display a thumbnail of the picture that was taken (or attached).
Question 2: Is it possible to have an <input type='file'> element and control it's value? As in, is it possible to set it's value based on whether the user chooses a file, or takes a picture?
Thanks in advance!
Here's a solution for others who might face this problem. Turns out it's incredibly simple!
Once you have a bucket setup for your GAE project, you can use this Python code to send an image to the bucket:
import cloudstorage as gcs
import webapp2
import cgi
import MySQLdb
import os
import logging
import time
from google.appengine.api import mail
from google.appengine.api import images
from google.appengine.ext import blobstore
class UploadImageHandler(webapp2.RequestHandler):
def post(self):
self.response.headers.add_header(ACCESS_CONTROL, '*')
f = self.request.POST['image']
fname = '/your-bucket-name/%s' % f.filename;
gcs_file = gcs.open(fname, 'w', content_type="image/jpeg")
gcs_file.write(self.request.get('image'))
gcs_file.close()
And the code used to upload the file from a PhoneGap application:
// Uploads images in "imageURIs" to the web service specified in "server".
function uploadImages(imageURIs, server) {
var success = function(data) {
alert("Successfully uploaded image!");
};
var fail = function(error) {
alert("Failed to upload image: "+error);
};
var options = new FileUploadOptions();
options.fileKey = "image";
options.mimeType = "image/jpeg";
var ft = new FileTransfer();
for (var i = 0; i < imageURIs.length; i++) {
alert("Uploading"+i);
options.fileName = imageURIs[i].substr(imageURIs[i].lastIndexOf('/') + 1);
ft.upload(imageURIs[i], encodeURI(server), success, fail, options);
}
}
I hope it helps someone else. :)
Yes, that is a fine use for GAE and GCS. You do not need an <input type=file>, per se. You can just set up POST parameters in your call to your GAE url. Make sure you send a hidden key as well, and work from SSL-secured urls, to prevent spammers from posting to your app.
This question is simlar to How to upload image from url to Blobstore?, but now the google.appengine.api.files is deprecated.
What I have is a web form where the user insert a url and the image should be uploaded to the blobstore. Then on server side I fetch the url to get the image data and I create a google.appengine.api.files.
Is it possibile to do it without google.appengine.api.files? I would like to keep it simple as in the case when the user submit an image and the server uses blobstore_handlers.BlobstoreUploadHandler
Maybe can I do something on the client side, to fetch the image on the client and then to use the same approach with blobstore_handlers.BlobstoreUploadHandler?
You will need to use urlfetch (or urllib2) to fetch the image(on server side, based on the url passed in).
Then you can use gcs client to upload the image to the blobstore.
Here's a snippet from the demo:
def create_file(self, filename):
"""Create a file.
The retry_params specified in the open call will override the default
retry params for this particular file handle.
Args:
filename: filename.
"""
self.response.write('Creating file %s\n' % filename)
write_retry_params = gcs.RetryParams(backoff_factor=1.1)
gcs_file = gcs.open(filename,
'w',
content_type='text/plain',
options={'x-goog-meta-foo': 'foo',
'x-goog-meta-bar': 'bar'},
retry_params=write_retry_params)
gcs_file.write('abcde\n')
gcs_file.write('f'*1024*1024 + '\n')
gcs_file.close()
self.tmp_filenames_to_clean_up.append(filename)
My code goes like this:
self.testbed.init_blobstore_stub()
upload_url = blobstore.create_upload_url('/image')
upload_url = re.sub('^http://testbed\.example\.com', '', upload_url)
response = self.testapp.post(upload_url, params={
'shopid': id,
'description': 'JLo',
}, upload_files=[('file', imgPath)])
self.assertEqual(response.status_int, 200)
how come it shows 404 error? For some reasons the upload path does not seem to exist at all.
You can't do this. I think the problem is that webtest (which I assume is where self.testapp came from) doesn't work well with testbed blobstore functionality. You can find some info at this question.
My solution was to override unittest.TestCase and add the following methods:
def create_blob(self, contents, mime_type):
"Since uploading blobs doesn't work in testing, create them this way."
fn = files.blobstore.create(mime_type = mime_type,
_blobinfo_uploaded_filename = "foo.blt")
with files.open(fn, 'a') as f:
f.write(contents)
files.finalize(fn)
return files.blobstore.get_blob_key(fn)
def get_blob(self, key):
return self.blobstore_stub.storage.OpenBlob(key).read()
You will also need the solution here.
For my tests where I would normally do a get or post to a blobstore handler, I instead call one of the two methods above. It is a bit hacky but it works.
Another solution I am considering is to use Selenium's HtmlUnit driver. This would require the dev server to be running but should allow full testing of blobstore and also javascript (as a side benefit).
I think Kekito is right, you cannot POST to the upload_url directly.
But if you want to test the BlobstoreUploadHandler, you can fake the POST request it would normally received from the blobstore in the following way. Assuming your handler is at /handler :
import email
...
def test_upload(self):
blob_key = 'abcd'
# The blobstore upload handler receives a multipart form request
# containing uploaded files. But instead of containing the actual
# content, the files contain an 'email' message that has some meta
# information about the file. They also contain a blob-key that is
# the key to get the blob from the blobstore
# see blobstore._get_upload_content
m = email.message.Message()
m.add_header('Content-Type', 'image/png')
m.add_header('Content-Length', '100')
m.add_header('X-AppEngine-Upload-Creation', '2014-03-02 23:04:05.123456')
# This needs to be valie base64 encoded
m.add_header('content-md5', 'd74682ee47c3fffd5dcd749f840fcdd4')
payload = m.as_string()
# The blob-key in the Content-type is important
params = [('file', webtest.forms.Upload('test.png', payload,
'image/png; blob-key='+blob_key))]
self.testapp.post('/handler', params, content_type='blob-key')
I figured that out by digging into the blobstore code. The important bit is that the POST request that the blobstore sends to the UploadHandler doesn't contain the file content. Instead, it contains an "email message" (well, informations encoded like in an email) with metadata about the file (content-type, content-length, upload time and md5). It also contains a blob-key that can be used to retrieve the file from the blobstore.
I'm trying to upload an image to the Face.com API. It either takes a url to an image, or images can be uploaded directly. Their website says:
A requests that uploads a photo must be formed as a MIME multi-part
message sent using POST data. Each argument, including the raw image
data, should be specified as a separate chunk of form data.
Problem is, I don't know exactly what that means. Right now my code looks like this:
import urllib
import json
apikey = "[redacted]"
secret = "[redacted]"
img = raw_input("Enter the URL of an image: ");
url = "http://api.face.com/faces/detect.json?api_key=" + apikey + "&api_secret=" + secret + "&urls=" + urllib.quote(img) + "&attributes=all"
data = json.loads(urllib.urlopen(url).read())
How can I convert this to work with a locally stored image?
The easiest way to upload photo in Python to face.com API is just using the Python Client Library that can be downloaded form http://developers.face.com/download/.
You got 2 there. Both support uploading by passing filename to the detected method (as a different param than the urls).
In my web application I send emails occasionally using a reusable mailer application like this:
user - self.user
subject = ("My subject")
from = "me#mydomain.com"
message = render_to_string("welcomeEmail/welcome.eml", {
"user" : user,
})
send_mail(subject, message, from, [email], priority="high" )
I want to send an email with embedded images in it, so I tried making the mail in a mail client, viewing the source, and putting it into my template (welcome.eml), but I've been unable to get it to render correctly in mail clients when its sent.
Does anyone know of an easy way for me to create mime encoded mail templates with inline images that will render correctly when I send them?
Update
Many thanks to Saqib Ali for resurrecting this old question nearly 5 years after my reply.
The instructions I gave at the time no longer work. I suspect there have been some improvements to Django in the intervening years which mean that send_mail() enforces plain text. No matter what you put in the content, it will always be delivered as plain text.
The most recent Django documentation explains that send_mail() is really just a convenience for creating an instance of the django.core.mail.EmailMessage class, and then calling send() on that instance. EmailMessage has this note for the body parameter, which explains the results we're seeing now in 2014:
body: The body text. This should be a plain text message.
... somewhat later in the docs ...
By default, the MIME type of the body parameter in an EmailMessage is "text/plain". It is good practice to leave this alone.
Fair enough (I confess I haven't taken the time to investigate why the 2009 instructions worked - I did test them back in 2009 - or when it changed). Django does provide, and document, a django.core.mail.EmailMultiAlternatives class to make it easier for sending a plain text and HTML representation of the same message.
The case in this question is slightly different. We're not seeking to append an alternative per se, but to append related parts to one of the alternatives. Within the HTML version (and it doesn't matter if you have or omit the plain text version), we want to embed an image data part. Not an alternative view of the content, but related content that is referenced in the HTML body.
Sending an embedded image is still possible, but I don't see a straightforward way to do it using send_mail. It's time to dispense with the convenience function and to instantiate an EmailMessage directly.
Here's an update to the previous example:
from django.core.mail import EmailMessage
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Load the image you want to send as bytes
img_data = open('logo.jpg', 'rb').read()
# Create a "related" message container that will hold the HTML
# message and the image. These are "related" (not "alternative")
# because they are different, unique parts of the HTML message,
# not alternative (html vs. plain text) views of the same content.
html_part = MIMEMultipart(_subtype='related')
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
body = MIMEText('<p>Hello <img src="cid:myimage" /></p>', _subtype='html')
html_part.attach(body)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
img.add_header("Content-Disposition", "inline", filename="myimage") # David Hess recommended this edit
html_part.attach(img)
# Configure and send an EmailMessage
# Note we are passing None for the body (the 2nd parameter). You could pass plain text
# to create an alternative part for this message
msg = EmailMessage('Subject Line', None, 'foo#bar.com', ['bar#foo.com'])
msg.attach(html_part) # Attach the raw MIMEBase descendant. This is a public method on EmailMessage
msg.send()
Original reply from 2009:
To send an e-mail with embedded images, use python's built-in email module to build up the MIME parts.
The following should do it:
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Load the image you want to send at bytes
img_data = open('logo.jpg', 'rb').read()
# Create a "related" message container that will hold the HTML
# message and the image
msg = MIMEMultipart(_subtype='related')
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
body = MIMEText('<p>Hello <img src="cid:myimage" /></p>', _subtype='html')
msg.attach(body)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
msg.attach(img)
send_mail(subject, msg.as_string(), from, [to], priority="high")
In reality, you'll probably want to send the HTML along with a plain-text alternative. In that case, use MIMEMultipart to create the "related" mimetype container as the root. Then create another MIMEMultipart with the subtype "alternative", and attach both a MIMEText (subtype html) and a MIMEText (subtype plain) to the alternative part. Then attach the image to the related root.
I was having trouble with Jarret's recipe on Django 1.10 - was getting MIME and encoding errors for the various ways you can attach MIME data.
Here's a simple multipart transactional template for an email with an embedded coupon_image file object that works on django 1.10:
from django.core.mail import EmailMultiAlternatives
from email.mime.image import MIMEImage
def send_mail(coupon_image):
params = {'foo':'bar'} # create a template context
text_body = render_to_string('coupon_email.txt', params)
html_body = render_to_string('coupon_email.html', params)
img_data = coupon_image.read() #should be a file object, or ImageField
img = MIMEImage(img_data)
img.add_header('Content-ID', '<coupon_image>')
img.add_header('Content-Disposition', 'inline', filename="coupon_image")
email = EmailMultiAlternatives(
subject="Here's your coupon!",
body=text_body,
from_email='noreply#example.com',
to=['someone#example.com',]
)
email.attach_alternative(html_body, "text/html")
email.mixed_subtype = 'related'
email.attach(img)
email.send(fail_silently=False)
Version using a text alternative
Jarret Hardie's answer says "You could pass plain text to create an alternative part for this message". However, I have found that doing so would lead gmail (though not outlook) to display both parts of the email, one after the other, as described here
As recommended there and elsewhere, I simply used EmailMultiAlternatives. The resulting code looks somewhat like this:
from django.core.mail import EmailMultiAlternatives
from email.mime.image import MIMEImage
# Load the image you want to send as bytes
img_data = open('logo.jpg', 'rb').read()
# Create the body with HTML. Note that the image, since it is inline, is
# referenced with the URL cid:myimage... you should take care to make
# "myimage" unique
html_content = '<p>Hello <img src="cid:myimage" /></p>'
text_content = 'Hello'
# Configure an EmailMultiAlternatives
msg = EmailMultiAlternatives('Subject Line', text_content, 'foo#bar.com', ['bar#foo.com'])
msg.attach_alternative(html_content)
# Now create the MIME container for the image
img = MIMEImage(img_data, 'jpeg')
img.add_header('Content-Id', '<myimage>') # angle brackets are important
img.add_header("Content-Disposition", "inline", filename="myimage") # David Hess recommended this edit
msg.attach(img)
# Finally, send the whole thing.
msg.send()