How to Upload a Photo To Django Using iOS and Django Rest Framework
I recently built an iOS application for fun that I use to take pictures of pages in a book, upload them to Django, and then ocr them for easy retrival. I was frustrated by the lack of documentation on how to upload an image to Django using the Django Rest Framework (DRF), so hopefully this tutorial will clear things up.
This tutorial assumes you have django<1.8
, djangorestframework==2.4.2
, and django-filter
installed.
The Backend
First, lets create the model that will store uploads:
class Upload(TimeStampedModel):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
path_to_image = models.CharField(null=True,max_length=100)
datafile = models.FileField(null=True)
url = models.URLField(null=True)
title = models.CharField(max_length=50)
text = models.TextField(null=True, max_length=500,
help_text="The OCRed text")
ocr_successful = models.BooleanField(default=False,
help_text="was the text successfully OCRed?")
As you can see, this model has multiple fields that are not really important for this tutorial. All you need to know here is that the owner
comes from request.user
(the client logged in uploading the image) and that the final image is stored as a url in the url
field.
Now lets create the serializer that is going to be used by DRF to handle the upload.
from rest_framework import serializers
class UploadSerializer(serializers.ModelSerializer):
class Meta:
model = Upload
read_only_fields = ('created', 'text','owner', 'url', 'title')
This is a very standard DRF serializer. You can read more about how they work here, but basically serializer can be used to validate data (which I am not doing in this tutorial) and also used to tell the DRF the format and composition of the JSON data returned from the API.
Next, lets create the DSR view to handle the image upload:
import datetime
class UploadList(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
parser_classes = (MultiPartParser, FormParser,)
def post(self, request, format=None):
""" handle upload, save it to local file system, start
ocr celery tasks """
upload = request.FILES['file']
fh = tempfile.NamedTemporaryFile(delete=False)
extension = upload.name.split(".")[1]
filename = "{}.{}".format(fh.name,extension)
with BufferedWriter( FileIO( filename, "w" ) ) as dest:
for c in upload.chunks():
dest.write(c)
now = datetime.datetime.now().strftime(UPLOAD_TITLE_NAME)
upload = Upload.objects.create(
owner=request.user,
title="Uploaded @ %s" %(now),
url="http://example.com/uploads/{name}".format(
name=filename.split("/")[2]),
path_to_image=filename)
upload.start_ocr_tasks()
return Response({}, status=201)
This view should be fairly self-explanatory. I’m using TokenAuthentication
rather than SessionAuthentication
for all of my APIs (I will convert to OAuth and some point). I’m extracting the photo data from the upload
in request.FILES
. After I find the data, I figured out the file extension being used , and save the file to my local hard drive by writing chucks of the image to a file handle using BufferedWriter
. Once the data is on my hard drive, I create a new Upload
in the database, and start an OCR task and return HTTP 201
because I just created a new database entry.
The important part of this piece of code are the parser_classes, which tell DRF that I’m uploading using multipart
rather than it expecting a regular HTTP POST
.
parser_classes = (MultiPartParser, FormParser,)
Finally, here is a unit test I used to confirm that the view I just outlined accepts the image data, downloads it locally to a temp file, and returns an HTTP 201
.
def test_upload_image(self):
""" unit tests for testing image uploads using DRF"""
path_to_image = os.path.join('path','to','image.jpg')
data = {
'file': open(path_to_image, 'rb')
}
response = self.client.post(reverse(self.URL), data ,format='multipart')
self.assertEquals(response.status_code,401)
self.client.force_authenticate(user=self.user)
data = {
'file': open(path_to_image, 'rb')
}
response = self.client.post(reverse(self.URL), data ,format='multipart')
self.assertEquals(response.status_code,201)
The Client
The actual HTTP POST
that uploads the image is utilizing the excellent AFNetworking library.
- (void)uploadImage:(NSData*)imageData
params:(NSDictionary*)params
success:(TFHTTPClientSuccess)success
failure:(TFHTTPClientFailure)failure;
- (void)uploadImage:(NSData*)imageData
params:(NSDictionary*)params
success:(TFHTTPClientSuccess)success
failure:(TFHTTPClientFailure)failure{
NSString* url = [NSString stringWithFormat:@"%@%@",
BASE_URL,@"/api/upload/image"];
[self POST:url parameters:params
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:imageData
name:@"file" fileName:@"photo.jpg"
mimeType:@"image/jpeg"];
} success:^(AFHTTPRequestOperation *operation,
id responseObject) {
if(success){
success((AFHTTPRequestOperation *)operation, responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (failure) {
failure((AFHTTPRequestOperation *)operation, error);
}
}];
The most important part of this code is the following
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:imageData
name:@"file" fileName:@"photo.jpg"
mimeType:@"image/jpeg"];
Which is taking the image data from my iPhone that is stored in an NSData
object, sticking it in the file
parameter in the body (request), and then performing a multipart request
The code that calls this function looks something like this:
- (void) camera:(id)cameraViewController didFinishWithImage:(UIImage *)image
withMetadata:(NSDictionary *)metadata
{
NSData *imageData = UIImageJPEGRepresentation(image, 0.5);
TFHTTPCLient* sharedClient = [TFHTTPCLient sharedClient];
NSDictionary *params = @{};
NSString* token = [[UserManager sharedClient] getAuthToken];
[sharedClient.requestSerializer
setValue:[NSString stringWithFormat:@"Token %@", token]
forHTTPHeaderField:@"Authorization"];
[MRProgressOverlayView showOverlayAddedTo:self.view animated:YES];
[sharedClient uploadImage:imageData params:params
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
[MRProgressOverlayView dismissOverlayForView:self.view animated:YES];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[MRProgressOverlayView dismissOverlayForView:self.view animated:YES];
}];
[self.presentedViewController dismissViewControllerAnimated:YES completion:nil];
}
I’m using DBCamera to take the photo and initiate the upload. cameraViewController
is a method that is called on by DBCamera
when the camera has successfully taken the photo. This method simply sets up the HTTP client, adds the current token I have stored in UserDefaults
(hacky, I know), and sticks it in an NSData
object. One important thing to notice here is that I downsample the image by 50% using UIImageJPEGRepresentation
before uploading it, to insure faster uploads (and man did they become much faster using this method).
That is about it. Everything here was taken directly from my project and is working as of 10/11/2015. If you have any questions, feel free to reach out to me on twitter and if you need iOS help, please reach me at joseph.misiti@mathandpencil.com