Files
baRSS/baRSS/Helper/TinySVG.m
2025-12-08 16:31:53 +01:00

184 lines
5.8 KiB
Objective-C

#include "TinySVG.h"
struct SVGState {
CGFloat scale; // technically not part of parser but easier to pass along
char op;
float x, y;
bool prevDot;
float num[6];
uint8 iNum;
char buf[15];
uint8 iBuf;
};
# pragma mark - Helper
/// if number buffer contains anything, write it to num array and start new buffer
static void finishNum(struct SVGState *state) {
if (state->iBuf > 0) {
state->buf[state->iBuf] = '\0';
state->num[state->iNum++] = (float)atof(state->buf);
state->iBuf = 0;
state->prevDot = false;
}
}
/// All numbers stored in num array, finalize SVG path operation and add path to @c CGContext
static void finishOp(CGMutablePathRef path, struct SVGState *state) {
char op = state->op;
if (op >= 'a' && op <= 'z') {
// convert relative to absolute coordinates
for (uint8 t = 0; t < state->iNum; t++) {
state->num[t] += (t % 2 || op == 'v') ? state->y : state->x;
}
// convert to upper-case
op = op - 'a' + 'A';
}
if (op == 'Z') {
CGPathCloseSubpath(path);
} else if (op == 'V' && state->iNum == 1) {
state->y = state->num[0];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'H' && state->iNum == 1) {
state->x = state->num[0];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'M' && state->iNum == 2) {
state->x = state->num[0];
state->y = state->num[1];
CGPathMoveToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
// Edge-case: "M 1 2 3 4 5 6" is valid SVG after move 1,2 all remaining points are lines (3,4 and 5,6)
// For this case we overwrite op here. It will be overwritten again if a new op starts. Else, assume line-op.
state->op = (state->op == 'm') ? 'l' : 'L';
} else if (op == 'L' && state->iNum == 2) {
state->x = state->num[0];
state->y = state->num[1];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'Q' && state->iNum == 4) {
state->x = state->num[2];
state->y = state->num[3];
CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[0] * state->scale, state->num[1] * state->scale, state->x * state->scale, state->y * state->scale);
} else if (op == 'C' && state->iNum == 6) {
state->x = state->num[4];
state->y = state->num[5];
CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[2] * state->scale, state->num[3] * state->scale, state->x * state->scale, state->y * state->scale);
} else {
NSLog(@"Unsupported SVG operation %c %d", state->op, state->iNum);
}
state->iNum = 0;
}
/// current number not finished yet. Append another char to internal buffer
inline static void continueNum(char chr, struct SVGState *state) {
state->buf[state->iBuf++] = chr;
}
# pragma mark - Parser
/// very basic svg path parser.
static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef path) {
struct SVGState state = {
.scale = scale,
.op = '_',
.x = 0,
.y = 0,
.prevDot = false,
//.num = {0, 0, 0, 0, 0, 0},
.iNum = 0,
//.buf = " ",
.iBuf = 0,
};
unsigned long len = strlen(code);
for (unsigned long i = 0; i < len; i++) {
char chr = code[i];
if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')) {
if (state.op != '_') {
finishNum(&state);
finishOp(path, &state);
}
state.op = chr;
} else if (chr >= '0' && chr <= '9') {
continueNum(chr, &state);
} else if (chr == '-' && state.iBuf == 0) {
continueNum(chr, &state);
} else if (chr == '.' && !state.prevDot) {
continueNum(chr, &state);
state.prevDot = true;
} else { // any number-separating character
finishNum(&state);
// Edge-Case: SVG can reuse the previous operation without declaration
// e.g. you can draw four lines with "L1 2 3 4 5 6 7 8"
// or two curves with "c1 2 3 4 5 6 -1 -2 -3 -4 -5 -6"
// Therefore we need to complete the operation if the number of arguments is reached
if (state.iNum == 1 && strchr("HhVv", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 4 && strchr("Qq", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) {
finishOp(path, &state);
}
if (chr == '-') {
continueNum(chr, &state);
} else if (chr == '.') {
continueNum(chr, &state);
state.prevDot = true;
}
}
}
}
# pragma mark - External API
/// calls @c tinySVG_path and handles @c CGPath creation and release.
void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
CGMutablePathRef path = CGPathCreateMutable();
tinySVG_parse(code, scale, path);
CGContextAddPath(context, path);
CGPathRelease(path);
}
/// calls @c CGPathAddArc with full circle
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddArc(tmp, NULL, x * scale, y * scale, radius * scale, 0, M_PI * 2, clockwise);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// Calls @c CGContextAddRect or @c CGPathAddRoundedRect (optional).
/// @param cornerRadius Use @c <=0 for no corners. Use half of @c min(w,h) for a full circle.
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
if (scale != 1.0) {
rect = CGRectMake(rect.origin.x * scale, rect.origin.y * scale,
rect.size.width * scale, rect.size.height * scale);
}
if (cornerRadius > 0) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddRoundedRect(tmp, NULL, rect, cornerRadius * scale, cornerRadius * scale);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
} else {
CGContextAddRect(context, rect);
}
}