Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for svg stroke width change and viewBox dimension #181

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Sources/SVGBezierPath.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,21 @@ - (instancetype)init
});
return pathCache;
}

+ (NSCache<NSURL*, NSString*> *)_svg_viewBoxCache
{
static NSCache<NSURL*, NSString*> *viewBoxCache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
viewBoxCache = [NSCache new];
});
return viewBoxCache;
}

+ (void)resetCache
{
[self._svg_pathCache removeAllObjects];
[self._svg_viewBoxCache removeAllObjects];
}

+ (NSArray<SVGBezierPath*> *)pathsFromSVGAtURL:(NSURL *)aURL
Expand All @@ -54,6 +66,29 @@ + (void)resetCache
return [[NSArray alloc] initWithArray:paths copyItems:YES];
}

+ (NSArray<SVGBezierPath*> *)pathsFromSVGAtURL:(NSURL *)aURL viewBox:(CGRect *)viewBox
{
NSArray<SVGBezierPath*> *paths = [self.class._svg_pathCache objectForKey:aURL];
NSString *strViewBox = [self.class._svg_viewBoxCache objectForKey:aURL];
if (!paths) {
paths = [self pathsFromSVGString:[NSString stringWithContentsOfURL:aURL
usedEncoding:NULL
error:NULL] viewBox:viewBox];
if (paths) {
[self.class._svg_pathCache setObject:paths forKey:aURL];
[self.class._svg_viewBoxCache setObject:[NSString stringWithFormat:@"%f %f %f %f", viewBox->origin.x, viewBox->origin.y, viewBox->size.width, viewBox->size.height] forKey:aURL];
}
}

if (strViewBox) {
NSArray *stringArray = [strViewBox componentsSeparatedByString:@" "];
if (stringArray && stringArray.count == 4) {
*viewBox = CGRectMake(CGFloat([stringArray[0] doubleValue]), CGFloat([stringArray[1] doubleValue]), CGFloat([stringArray[2] doubleValue]), CGFloat([stringArray[3] doubleValue]));
}
}
return [[NSArray alloc] initWithArray:paths copyItems:YES];
}

+ (NSArray<SVGBezierPath*> *)pathsFromSVGString:(NSString * const)svgString
{
SVGAttributeSet *cgAttrs;
Expand All @@ -68,6 +103,20 @@ + (void)resetCache
return paths;
}

+ (NSArray<SVGBezierPath*> *)pathsFromSVGString:(NSString * const)svgString viewBox:(CGRect *)viewBox
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a pointer to a CGRect? This creates the need to pass in an UnsafeMutablePointer<CGRect> in Swift.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this pointer to get the viewBox info and put this value in the _svg_viewBoxCache variable. I don't know if this part can be write in a better way :)

{
SVGAttributeSet *cgAttrs;
NSArray * const pathRefs = CGPathsFromSVGStringViewBox(svgString, &cgAttrs, viewBox);
NSMutableArray<SVGBezierPath*> * const paths = [NSMutableArray arrayWithCapacity:pathRefs.count];
for(id pathRef in pathRefs) {
SVGBezierPath * const uiPath = [self bezierPathWithCGPath:(__bridge CGPathRef)pathRef];
uiPath->_svgAttributes = [cgAttrs attributesForPath:(__bridge CGPathRef)pathRef] ?: @{};
uiPath.lineWidth = uiPath->_svgAttributes[@"stroke-width"] ? [uiPath->_svgAttributes[@"stroke-width"] doubleValue] : 1.0;
[paths addObject:uiPath];
}
return paths;
}

- (NSString *)SVGRepresentation
{
SVGMutableAttributeSet *attributes = [SVGMutableAttributeSet new];
Expand Down
32 changes: 28 additions & 4 deletions Sources/SVGEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

struct svgParser {
svgParser(NSString *);
NSArray *parse(NSMapTable **aoAttributes);
NSArray *parse(NSMapTable **aoAttributes, CGRect *viewBox);

protected:
NSString *_source;
Expand Down Expand Up @@ -95,7 +95,7 @@ @interface SVGAttributeSet () {
_source = aSource;
}

NSArray *svgParser::parse(NSMapTable ** const aoAttributes)
NSArray *svgParser::parse(NSMapTable ** const aoAttributes, CGRect *viewBox)
{
_xmlReader = xmlReaderForDoc((xmlChar *)[_source UTF8String], NULL, NULL, 0);
NSCAssert(_xmlReader, @"Failed to create XML parser");
Expand All @@ -118,7 +118,20 @@ @interface SVGAttributeSet () {
else if(type == XML_READER_TYPE_END_ELEMENT)
--depthWithinUnknownElement;
} else if(type == XML_READER_TYPE_ELEMENT && strcasecmp(tag, "svg") == 0) {
// recognize the root svg element but we don't need to do anything with it
// recognize the root svg element: we get the viewBox information
NSDictionary * const attrs = readAttributes();
if (attrs) {
NSString *strViewBox = attrs[@"viewBox"];
if (strViewBox) {
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\s{2,}" options:NSRegularExpressionCaseInsensitive error:&error];
NSString *modifiedString = [regex stringByReplacingMatchesInString:strViewBox options:0 range:NSMakeRange(0, [strViewBox length]) withTemplate:@" "];
NSArray *stringArray = [modifiedString componentsSeparatedByString:@" "];
if (stringArray && stringArray.count == 4) {
*viewBox = CGRectMake(CGFloat([stringArray[0] doubleValue]), CGFloat([stringArray[1] doubleValue]), CGFloat([stringArray[2] doubleValue]), CGFloat([stringArray[3] doubleValue]));
}
}
}
} else if(type == XML_READER_TYPE_ELEMENT && strcasecmp(tag, "path") == 0)
path = readPathTag();
else if(type == XML_READER_TYPE_ELEMENT && strcasecmp(tag, "polyline") == 0)
Expand Down Expand Up @@ -433,7 +446,18 @@ @interface SVGAttributeSet () {
NSArray *CGPathsFromSVGString(NSString * const svgString, SVGAttributeSet **outAttributes)
{
NSMapTable *attributes;
NSArray *paths = svgParser(svgString).parse(outAttributes ? &attributes : NULL);
CGRect viewBox = CGRectZero;
NSArray *paths = svgParser(svgString).parse(outAttributes ? &attributes : NULL, &viewBox);
if (outAttributes && (*outAttributes = [SVGAttributeSet new])) {
(*outAttributes)->_attributes = attributes;
}
return paths;
}

NSArray *CGPathsFromSVGStringViewBox(NSString * const svgString, SVGAttributeSet **outAttributes, CGRect *viewBox)
{
NSMapTable *attributes;
NSArray *paths = svgParser(svgString).parse(outAttributes ? &attributes : NULL, viewBox);
if (outAttributes && (*outAttributes = [SVGAttributeSet new])) {
(*outAttributes)->_attributes = attributes;
}
Expand Down
62 changes: 62 additions & 0 deletions Sources/SVGImageView.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ @interface SVGImageView ()

@implementation SVGImageView {
SVGLayer *_svgLayer;
CGRect _viewBox;

#ifdef DEBUG
dispatch_source_t _fileWatcher;
Expand All @@ -30,13 +31,15 @@ - (instancetype)initWithFrame:(CGRect)frame
if ((self = [super initWithFrame:frame])) {
_svgLayer = (SVGLayer *)self.layer;
}
_viewBox = CGRectZero;
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super initWithCoder:aDecoder])) {
_svgLayer = (SVGLayer *)self.layer;
}
_viewBox = CGRectZero;
return self;
}
#else
Expand All @@ -46,6 +49,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_svgLayer = [SVGLayer new];
self.wantsLayer = YES;
}
_viewBox = CGRectZero;
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
Expand All @@ -54,17 +58,31 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder
_svgLayer = [SVGLayer new];
self.wantsLayer = YES;
}
_viewBox = CGRectZero;
return self;
}
#endif

- (instancetype)initWithContentsOfURL:(NSURL *)url {
_viewBox = CGRectZero;
if (self = [self init]) {
[self _cr_loadSVGFromURL:url];
}
return self;
}

- (instancetype)initWithContentsOfURL:(NSURL *)url readViewBox:(bool)readViewBox {
_viewBox = CGRectZero;
if (self = [self init]) {
[self _cr_loadSVGFromURL:url readViewBox:readViewBox];
}
return self;
}


- (CGRect)getViewBox {
return _viewBox;
}

#if TARGET_OS_IPHONE
+ (Class)layerClass
Expand Down Expand Up @@ -163,6 +181,40 @@ - (void)_cr_loadSVGFromURL:(NSURL *)url
_svgLayer.paths = [SVGBezierPath pathsFromSVGAtURL:url];
}

- (void)_cr_loadSVGFromURL:(NSURL *)url readViewBox:(bool)readViewBox {
if(!readViewBox) {
[self _cr_loadSVGFromURL:url];
return;
}
#if defined(DEBUG) && !defined(POCKETSVG_DISABLE_FILEWATCH)
if(_fileWatcher)
dispatch_source_cancel(_fileWatcher);

int const fdes = open([url fileSystemRepresentation], O_RDONLY);
_fileWatcher = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fdes,
DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE,
dispatch_get_main_queue());
dispatch_source_set_event_handler(_fileWatcher, ^{
unsigned long const l = dispatch_source_get_data(self->_fileWatcher);
if(l & DISPATCH_VNODE_DELETE || l & DISPATCH_VNODE_WRITE) {
NSLog(@"Reloading %@", url.lastPathComponent);
dispatch_source_cancel(self->_fileWatcher);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[SVGBezierPath resetCache];
[self _cr_loadSVGFromURL:url readViewBox:readViewBox];
});
}
});
dispatch_source_set_cancel_handler(_fileWatcher, ^{
close(fdes);
});
dispatch_resume(_fileWatcher);
#endif

_svgLayer.paths = [SVGBezierPath pathsFromSVGAtURL:url viewBox:&_viewBox];
}

- (void)dealloc
{
#ifdef DEBUG
Expand All @@ -182,6 +234,16 @@ - (PSVGColor *)strokeColor { return _svgLayer.strokeColor
: nil; }
- (void)setStrokeColor:(PSVGColor * const)aColor { _svgLayer.strokeColor = aColor.CGColor; }


- (CGFloat)strokeWidth
{
return _svgLayer.strokeWidth;
}
- (void)setStrokeWidth:(CGFloat const)aSize
{
_svgLayer.strokeWidth = aSize;
}

- (CGSize)sizeThatFits:(CGSize)aSize
{
return self.layer.preferredFrameSize;
Expand Down
14 changes: 13 additions & 1 deletion Sources/SVGLayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ - (instancetype)init
- (instancetype)initWithContentsOfURL:(NSURL *)url
{
if ((self = [self init])) {
[self setPaths:[SVGBezierPath pathsFromSVGAtURL:url]];
CGRect viewBox = CGRectMake(0, 0, 0, 0);
[self setPaths:[SVGBezierPath pathsFromSVGAtURL:url viewBox:&viewBox]];
}
return self;
}
Expand Down Expand Up @@ -111,6 +112,14 @@ - (void)setStrokeColor:(CGColorRef)aColor
[_shapeLayers setValue:(__bridge id)_strokeColor forKey:@"strokeColor"];
}

- (void)setStrokeWidth:(CGFloat)aSize
{
_strokeWidth = aSize;

//[_shapeLayers setValue:(__bridge id)_strokeWidth forKey:@"strokeWidth"];
}


- (CGSize)preferredFrameSize
{
return SVGBoundingRectForPaths(_untouchedPaths).size;
Expand Down Expand Up @@ -150,6 +159,9 @@ - (void)layoutSublayers
CGFloat lineScale = (frame.size.width/size.width + frame.size.height/size.height) / 2.0;
layer.lineWidth = path.lineWidth * lineScale;
}
else {
layer.lineWidth = _strokeWidth;
}
layer.fillRule = [path.svgAttributes[@"fill-rule"] isEqualToString:@"evenodd"] ? kCAFillRuleEvenOdd : kCAFillRuleNonZero;
NSString *lineCap = path.svgAttributes[@"stroke-linecap"];
layer.lineCap = [lineCap isEqualToString:@"round"] ? kCALineCapRound : ([lineCap isEqualToString:@"square"] ? kCALineCapSquare : kCALineCapButt);
Expand Down
12 changes: 11 additions & 1 deletion Sources/include/SVGBezierPath.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ FOUNDATION_EXTERN void SVGDrawPathsWithBlock(NSArray<SVGBezierPath*> * const pat
@property(nonatomic, readonly) NSString *SVGRepresentation;



/*!
* @brief Returns an array of SVGBezierPaths given an SVG's URL.
*
Expand All @@ -51,11 +50,22 @@ FOUNDATION_EXTERN void SVGDrawPathsWithBlock(NSArray<SVGBezierPath*> * const pat
+ (NSArray<SVGBezierPath*> *)pathsFromSVGAtURL:(NSURL *)aURL;


/*!
* @brief Returns an array of SVGBezierPaths given an SVG's URL.
*
* @param aURL The URL from which to load an SVG.
* @param viewBox The SVG viewBox tag rect.
*
*/
+ (NSArray<SVGBezierPath*> *)pathsFromSVGAtURL:(NSURL *)aURL viewBox:(CGRect *)viewBox;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the SVG already contains a viewBox tag?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this question. Do you mean if the SVGBezierPath class? This code read the viewBox tag from the SVG file and return to the caller (by the viewBox pointer) these values in a CGRect struct.

Copy link
Collaborator

@arielelkin arielelkin Oct 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok now I understand the intention of your code and why you pass a pointer to a CGRect (I initially thought you wanted the caller to pass a CGRect as the viewBox to render the SVG in).

If the caller wants to read the viewBox tag in the SVG file, what if we expose this property on SVGBezierPath:

@property(nonatomic, readonly) CGrect viewBox;

This way, no need to pass a pointer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, you are right, but I use the SVGBezierPath static function to get the viewBox information so I can't store the value in a class property.
Do you have any suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ciao Alessio, sorry for the delay, I'll be able to take a look at this soon.



/*!
* @brief Returns an array of paths given the XML string of an SVG.
*
*/
+ (NSArray<SVGBezierPath*> *)pathsFromSVGString:(NSString *)svgString;
+ (NSArray<SVGBezierPath*> *)pathsFromSVGString:(NSString *)svgString viewBox:(CGRect *)viewBox;

/*!
* @brief Returns a new path with the values of `attributes` added to `svgAttributes`
Expand Down
1 change: 1 addition & 0 deletions Sources/include/SVGEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extern "C" {
*
*/
NSArray *CGPathsFromSVGString(NSString *svgString, SVGAttributeSet **attributes);
NSArray *CGPathsFromSVGStringViewBox(NSString *svgString, SVGAttributeSet **attributes, CGRect *viewBox);

/*!
* @brief Returns a single CGPathRef parsed from the contents of a single string formatted like the d attribute inside a path element
Expand Down
15 changes: 15 additions & 0 deletions Sources/include/SVGImageView.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ IB_DESIGNABLE
*
*/
- (instancetype)initWithContentsOfURL:(NSURL *)url;
- (instancetype)initWithContentsOfURL:(NSURL *)url readViewBox:(bool)readViewBox;


/*!
* @brief retrieve the viewBox size saved inside the original SVG file.
*
*/
- (CGRect)getViewBox;


/*!
Expand All @@ -53,6 +61,13 @@ IB_DESIGNABLE
@property(nonatomic, copy) IBInspectable PSVGColor *strokeColor;


/*!
* @brief Specifies the line thickness of every paths.
*
*/
@property(nonatomic) IBInspectable CGFloat strokeWidth;


/*!
* @brief Specifies whether line thickness should be scaled when scaling paths.
*
Expand Down
7 changes: 7 additions & 0 deletions Sources/include/SVGLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic) CGColorRef strokeColor;


/*!
* @brief Specifies the line thickness for every paths.
*
*/
@property(nonatomic) CGFloat strokeWidth;


/*!
* @brief Specifies whether line thickness should be scaled when scaling paths.
*
Expand Down