Skip to content

Commit

Permalink
Implement contextOptions for advanced control; include module for gen…
Browse files Browse the repository at this point in the history
…erating local image files / URIs
  • Loading branch information
Ben Turley committed May 29, 2015
1 parent eee357f commit f4895a8
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 27 deletions.
3 changes: 2 additions & 1 deletion ASCIImage.h
Expand Up @@ -5,7 +5,8 @@
// Copyright (c) 2015 Ben Turley. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "PARImage+ASCIIInput.h"

@interface ASCIImage : UIView

Expand Down
3 changes: 3 additions & 0 deletions ASCIImage.js
Expand Up @@ -5,6 +5,7 @@ var ASCIImage = React.createClass({
propTypes: {
ascii: React.PropTypes.arrayOf(React.PropTypes.string),
color: React.PropTypes.string,
contextOptions: React.PropTypes.arrayOf(React.PropTypes.object),
style: React.View.propTypes.style,
},

Expand All @@ -20,12 +21,14 @@ var ASCIImage = React.createClass({
style={[{ width: defaultDimensions[0], height: defaultDimensions[1]}, this.props.style]}
ascii={this.props.ascii}
color={this.props.color}
contextOptions={this.props.contextOptions}
/>
);
}

});

var RNASCIImage = React.requireNativeComponent('RCTASCIImage', ASCIImage);
ASCIImage.Writer = require('NativeModules').ASCIImageWriter;

module.exports = ASCIImage;
19 changes: 11 additions & 8 deletions ASCIImage.m
Expand Up @@ -6,17 +6,17 @@
//

#import "ASCIImage.h"
#import "PARImage+ASCIIInput.h"
#import "ASCIImageCommon.h"

#define DEFAULT_COLOR [UIColor blackColor]
#define OBSERVED_KEYS @[NSStringFromSelector(@selector(ascii)), NSStringFromSelector(@selector(color))]
#define OBSERVED_KEYS @[NSStringFromSelector(@selector(ascii)), NSStringFromSelector(@selector(color)), NSStringFromSelector(@selector(contextOptions))]

@interface ASCIImage () {
UIImageView *_imageView;
}

@property (nonatomic, strong) NSArray *ascii;
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, strong) NSArray *contextOptions;

@end

Expand All @@ -31,7 +31,6 @@ - (instancetype)init
[self addObserver:self forKeyPath:key options:0 context:nil];
}

self.color = DEFAULT_COLOR;
_imageView = [[UIImageView alloc] initWithFrame:self.bounds];
[_imageView setContentMode:UIViewContentModeScaleAspectFit];
[self addSubview:_imageView];
Expand All @@ -48,14 +47,18 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
- (void)updateASCIImage
{
[_imageView setFrame:self.bounds];
[_imageView setImage:[PARImage imageWithASCIIRepresentation:_ascii scaleFactor:[self bestScale] color:self.color shouldAntialias:YES]];
[_imageView setImage:[ASCIImageCommon imageFromASCII:_ascii scaleFactor:[self bestScale] defaultColor:_color contextOptions:_contextOptions]];
}

- (CGFloat)bestScale
{
NSUInteger imgWidth = 0;
if (self.ascii != nil && self.ascii.firstObject != nil) imgWidth = [(NSString*)self.ascii.firstObject length];
return (ceil(self.frame.size.width / imgWidth) + 1.0) * [UIScreen mainScreen].scale;
NSArray *strictRep = [PARImage strictASCIIRepresentationFromLenientASCIIRepresentation:self.ascii];

if (strictRep != nil) {
NSUInteger imgWidth = [(NSString*)strictRep.firstObject length] * [UIScreen mainScreen].scale;
return (ceil(self.frame.size.width / imgWidth) + 1.0) * [UIScreen mainScreen].scale;
}
return [UIScreen mainScreen].scale;
}

- (void)layoutSubviews
Expand Down
15 changes: 15 additions & 0 deletions ASCIImageCommon.h
@@ -0,0 +1,15 @@
//
// ASCIImageCommon.h
//
// Created by Ben Turley on 5/29/15.
// Copyright (c) 2015 Ben Turley. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "PARImage+ASCIIInput.h"

@interface ASCIImageCommon : NSObject

+ (PARImage*)imageFromASCII:(NSArray*)ascii scaleFactor:(CGFloat)scale defaultColor:(UIColor*)color contextOptions:(NSArray*)contextOptions;

@end
86 changes: 86 additions & 0 deletions ASCIImageCommon.m
@@ -0,0 +1,86 @@
//
// ASCIIImageCommon.m
//
// Created by Ben Turley on 5/29/15.
// Copyright (c) 2015 Ben Turley. All rights reserved.
//

#import "ASCIImageCommon.h"
#import "RCTConvert.h"

#define DEFAULT_COLOR [UIColor blackColor]

typedef enum {
kASCIIContextFillColor = 0,
kASCIIContextStrokeColor,
kASCIIContextLineWidth,
kASCIIContextShouldClose,
kASCIIContextShouldAntialias
} ASCIIContextKey;

@implementation ASCIImageCommon


+ (PARImage*)imageFromASCII:(NSArray*)ascii scaleFactor:(CGFloat)scale defaultColor:(UIColor*)color contextOptions:(NSArray*)contextOptions
{
return [PARImage imageWithASCIIRepresentation:ascii scaleFactor:scale contextHandler:^(NSMutableDictionary *context) {
// Default context options
UIColor *defaultColor = (color == nil) ? DEFAULT_COLOR : color;
[context setObject:defaultColor forKey:ASCIIContextFillColor];
[context setObject:defaultColor forKey:ASCIIContextStrokeColor];
[context setObject:[NSNumber numberWithFloat:1.0] forKey:ASCIIContextLineWidth];
[context setObject:[NSNumber numberWithBool:TRUE] forKey:ASCIIContextShouldClose];
[context setObject:[NSNumber numberWithBool:TRUE] forKey:ASCIIContextShouldAntialias];

// Override context options from user-supplied values
if (contextOptions != nil) {
NSInteger shapeIndex = [[context objectForKey:ASCIIContextShapeIndex] integerValue];
NSDictionary *optionsDict = (shapeIndex < [contextOptions count]) ? [contextOptions objectAtIndex:shapeIndex] : nil;
if (optionsDict != nil && [optionsDict isKindOfClass:[NSDictionary class]]) {

for (NSString *key in [optionsDict allKeys]) {

id optionValue = [optionsDict valueForKey:key];
if (optionValue != nil) {
switch ([[[self contextKeyMap] objectForKey:[[self javascriptKeyMap] objectForKey:key]] integerValue]) {
case kASCIIContextFillColor:
case kASCIIContextStrokeColor:
[context setObject:[RCTConvert UIColor:optionValue] forKey:[[self javascriptKeyMap] objectForKey:key]];
break;
case kASCIIContextLineWidth:
case kASCIIContextShouldClose:
case kASCIIContextShouldAntialias:
[context setObject:[RCTConvert NSNumber:optionValue] forKey:[[self javascriptKeyMap] objectForKey:key]];
break;
}
}
}
}
}

}];
}

+ (NSDictionary*)contextKeyMap
{
return @{
ASCIIContextFillColor : @(kASCIIContextFillColor),
ASCIIContextStrokeColor : @(kASCIIContextStrokeColor),
ASCIIContextLineWidth : @(kASCIIContextLineWidth),
ASCIIContextShouldClose : @(kASCIIContextShouldClose),
ASCIIContextShouldAntialias : @(kASCIIContextShouldAntialias)
};
}

+ (NSDictionary*)javascriptKeyMap
{
return @{
@"fillColor" : ASCIIContextFillColor,
@"strokeColor" : ASCIIContextStrokeColor,
@"lineWidth" : ASCIIContextLineWidth,
@"shouldClose" : ASCIIContextShouldClose,
@"shouldAntialias" : ASCIIContextShouldAntialias
};
}

@end
12 changes: 12 additions & 0 deletions ASCIImageWriter.h
@@ -0,0 +1,12 @@
//
// ASCIImageWriter.h
//
// Created by Ben Turley on 5/28/15.
// Copyright (c) 2015 Ben Turley. All rights reserved.
//

#import <RCTBridgeModule.h>

@interface ASCIImageWriter : NSObject <RCTBridgeModule>

@end
114 changes: 114 additions & 0 deletions ASCIImageWriter.m
@@ -0,0 +1,114 @@
//
// ASCIImageWriter.m
//
// Created by Ben Turley on 5/28/15.
// Copyright (c) 2015 Ben Turley. All rights reserved.
//

#import "ASCIImageWriter.h"
#import "ASCIImageCommon.h"
#import "PARImage+ASCIIInput.h"
#import "RCTConvert.h"
#import <CommonCrypto/CommonDigest.h>

@implementation ASCIImageWriter

RCT_EXPORT_MODULE()


RCT_EXPORT_METHOD(createImageFromASCII:(NSArray*)ascii withColor:(UIColor*)color width:(NSInteger)imageWidth callback:(RCTResponseSenderBlock)imageURLCallback)
{
[self createImageFromASCIIWithOptions:ascii withColor:color width:imageWidth contextOptions:nil callback:imageURLCallback];
}

RCT_EXPORT_METHOD(createImageFromASCIIWithOptions:(NSArray*)ascii withColor:(UIColor*)color width:(NSInteger)imageWidth contextOptions:(NSArray*)contextOptions callback:(RCTResponseSenderBlock)imageURLCallback)
{
NSString *supportDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
supportDirectory = [supportDirectory stringByAppendingPathComponent:@"ASCIImageCache"];
[self checkDirectory:supportDirectory];

NSArray *strictRep = [PARImage strictASCIIRepresentationFromLenientASCIIRepresentation:ascii];
NSString *baseImagePath = nil;

NSDictionary *sizes = @{@"" : @(1.0), @"@2x" : @(2.0), @"@3x" : @(3.0) };
for (NSString *key in [sizes allKeys]) {
NSString *filename = [self fileNameForASCII:strictRep withColor:color width:imageWidth contextOptions:contextOptions suffix:key];
NSString *imagePath = [supportDirectory stringByAppendingPathComponent:filename];
if ([[sizes objectForKey:key] floatValue] == 1.0) {
baseImagePath = imagePath;
}

// Only generate/create image if it doesn't already exist
if (![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
CGFloat actualSize = imageWidth * [[sizes objectForKey:key] floatValue];
CGFloat scale = 1.0;
if (strictRep != nil) {
scale = actualSize / [strictRep.firstObject length];
}

UIImage *image = [ASCIImageCommon imageFromASCII:strictRep scaleFactor:scale defaultColor:color contextOptions:contextOptions];
[[NSFileManager defaultManager] createFileAtPath:imagePath contents:UIImagePNGRepresentation(image) attributes:nil];
}
}

imageURLCallback(@[ baseImagePath ]);
}

- (void)checkDirectory:(NSString*)directory
{
// If directory doesn't exist
if (![[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:NULL]) {
NSError *error = nil;

// Create directory
if (![[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"%@", error.localizedDescription);
}
}
}

- (NSString*)fileNameForASCII:(NSArray*)ascii withColor:(UIColor*)color width:(NSInteger)imageWidth contextOptions:(NSArray*)contextOptions suffix:(NSString*)suffix
{
NSData *jsonContextOptions = [NSJSONSerialization dataWithJSONObject:contextOptions options:0 error:NULL];

NSArray *key = @[
[ascii componentsJoinedByString:@"\n"],
[self hexStringFromColor:color],
[[NSString alloc] initWithData:jsonContextOptions encoding:NSUTF8StringEncoding],
[NSString stringWithFormat:@"%ld", imageWidth]
];
return [NSString stringWithFormat:@"%@%@.png", [self md5:[key componentsJoinedByString:@"|"]], suffix];
}



- (NSString*)md5:(NSString*)inputString
{
const char *cstr = [inputString UTF8String];
unsigned char result[16];
CC_MD5(cstr, (unsigned int)strlen(cstr), result);

return [NSString stringWithFormat:
@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}

- (NSString*)hexStringFromColor:(UIColor *)color
{
const CGFloat *components = CGColorGetComponents(color.CGColor);

CGFloat r = components[0];
CGFloat g = components[1];
CGFloat b = components[2];

return [NSString stringWithFormat:@"#%02lX%02lX%02lX",
lroundf(r * 255),
lroundf(g * 255),
lroundf(b * 255)];
}

@end
1 change: 1 addition & 0 deletions RCTASCIImageManager.m
Expand Up @@ -21,5 +21,6 @@ - (UIView *)view

RCT_EXPORT_VIEW_PROPERTY(ascii, NSStringArray)
RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
RCT_EXPORT_VIEW_PROPERTY(contextOptions, NSArray)

@end
36 changes: 33 additions & 3 deletions README.md
Expand Up @@ -35,7 +35,7 @@ var myImage = [
'· · · · # # # · · · ',
'· · · 7 # # · · · · ',
'· · · 6 5 · · · · · '
]
];

var App = React.createClass({

Expand All @@ -57,15 +57,45 @@ AppRegistry.registerComponent('App', () => App);

Result:

![Example image](https://raw.githubusercontent.com/turley/react-native-asciimage/master/example.png)
<img src="https://raw.githubusercontent.com/turley/react-native-asciimage/master/example.png" height="27" alt="Example Result" />

## Alternate Usage

In some situations it's useful to work with an image URI rather than an `Image` component (e.g., for use in `TabBarIOS.Item` or `NavigatorIOS`). To generate a (local) image URI, do the following:

```
...
var ASCIImage = require('react-native-asciimage');
var ASCIImageWriter = ASCIImage.Writer;
ASCIImageWriter.createImageFromASCII(myImage, '#ffffff', 40, function(imageURI) {
// Use the imageURI wherever it's needed
console.log(imageURI);
});
```

This will create an image saved in your application's `Caches` directory for the specified color (example: `#ffffff`) and width (example: `40`). It will automatically generate standard, @2x, and @3x sizes of the image, and will use cached images when they exist.

For advanced options (which would normally be passed in via the `contextOptions` prop), you can use the expanded form:

```
var options = [
{ fillColor: "rgba(0, 0, 0, 0)", lineWidth: 5 }, // First shape
{ fillColor: "0000ff" } // Second shape
];
ASCIImageWriter.createImageFromASCIIWithOptions(myImage, '#ffffff', 40, options, function(imageURI) {
console.log(imageURI);
}
```

## Props

The following properties are used:
The following props are used:

- **`ascii`** _(Array)_ REQUIRED - an array of strings representing rows of the image (see the ASCIImage [documention](https://github.com/cparnot/ASCIImage) for details)
- **`color`** _(String)_ the color value to use for the foreground, e.g. `#0000FF` or `rgba(0, 255, 0, 0.5)`. Default: `#000000`
- **`contextOptions`** _(Array)_ Array of options for advanced control over the drawing of each shape. Array indices correspond to the `ASCIIContextShapeIndex` for each shape passed to the underlying `contextHandler` block. Array values should be plain JavaScript objects with any of the following keys: `fillColor`, `strokeColor`, `lineWidth`, `shouldClose`, or `shouldAntialias`.


---
Expand Down

0 comments on commit f4895a8

Please sign in to comment.