From 15b2f38ba64aab55180a0a1b0f04982d8800320d Mon Sep 17 00:00:00 2001
From: Sam Freney
Date: Thu, 19 Dec 2013 13:22:22 +1100
Subject: [PATCH] Added ePub processing to convert files to Baker's Hpub
format.
---
BakerView/BakerBook.h | 18 +++-
BakerView/BakerBook.m | 212 ++++++++++++++++++++++++++++++++++++++----
2 files changed, 213 insertions(+), 17 deletions(-)
diff --git a/BakerView/BakerBook.h b/BakerView/BakerBook.h
index 0404a827..928992f6 100644
--- a/BakerView/BakerBook.h
+++ b/BakerView/BakerBook.h
@@ -31,7 +31,19 @@
#import
-@interface BakerBook : NSObject
+@interface BakerBook : NSObject {
+ NSMutableString *element;
+ NSString *opfFile;
+ NSString *opfDirectory;
+ NSMutableDictionary *manifest;
+ NSMutableArray *spine;
+ NSString *ePubTitle;
+ NSString *ePubAuthor;
+ NSString *ePubCreator;
+ NSString *ePubDate;
+ NSString *ePubID;
+ NSString *ePubStartPage;
+}
#pragma mark - HPub Parameters Properties
@@ -98,6 +110,10 @@
- (BOOL)validateNumber:(NSNumber *)number forParam:(NSString *)param;
- (BOOL)matchParam:(NSString *)param againstParamsArray:(NSArray *)paramsArray;
+#pragma mark - ePub processing
+
+- (BOOL)convertEpubBookToHpub:(NSString *)bookJSONPath;
+
#pragma mark - Book status management
- (BOOL)updateBookPath:(NSString *)bookPath bundled:(BOOL)bundled;
diff --git a/BakerView/BakerBook.m b/BakerView/BakerBook.m
index 1ac00de5..3259bad0 100644
--- a/BakerView/BakerBook.m
+++ b/BakerView/BakerBook.m
@@ -102,13 +102,14 @@ - (id)initWithBookPath:(NSString *)bookPath bundled:(BOOL)bundled
- (id)initWithBookJSONPath:(NSString *)bookJSONPath
{
if (![[NSFileManager defaultManager] fileExistsAtPath:bookJSONPath]) {
- return nil;
+ if (![self convertEpubBookToHpub:bookJSONPath])
+ return nil;
}
NSError* error = nil;
NSData* bookJSON = [NSData dataWithContentsOfFile:bookJSONPath options:0 error:&error];
if (error) {
- NSLog(@"[BakerBook] ERROR reading 'book.json': %@", error.localizedDescription);
+ // NSLog(@"[BakerBook] ERROR reading 'book.json': %@", error.localizedDescription);
return nil;
}
@@ -116,7 +117,7 @@ - (id)initWithBookJSONPath:(NSString *)bookJSONPath
options:0
error:&error];
if (error) {
- NSLog(@"[BakerBook] ERROR parsing 'book.json': %@", error.localizedDescription);
+ // NSLog(@"[BakerBook] ERROR parsing 'book.json': %@", error.localizedDescription);
return nil;
}
@@ -129,7 +130,7 @@ - (id)initWithBookData:(NSDictionary *)bookData
NSString *baseID = [self.title stringByAppendingFormat:@" %@", [self.url stringSHAEncoded]];
self.ID = [self sanitizeForPath:baseID];
- NSLog(@"[BakerBook] 'book.json' parsed successfully. Book '%@' created with id '%@'.", self.title, self.ID);
+ // NSLog(@"[BakerBook] 'book.json' parsed successfully. Book '%@' created with id '%@'.", self.title, self.ID);
return self;
}
@@ -259,13 +260,13 @@ - (BOOL)validateBookJSON:(NSDictionary *)bookData withRequirements:(NSArray *)re
{
for (NSString *param in requirements) {
if ([bookData objectForKey:param] == nil) {
- NSLog(@"[BakerBook] ERROR: param '%@' is missing. Add it to 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' is missing. Add it to 'book.json'.", param);
return NO;
}
}
for (NSString *param in bookData) {
- //NSLog(@"[BakerBook] Validating 'book.json' param: '%@'.", param);
+ // NSLog(@"[BakerBook] Validating 'book.json' param: '%@'.", param);
id obj = [bookData objectForKey:param];
if ([obj isKindOfClass:[NSArray class]] && ![self validateArray:(NSArray *)obj forParam:param]) {
@@ -287,26 +288,26 @@ - (BOOL)validateArray:(NSArray *)array forParam:(NSString *)param
if (![self matchParam:param againstParamsArray:shouldBeArray]) {
- NSLog(@"[BakerBook] ERROR: param '%@' should not be an Array. Check it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' should not be an Array. Check it in 'book.json'.", param);
return NO;
}
if (([param isEqualToString:@"author"] || [param isEqualToString:@"contents"]) && [array count] == 0) {
- NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param);
return NO;
}
for (id obj in array) {
if ([param isEqualToString:@"author"] && (![obj isKindOfClass:[NSString class]] || [(NSString *)obj isEqualToString:@""])) {
- NSLog(@"[BakerBook] ERROR: param 'author' is empty. Fill it in 'book.json'.");
+ // NSLog(@"[BakerBook] ERROR: param 'author' is empty. Fill it in 'book.json'.");
return NO;
} else if ([param isEqualToString:@"contents"]) {
if ([obj isKindOfClass:[NSDictionary class]] && ![self validateBookJSON:(NSDictionary *)obj withRequirements:[NSArray arrayWithObjects:@"url", nil]]) {
- NSLog(@"[BakerBook] ERROR: param 'contents' is not validating. Check it in 'book.json'.");
+ // NSLog(@"[BakerBook] ERROR: param 'contents' is not validating. Check it in 'book.json'.");
return NO;
}
} else if (![obj isKindOfClass:[NSString class]]) {
- NSLog(@"[BakerBook] ERROR: param '%@' type is wrong. Check it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' type is wrong. Check it in 'book.json'.", param);
return NO;
}
}
@@ -332,12 +333,12 @@ - (BOOL)validateString:(NSString *)string forParam:(NSString *)param
if (![self matchParam:param againstParamsArray:shouldBeString]) {
- NSLog(@"[BakerBook] ERROR: param '%@' should not be a String. Check it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' should not be a String. Check it in 'book.json'.", param);
return NO;
}
if (([param isEqualToString:@"title"] || [param isEqualToString:@"author"] || [param isEqualToString:@"url"]) && [string isEqualToString:@""]) {
- NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param);
return NO;
}
@@ -346,8 +347,8 @@ - (BOOL)validateString:(NSString *)string forParam:(NSString *)param
}
if ([param isEqualToString:@"-baker-rendering"] && (![string isEqualToString:@"screenshots"] && ![string isEqualToString:@"three-cards"])) {
- NSLog(@"Error: param \"-baker-rendering\" should be equal to \"screenshots\" or \"three-cards\" but it's not");
- NSLog(@"[BakerBook] ERROR: param '-baker-rendering' must be equal to 'screenshots' or 'three-cards'. Check it in 'book.json'.");
+ // NSLog(@"Error: param \"-baker-rendering\" should be equal to \"screenshots\" or \"three-cards\" but it's not");
+ // NSLog(@"[BakerBook] ERROR: param '-baker-rendering' must be equal to 'screenshots' or 'three-cards'. Check it in 'book.json'.");
return NO;
}
@@ -370,7 +371,7 @@ - (BOOL)validateNumber:(NSNumber *)number forParam:(NSString *)param
if (![self matchParam:param againstParamsArray:shouldBeNumber]) {
- NSLog(@"[BakerBook] ERROR: param '%@' should not be a Number. Check it in 'book.json'.", param);
+ // NSLog(@"[BakerBook] ERROR: param '%@' should not be a Number. Check it in 'book.json'.", param);
return NO;
}
@@ -387,6 +388,185 @@ - (BOOL)matchParam:(NSString *)param againstParamsArray:(NSArray *)paramsArray
return NO;
}
+
+#pragma mark - ePub processing
+// One-time minimal conversion from ePub to Hpub. For a downloaded title this function will run the first time, and then
+// save the resulting book.json file to the document directory.
+- (BOOL)convertEpubBookToHpub:(NSString *)bookJSONPath {
+
+ NSString *bookPath = [bookJSONPath stringByDeletingLastPathComponent];
+
+ // META-INF/container.xml is the foundational document for ePubs. It defines the location of the OPF file, which in turn gives the contents of the package.
+ // If this exists, we use it to find the OPF file (often in OEBPS/content.opf, but not necessarily).
+
+ NSString *containerXML = [bookPath stringByAppendingPathComponent:@"META-INF/container.xml"];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:containerXML]) {
+ NSLog(@"ePub XML found.");
+
+
+ NSError *error;
+ NSXMLParser *parser = [[NSXMLParser alloc] initWithData:[NSData dataWithContentsOfFile:containerXML options:0 error:&error]];
+ [parser setDelegate:self];
+ [parser setShouldResolveExternalEntities:NO];
+
+ [parser parse];
+ error = [parser parserError];
+ if (error) {
+ NSLog(@"[BakerBook] ERROR reading 'META-INF/container.xml': %@", error.localizedDescription);
+ }
+ else
+ NSLog(@"OK reading container.xml file.");
+
+ [parser release];
+
+ NSLog(@"opfFile: %@, opfDirectory: %@", opfFile, opfDirectory);
+
+ NSString *opfFilePath = [bookPath stringByAppendingPathComponent:opfFile];
+ NSXMLParser *opfParser = [[NSXMLParser alloc] initWithData:[NSData dataWithContentsOfFile:opfFilePath options:0 error:&error]];
+ [opfParser setDelegate:self];
+ [opfParser setShouldResolveExternalEntities:NO];
+
+ // There are two major parts of the OPF file: the manifest, which details each and every file in the epub package, and the spine, which defines the 'reading order' of the epub.
+ // The spine is what we can therefore use to create the page contents of the book.json file.
+
+ manifest = [[NSMutableDictionary alloc] init];
+ spine = [[NSMutableArray alloc] init];
+
+ [opfParser parse];
+ error = [opfParser parserError];
+ if (error)
+ NSLog(@"[BakerBook] ERROR reading '%@': %@", opfFilePath, error.localizedDescription);
+
+ [opfParser release];
+
+ return [self createBookJSONFromSpine:bookJSONPath];
+
+ }
+ return FALSE;
+}
+
+- (BOOL)createBookJSONFromSpine:(NSString *)bookJSONPath {
+
+ NSMutableDictionary *bookJSONDictionary = [[NSMutableDictionary alloc] init];
+
+ // Create a book.json dictionary with reasonable defaults (change these as to your tastes, or externalise them to a global document):
+ [bookJSONDictionary setObject:[NSNumber numberWithInteger:1] forKey:@"hpub"];
+ [bookJSONDictionary setObject:ePubTitle forKey:@"title"];
+ if (!ePubAuthor) ePubAuthor = @"";
+ [bookJSONDictionary setObject:ePubAuthor forKey:@"author"];
+ if (ePubCreator) [bookJSONDictionary setObject:ePubCreator forKey:@"creator"];
+ if (ePubDate) [bookJSONDictionary setObject:ePubDate forKey:@"date"];
+ [bookJSONDictionary setObject:ePubID forKey:@"url"];
+
+ [bookJSONDictionary setObject:@"#000000" forKey:@"-baker-background"];
+ [bookJSONDictionary setObject:@"#ffffff" forKey:@"-baker-page-numbers-color"];
+ [bookJSONDictionary setObject:[NSNumber numberWithFloat:0.3] forKey:@"-baker-page-numbers-alpha"];
+ [bookJSONDictionary setObject:@"screenshots" forKey:@"-baker-rendering"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:YES] forKey:@"-baker-vertical-bounce"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:NO] forKey:@"-baker-vertical-pagination"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:YES] forKey:@"-baker-page-turn-tap"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:YES] forKey:@"-baker-page-turn-swipe"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:NO] forKey:@"-baker-media-autoplay"];
+ [bookJSONDictionary setObject:[NSNumber numberWithBool:NO] forKey:@"-baker-index-bounce"];
+ [bookJSONDictionary setObject:[NSNumber numberWithInteger:200] forKey:@"-baker-index-height"];
+
+ if (ePubStartPage) {
+ NSUInteger fragmentLoc = [ePubStartPage rangeOfString:@"#"].location;
+ if (fragmentLoc != NSNotFound)
+ ePubStartPage = [ePubStartPage substringToIndex:fragmentLoc];
+ [bookJSONDictionary setObject:[NSNumber numberWithInteger:([spine indexOfObject:ePubStartPage]+1)] forKey:@"-baker-start-at-page"];
+ }
+
+ [bookJSONDictionary setObject:spine forKey:@"contents"];
+
+ NSError *error = nil;
+ NSData *bookJSONData = [NSJSONSerialization dataWithJSONObject:bookJSONDictionary options:0 error:&error];
+ if (bookJSONData) {
+ [bookJSONData writeToFile:bookJSONPath atomically:YES];
+ }
+ else {
+ NSLog(@"Write error: %@", error.localizedDescription);
+ [error release];
+ return FALSE;
+ }
+
+ NSLog(@"bookJSONDictionary: %@", bookJSONDictionary);
+
+ return TRUE;
+}
+
+
+#pragma mark - XML Parsing
+// What follows is some specific pattern matching to find the relevant entries in the OPF file, and match them up to entries in book.json
+-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
+ NSLog(@"didStartElement: %@", elementName);
+
+ if ([elementName isEqualToString:@"rootfile"]) {
+ if ([attributeDict objectForKey:@"full-path"]) {
+ opfFile = [attributeDict objectForKey:@"full-path"];
+ opfDirectory = [opfFile stringByDeletingLastPathComponent];
+ }
+ }
+
+ if ([elementName isEqualToString:@"item"]) {
+ if (([attributeDict objectForKey:@"id"]) && ([attributeDict objectForKey:@"href"])) {
+ [manifest setObject:[attributeDict objectForKey:@"href"] forKey:[attributeDict objectForKey:@"id"]];
+ }
+ }
+
+ if ([elementName isEqualToString:@"itemref"]) {
+ if ([attributeDict objectForKey:@"idref"]) {
+ NSString *filename = [manifest objectForKey:[attributeDict objectForKey:@"idref"]];
+ [spine addObject:[opfDirectory stringByAppendingPathComponent:filename]];
+ }
+ }
+
+ if ([elementName isEqualToString:@"dc:title"] || [elementName isEqualToString:@"dc:creator"] || [elementName isEqualToString:@"dc:publisher"] || [elementName isEqualToString:@"dc:date"] || [elementName isEqualToString:@"dc:identifier"]) {
+ element = nil;
+ element = [[NSMutableString alloc] init];
+ }
+
+ if ([elementName isEqualToString:@"reference"] && [attributeDict objectForKey:@"type"])
+ if ([[attributeDict objectForKey:@"type"] isEqualToString:@"text"])
+ ePubStartPage = [attributeDict objectForKey:@"href"];
+ if ([attributeDict objectForKey:@"epub:type=\"bodymatter\""])
+ ePubStartPage = [attributeDict objectForKey:@"epub:type=\"bodymatter\""];
+
+}
+
+-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
+ [element appendString:string];
+}
+
+-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
+ if ([elementName isEqualToString:@"dc:title"]) {
+ ePubTitle = [NSString stringWithString:element];
+ }
+ if ([elementName isEqualToString:@"dc:creator"]) {
+ ePubAuthor = [NSString stringWithString:element];
+ }
+ if ([elementName isEqualToString:@"dc:publisher"]) {
+ ePubCreator = [NSString stringWithString:element];
+ }
+ if ([elementName isEqualToString:@"dc:date"]) {
+ ePubDate = [NSString stringWithString:element];
+ }
+ if ([elementName isEqualToString:@"dc:identifier"]) {
+ ePubID = [NSString stringWithString:element];
+ }
+}
+
+// error handling
+-(void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
+ NSLog(@"XMLParser error: %@", [parseError localizedDescription]);
+}
+
+-(void)parser:(NSXMLParser *)parser validationErrorOccurred:(NSError *)validationError {
+ NSLog(@"XMLParser error: %@", [validationError localizedDescription]);
+}
+
+
+
#pragma mark - Book status management
- (BOOL)updateBookPath:(NSString *)bookPath bundled:(BOOL)bundled