Matt McMurry

Pasting (simple)HTML into the Mail app (iOS)

with 4 comments

If you are not familiar with the UIPasteBoard class and how to copy and paste you should first get caught up on the basics. This post will explain a few steps that may be too advanced for some people but if you take your time, you should be able to get through it.

If you need to get started first with Copy and Paste, check out these guides: Apples Copy, Cut, Paste Guide , UIPasteboard Class Reference, and Understanding UTIs

Honestly I don’t think they are the best guides and I don’t think that there are any good ones out there.

Pasting HTML into the Mail app is a little tricky to figure out. You may have tried to just paste regular html into Mail as an html or text type of data and saw that all you get is the HTML code. The problem is, Mail expects an archive paste type as opposed to regular text or html type. Further the archive must be correctly put together.

This guide is only about copying strings of html code. You can copy images that are part of the html block you want to copy with the html too, but I haven’t spent anytime on that. I’ll give some tips at the end of where to start on copying the images contained in the html block.

Lets get to it.

Prepare your HTML

Get the HTML you want to copy but start on the highest most level of you HTML that is inside of the <body> tag. The HTML you are going to copy should not contain a Doctype, html, head, or body tag. (I am fairly certain it will get stripped if you try to put it on there). You are also going to have to inline ALL of you css.
The reason for this is that the Mail app Compose Message is already an HTML block, so you are pasting directly into the body. I know it can be frustrating to inline a bunch of css, but right now I know of no other way. Mobile safari took my css block and inlined all of it automatically, meaning it didn’t copy the the <style> block.

NSString *htmlString = [[NSString alloc] initWithString:@"<h1>This is a Header</h1>"];

Encode your html

Now we need to base64 encode all the html you want to copy. Objective-C doesn’t have a read base64 encode (if I wrong please tell me). Here is a category implementation of the NSData Method we will use to encode the data:
I store this category implementation in a class called Extensions.h/m where I do all my category implementations:

Extensions.h

@interface NSData (MBBase64)
 + (id)dataWithBase64EncodedString:(NSString *)string;
 - (NSString *)base64Encoding;
@end

Extensions.m

static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@implementation NSData (MBBase64)
 + (id)dataWithBase64EncodedString:(NSString *)string;
 {
  if (string == nil)
   [NSException raise:NSInvalidArgumentException format:@""];
  if ([string length] == 0)
   return [NSData data];
  static char *decodingTable = NULL;
  if (decodingTable == NULL)
  {
   decodingTable = malloc(256);
   if (decodingTable == NULL)
    return nil;
   memset(decodingTable, CHAR_MAX, 256);
   NSUInteger i;
   for (i = 0; i < 64; i++)
    decodingTable[(short)encodingTable[i]] = i;
  }
  const char *characters = [string cStringUsingEncoding:NSASCIIStringEncoding];
  if (characters == NULL)     //  Not an ASCII string!
   return nil;
  char *bytes = malloc((([string length] + 3) / 4) * 3);
  if (bytes == NULL)
   return nil;
  NSUInteger length = 0;
  NSUInteger i = 0;
  while (YES)
  {
   char buffer[4];
   short bufferLength;
   for (bufferLength = 0; bufferLength < 4; i++)
   {
    if (characters[i] == '')
     break;
    if (isspace(characters[i]) || characters[i] == '=')
     continue;
    buffer[bufferLength] = decodingTable[(short)characters[i]];
    if (buffer[bufferLength++] == CHAR_MAX)      //  Illegal character!
    {
     free(bytes);
     return nil;
    }
   }
   if (bufferLength == 0)
    break;
   if (bufferLength == 1)      //  At least two characters are needed to produce one byte!
   {
    free(bytes);
    return nil;
   }
  //  Decode the characters in the buffer to bytes.
  bytes[length++] = (buffer[0] << 2) | (buffer[1] >> 4);
  if (bufferLength > 2)
   bytes[length++] = (buffer[1] << 4) | (buffer[2] >> 2);
  if (bufferLength > 3)
   bytes[length++] = (buffer[2] << 6) | buffer[3];
 }
 realloc(bytes, length);
 return [NSData dataWithBytesNoCopy:bytes length:length];
}
- (NSString *)base64Encoding;
{
 if ([self length] == 0)
  return @"";
 char *characters = malloc((([self length] + 2) / 3) * 4);
 if (characters == NULL)
  return nil;
 NSUInteger length = 0;
 NSUInteger i = 0;
 while (i < [self length])
 {
  char buffer[3] = {0,0,0};
  short bufferLength = 0;
  while (bufferLength < 3 && i < [self length])
   buffer[bufferLength++] = ((char *)[self bytes])[i++];
  //  Encode the bytes in the buffer to four characters, including padding "=" characters if necessary.
  characters[length++] = encodingTable[(buffer[0] & 0xFC) >> 2];
  characters[length++] = encodingTable[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xF0) >> 4)];
  if (bufferLength > 1)
   characters[length++] = encodingTable[((buffer[1] & 0x0F) << 2) | ((buffer[2] & 0xC0) >> 6)];
  else characters[length++] = '=';
  if (bufferLength > 2)
   characters[length++] = encodingTable[buffer[2] & 0x3F];
  else characters[length++] = '=';
 }
 return [[[NSString alloc] initWithBytesNoCopy:characters length:length encoding:NSASCIIStringEncoding freeWhenDone:YES] autorelease];
}
@end

base64 method source: http://www.cocoadev.com/index.pl?BaseSixtyFour

I must promptly apologize for not formatting that code better, however I imagine that you are less concerned with how the base64 encoding process works than you are being able to paste HTML.

For the sake of avoiding the answer to a much expected question, make sure you import this class Extensions.h (or whatever the name of your class is) into the class you are working in.
Now we convert the NSString to NSData:

NSData *htmlData = [htmlString dataUsingEncoding:NSUTF8StringEncoding];
[htmlString release];

Now we need to encode it:

NSString *htmlEncoded = [htmlData base64Encoding];

Add the Wrapper

The next step is to put the encoded data into the xml wrapper that will make this html into an archive.
You’ll need to create a template file. Mine is: webArchiveTemplate.txt
It contains this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
 <dict>
  <key>WebMainResource</key>
  <dict>
   <key>WebResourceData</key>
   <data>
    REPLACE_WITH_ENCODED_DATA
   </data>
   <key>WebResourceFrameName</key>
   <string></string>
   <key>WebResourceMIMEType</key>
   <string>text/html</string>
   <key>WebResourceTextEncodingName</key>
   <string>UTF-8</string>
   <key>WebResourceURL</key>
   <string>about:blank</string>
  </dict>
 </dict>
</plist>

Put the file in a string and replace the “REPLACE_WITH_ENCODED_DATA” with the encodedHTML:


NSMutableString *webArchiveTemplate = [[NSMutableString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"webArchiveTemplate" ofType:@"txt"]];
[webArchiveTemplate replaceOccurrencesOfString:@"REPLACE_WITH_ENCODED_DATA"
withString:encoded
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [webArchiveTemplate length])];

Finally. Add it To the paste board


NSString *htmlType = @"Apple Web Archive pasteboard type";
NSDictionary *item = [NSDictionary dictionaryWithObjectsAndKeys:webArchiveTemplate,htmlType,nil];
[webArchiveTemplate release];
UIPasteBoard  *pasteBoard = [UIPasteBoard generalPasteBoard];
pasteBoard.items = [NSArray arrayWithObject:item];

The way I am adding above is the way to support multiple representations of the data set so I stuck with it for this example to make it easy to expand on.

Go Paste your HTML into a Mail Message!

I will try to have a demo app put together soon.

If you implement this code in an as is state, you will not have to touch any “copy” button, this code just throws the example straight into the pasteboard.

How to Copy Images that are part of a webpage.

While I haven’t done this. I know how you can get an example archive that contains images.

Create a sample html file that represents what you will be copy and pasting. Be sure to include all the images that will be on the webpage.

Open the html file in Safari on your desktop. Save the webpage as an archive.

Open the archive file in something like TextWrangler. If you open it in Text Edit, it will attempt to render the archive as a webpage.

You should now be able to see how the archive xml links the encoded image data into the html page.

Good luck!


Advertisements

Written by mcmurrym

August 13, 2010 at 4:41 am

Posted in Uncategorized

Tagged with , , , , ,

4 Responses

Subscribe to comments with RSS.

  1. If you want to figure out how webarchives deal with images, it looks like you can open .webarchive files with the Property List Editor that comes with OSX. It makes it easier to read.

    Also, since the web archives are plists, you can probably build the web archive from dictionaries with the keys that you see in the XML. I am about to try this out. I’ll post back if I find anything interesting.

    colin

    October 26, 2010 at 11:14 pm

    • I’ve had some success creating the web archive in objective-c without the base64 encoding step. It looks something like this:

      NSMutableDictionary *archiveDictionary = [NSMutableDictionary dictionary];

      NSMutableDictionary *mainResourceDictionary = [NSMutableDictionary dictionary];
      [mainResourceDictionary setObject:[htmlString dataUsingEncoding:NSUTF8StringEncoding]
      forKey:kWebArchiveResourceData];
      [mainResourceDictionary setObject:@””
      forKey:kWebArchiveResourceFrameName];
      [mainResourceDictionary setObject:@”text/html”
      forKey:kWebArchiveResourceMIMEType];
      [mainResourceDictionary setObject:@”UTF-8″
      forKey:kWebArchiveResourceTextEncodingName];
      [mainResourceDictionary setObject:@”file:///html.html”
      forKey:kWebArchiveResourceURL];

      [archiveDictionary setObject:mainResourceDictionary forKey:kWebArchiveMainResource];

      NSMutableArray *subResourcesArray = [NSMutableArray array];
      NSMutableDictionary *subResourceDictionary = [NSMutableDictionary dictionary];
      [subResourceDictionary setObject:[cssString dataUsingEncoding:NSUTF8StringEncoding]
      forKey:kWebArchiveResourceData];
      [subResourceDictionary setObject:@”text/css”
      forKey:kWebArchiveResourceMIMEType];
      [subResourceDictionary setObject:@”file:///css.css”
      forKey:kWebArchiveResourceURL];

      [subResourcesArray addObject:subResourceDictionary];

      [archiveDictionary setObject:subResourcesArray forKey:kWebArchiveSubresources];

      Set htmlString to your HTML and cssString to your styling.
      For instance (I haven’t tested this particular html/css pair…):
      htmlString = @”Hello there.”;
      cssString = @”.p{color:red;}”

      Add the resulting archiveDictionary to the UIPasteboard with the pasteboard key that you used above and you can paste this into Mail.app.

      colin

      October 27, 2010 at 11:00 pm

      • Woops – The HTML string needs to contain a reference to the CSS, something like this:
        htmlString = @” Hi there.”;

        colin

        October 27, 2010 at 11:02 pm

      • Hmm.. wordpress is filtering out the html tags that I keep trying to post.

        The html string needs a link tag, referencing the CSS (using parentheses instead of caret brackets):
        (link rel=”stylesheet” type=”text/css” href=”css.css”)

        and then its content:
        (p)Hello there.(/p)

        colin

        October 27, 2010 at 11:19 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: