//| Copyright: (C) 2020-2024 Kevin Larke //| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. #include "cwCommon.h" #include "cwLog.h" #include "cwCommonImpl.h" #include "cwMem.h" #include "cwFile.h" #include "cwText.h" #include "cwTest.h" #include "cwObject.h" #include "cwNumericConvert.h" #include "cwSvg.h" namespace cw { namespace svg { enum { kLineTId, kPLineTId, kRectTId, kTextTId, kPixelTId }; // Attribute record used for CSS and SVG element attributes typedef struct attr_str { char* label; char* value; struct attr_str* link; } attr_t; // CSS selector record with attribute list typedef struct css_str { char* selectorStr; attr_t* attrL; struct css_str* link; } css_t; // SVG element record typedef struct ele_str { unsigned typeId; // kLineTId,kRectPId,... attr_t* attrL; // element attributes (e.g. id, class) css_t* css; // style attributes double x0; // left,top double y0; double x1; // right,bot double y1; char* text; double* xV; // poly line coords double* yV; unsigned yN; struct ele_str* link; } ele_t; typedef struct color_map_str { unsigned id; unsigned colorN; unsigned* colorV; struct color_map_str* link; } color_map_t; typedef struct svg_str { css_t* cssL; // CSS selector records color_map_t* cmapL; // Color maps ele_t* eleL; // SVG elements double dx; // Offset new elements by this amount double dy; ele_t* curEle; // Last SVG element allocated } svg_t; inline svg_t* _handleToPtr(handle_t h ) { return handleToPtr(h); } void _destroy_attr_list( attr_t* a ) { while( a != nullptr ) { attr_t* a0 = a->link; mem::release(a->label); mem::release(a->value); mem::release(a); a = a0; } } void _destroy_css( css_t* r ) { if( r != nullptr ) { mem::release(r->selectorStr); _destroy_attr_list(r->attrL); mem::release(r); } } rc_t _destroy( struct svg_str* p ) { ele_t* e = p->eleL; while( e != nullptr ) { ele_t* e0 = e->link; mem::release(e->text); mem::release(e->xV); mem::release(e->yV); _destroy_attr_list(e->attrL); _destroy_css(e->css); mem::release(e); e = e0; } color_map_t* c = p->cmapL; while( c != nullptr ) { color_map_t* c0 = c->link; mem::release(c->colorV); mem::release(c); c = c0; } css_t* r = p->cssL; while( r != nullptr ) { css_t* r0 = r->link; _destroy_css(r); r = r0; } mem::release(p); return kOkRC; } rc_t _install_cmap( svg_t* p, unsigned id, unsigned* cV, unsigned cN ) { color_map_t* cmap = mem::allocZ(1); cmap->id = id; cmap->colorN = cN; cmap->colorV = cV; cmap->link = p->cmapL; p->cmapL = cmap; return kOkRC; } color_map_t* _cmap_from_id( svg_t* p, unsigned id ) { color_map_t* cm; for(cm=p->cmapL; cm!=nullptr; cm=cm->link) if( cm->id == id ) return cm; cwLogWarning("Unknown color map id:%i.", id ); return nullptr; } rc_t _install_basic_eight_cmap( svg_t* p ) { unsigned colorN = 8; unsigned* colorV = mem::allocZ(colorN); colorV[0] = 0x0000ff; colorV[1] = 0x00ff00; colorV[2] = 0xff0000; colorV[3] = 0x00ffff; colorV[4] = 0xffff00; colorV[5] = 0xff00ff; colorV[6] = 0x000000; colorV[7] = 0xffffff; return _install_cmap( p, kStandardColorMapId, colorV, colorN ); } rc_t _install_gray_scale_cmap( svg_t* p ) { unsigned colorN = 256; unsigned* colorV = mem::allocZ(colorN); for(unsigned i=0; i(cm->colorN); for(unsigned i=0; icolorN; ++i) colorV[i] = 0xffffff - cm->colorV[i]; return _install_cmap( p, kInvGrayScaleColorMapId, colorV, cm->colorN ); } rc_t _install_heat_cmap( svg_t* p ) { unsigned colorN = 256; unsigned* colorV = mem::allocZ(colorN); double rV[] = { 0.0, 0.0, 1.0, 1.0 }; double gV[] = { 1.0, 0.0, 0.0, 1.0 }; double bV[] = { 1.0, 1.0, 0.0, 0.0 }; const unsigned xN = cwCountOf(rV); for(unsigned i=1,k=0; i(1); e->typeId = typeId; e->x0 = x; e->y0 = y; e->x1 = x+w; e->y1 = y+h; e->text = mem::duplStr(text); e->yV = yN>0 && yV!=nullptr ? mem::allocDupl(yV,yN) : nullptr; e->xV = yN>0 && xV!=nullptr ? mem::allocDupl(xV,yN) : nullptr; e->yN = yN; if( yN == 0 ) { e->x0 += p->dx; e->y0 += p->dy; e->x1 += p->dx; e->y1 += p->dy; } else { if( e->xV == nullptr ) { e->xV = mem::alloc(e->yN); for(unsigned i=0; iyN; ++i) e->xV[i] = i; } for(unsigned i=0; iyV[i] += p->dy; e->xV[i] += p->dx; } } ele_t* e0 = nullptr; ele_t* e1 = p->eleL; while( e1!=nullptr) { e0 = e1; e1 = e1->link; } if( e0 == NULL ) p->eleL = e; else e0->link = e; p->curEle = e; return rc; } void _offsetEles(svg_t* p, double dx, double dy ) { for(ele_t* e=p->eleL; e!=nullptr; e=e->link) { if( e->yN == 0 ) { e->x0 += dx; e->x1 += dx; e->y0 += dy; e->y1 += dy; } else { for(unsigned i=0; iyN; ++i) { e->yV[i] += dy; e->xV[i] += dx; } } } } void _ele_extents( ele_t* e, double& min_x, double& min_y, double& max_x, double& max_y ) { if( e->yN==0 ) { min_x = std::min(e->x0,e->x1); max_x = std::max(e->x0,e->x1); min_y = std::min(e->y0,e->y1); max_y = std::max(e->y0,e->y1); } else { min_y = e->yV[0]; max_y = e->yV[0]; min_x = e->xV[0]; max_x = e->xV[0]; for(unsigned i=0; iyN; ++i) { min_x = std::min( min_x, e->yV[i] ); max_x = std::max( max_x, e->yV[i] ); min_y = std::min( min_y, e->xV[i] ); max_y = std::max( max_y, e->xV[i] ); } } } void _extents( svg_t* p, double& min_x, double& min_y, double& max_x, double& max_y ) { if( p->eleL == NULL ) return; ele_t* e = p->eleL; _ele_extents(e,min_x,min_y,max_x,max_y); for(e=e->link; e!=NULL; e=e->link) { double e_min_x,e_min_y,e_max_x,e_max_y; _ele_extents(e,e_min_x,e_min_y,e_max_x,e_max_y); min_x = std::min(min_x,e_min_x); max_x = std::max(max_x,e_max_x); min_y = std::min(min_y,e_min_y); max_y = std::max(max_y,e_max_y); } } double _size( svg_t* p, double& widthRef, double& heightRef ) { double min_x,min_y,max_x,max_y; widthRef = 0; heightRef = 0; _extents( p, min_x, min_y, max_x, max_y ); widthRef = max_x - min_x; heightRef = max_y - min_y; return max_y; } void _flipY( svg_t* p, unsigned height ) { ele_t* e = p->eleL; for(; e!=NULL; e=e->link) { if( e->yN == 0 ) { e->y0 = (-e->y0) + height; e->y1 = (-e->y1) + height; if( e->typeId == kRectTId ) { double t = e->y1; e->y1 = e->y0; e->y0 = t; } } else { for(unsigned i=0; iyN; ++i) e->yV[i] = (-e->yV[i]) + height; } } } // Write a HTML element attribute list char* _print_ele_attr_list( const attr_t* attrL, char*& s ) { for(const attr_t* a=attrL; a!=nullptr; a=a->link) s = mem::printp(s,"%s=\"%s\" ",a->label,a->value); return s; } // Write a CSS record attribute list char* _print_css_attr_list( const attr_t* attrL, char*& s ) { for(const attr_t* a=attrL; a!=nullptr; a=a->link) s = mem::printp(s,"%s: %s;",a->label,a->value); return s; } // Print a CSS record char* _print_css( const css_t* r, char*& s ) { char* s0 = nullptr; s0 = _print_css_attr_list( r->attrL, s0 ); s = mem::printp(s,"%s { %s }\n", r->selectorStr, s0 ); mem::release(s0); return s; } char* _print_css_list( svg_t* p, char*& s ) { for( const css_t* r=p->cssL; r!=nullptr; r=r->link) s = _print_css(r,s); return s; } rc_t _writeCssFile( svg_t* p, const char* fn ) { rc_t rc; file::handle_t fH; char* s = nullptr; if( p->cssL == nullptr ) return kOkRC; if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC ) return cwLogError(rc,"CSS file create failed on '%s'.",cwStringNullGuard(fn)); s = _print_css_list(p,s); file::printf(fH,"%s\n",s); mem::release(s); file::close(fH); return rc; } css_t* _cssSelectorToRecd( svg_t* p, const char* selectorStr ) { css_t* r = p->cssL; for(; r!=nullptr; r=r->link) if( strcmp(selectorStr,r->selectorStr)==0 ) return r; return nullptr; } css_t* _cssCreate( svg_t* p ) { return mem::allocZ(); } css_t* _cssFindOrCreate( svg_t* p, const char* selectorStr ) { css_t* r; if((r = _cssSelectorToRecd(p,selectorStr)) == nullptr ) { r = _cssCreate(p); r->selectorStr = mem::duplStr(selectorStr); r->link = p->cssL; p->cssL = r; } return r; } char* _cssGenUniqueIdLabel( svg_t* p ) { unsigned i = 0; const unsigned bufN = 64; char buf[ bufN + 1]; do { snprintf(buf,bufN,"#id_%i",i); if( _cssSelectorToRecd(p,buf) == nullptr ) return mem::duplStr(buf); i += 1; }while(1); return nullptr; } template< typename T > rc_t _set_attr_int( handle_t h, const char* selectorStr, const char* attrLabel, const T& value, const char* suffix ) { const int bufN = 64; char buf[ bufN + 1 ]; if( suffix == nullptr ) suffix = ""; else { if( strcmp(suffix,"rgb") == 0 ) { snprintf(buf,bufN,"#%06x",value); } else { snprintf(buf,bufN,"%i%s",value,suffix); } } return _set_attr( h, selectorStr, attrLabel, buf, nullptr ); } rc_t _write_pline(file::handle_t fH, ele_t* e, const char* styleStr) { rc_t rc = kOkRC; if( e->yN == 0 ) return rc; file::printf(fH,"yN; ++i) if((rc = file::printf(fH,"%f,%f ", e->xV[i], e->yV[i] )) != kOkRC) return rc; return file::printf(fH,"\" %s />\n", styleStr); } } } cw::rc_t cw::svg::create( handle_t& h ) { rc_t rc; if((rc = destroy(h)) != kOkRC ) return rc; svg_t* p = mem::allocZ(); _install_basic_eight_cmap(p); _install_gray_scale_cmap(p); _install_inv_gray_scale_cmap(p); _install_heat_cmap(p); h.set(p); return rc; } cw::rc_t cw::svg::destroy( handle_t& h ) { rc_t rc = kOkRC; if( !h.isValid()) return rc; svg_t* p = _handleToPtr(h); if((rc = _destroy(p)) != kOkRC ) return rc; h.clear(); return rc; } cw::rc_t cw::svg::install_color_map( handle_t h, const unsigned* colorV, unsigned colorN, unsigned id ) { return _install_cmap( _handleToPtr(h), id, mem::allocDupl(colorV,colorN), colorN ); } void cw::svg::offset( handle_t h, double dx, double dy ) { svg_t* p = _handleToPtr(h); p->dx = dx; p->dy = dy; } unsigned cw::svg::color( handle_t h, unsigned colorMapId, double colorMin, double colorMax, double colorValue ) { color_map_t* cm; svg_t* p = _handleToPtr(h); if((cm = _cmap_from_id(p, colorMapId )) == nullptr ) { cwLogWarning("Unknown color map id:%i.", colorMapId ); return 0; } double c = std::min( colorMax, std::max( colorMin, colorValue ) ); unsigned idx = (cm->colorN-1) * (c - colorMin)/(colorMax - colorMin); assert(idxcolorN); return cm->colorV[idx]; } unsigned cw::svg::color( handle_t h, unsigned colorMapId, unsigned colorIdx ) { color_map_t* cm; svg_t* p = _handleToPtr(h); if((cm = _cmap_from_id(p, colorMapId )) == nullptr ) return 0; colorIdx = colorIdx % cm->colorN; return cm->colorV[colorIdx]; } cw::rc_t cw::svg::_set_attr( handle_t h, const char* selectorStr, const char* attrLabel, const char* value, const char* suffix ) { svg_t* p = _handleToPtr(h); css_t* r = nullptr; int bufN = 64; char buf[ bufN + 1 ]; if( suffix != nullptr ) { snprintf(buf,bufN,"%s%s",value,suffix); value = buf; } // allocate and fill the CSS attribute record attr_t* a = mem::allocZ(); a->label = mem::duplStr(attrLabel); a->value = mem::duplStr(value); // if a selector is given then find or create a CSS selector record if( selectorStr != nullptr ) r = _cssFindOrCreate(p,selectorStr); else { // 'id' and 'class' attributes are always added to the ele attribute list ... if( strcmp(attrLabel,"id")!=0 && strcmp(attrLabel,"class")!=0 ) { // ... otherwise the attributes are added to the ele style list if( p->curEle->css == nullptr ) p->curEle->css = _cssCreate(p); r = p->curEle->css; } } if( r != nullptr ) { a->link = r->attrL; r->attrL = a; } else { a->link = p->curEle->attrL; p->curEle->attrL = a; } return kOkRC; } cw::rc_t cw::svg::_set_attr( handle_t h, const char* selectorStr, const char* attrLabel, const unsigned& value, const char* suffix ) { return _set_attr_int(h,selectorStr,attrLabel,value,suffix); } cw::rc_t cw::svg::_set_attr( handle_t h, const char* selectorStr, const char* attrLabel, const int& value, const char* suffix ) { return _set_attr_int(h,selectorStr,attrLabel,value,suffix); } cw::rc_t cw::svg::_set_attr( handle_t h, const char* selectorStr, const char* attrLabel, const double& value, const char* suffix ) { const int bufN = 32; char buf[ bufN+1 ]; number_to_string(value,buf,bufN); return _set_attr(h,selectorStr,attrLabel,buf,suffix); } cw::rc_t cw::svg::_rect( handle_t h, double x, double y, double ww, double hh ) { svg_t* p = _handleToPtr(h); return _insert( p, kRectTId, x, y, ww, hh, nullptr); } cw::rc_t cw::svg::_line( handle_t h, double x0, double y0, double x1, double y1 ) { svg_t* p = _handleToPtr(h); return _insert( p, kLineTId, x0, y0, x1-x0, y1-y0, nullptr); } cw::rc_t cw::svg::_pline( handle_t h, const double* yV, unsigned n, const double* xV ) { svg_t* p = _handleToPtr(h); return _insert( p, kPLineTId, 0,0,0,0, nullptr, yV, n, xV); } cw::rc_t cw::svg::_text( handle_t h, double x, double y, const char* text ) { svg_t* p = _handleToPtr(h); return _insert( p, kTextTId, x, y, 0, 0, text); } cw::rc_t cw::svg::image( handle_t h, const float* xM, unsigned rowN, unsigned colN, unsigned pixSize, unsigned cmapId ) { svg_t* p = _handleToPtr(h); char* idLabel = _cssGenUniqueIdLabel(p); _parse_attr( h, idLabel, "width", pixSize, "px", "height", pixSize, "px", "stroke-width", 1, "px", "stroke-opacity", 1.0, nullptr, "fill-opacity", 1.0, nullptr ); for(unsigned i=0; ieleL; bool standAloneFl = cwIsFlag(flags,kStandAloneFl); bool panZoomFl = cwIsFlag(flags,kPanZoomFl); bool genCssFileFl = cwIsFlag(flags,kGenCssFileFl) && cssFn!=nullptr; file::handle_t fH; char panZoomHdr[] = "\n" "\n"; char standAloneFmt[] = "\n" "\n" "\n" "\n" "%s" "\n" "%s\n" "\n" "\n"; char svgFmt[] = "\n"; char cssFmt[] = "\n"; //double max_y = _size(p, svgWidth, svgHeight ); //_flipY( p, max_y ); _size(p,svgWidth,svgHeight); _offsetEles(p, bordR, bordT ); svgWidth += bordR + bordL; svgHeight += bordT + bordB; if( p->cssL != nullptr ) { if( genCssFileFl ) _writeCssFile( p, cssFn ); else styleStr = _print_css_list(p,styleStr); } cssStr = mem::printf(cssStr,cssFn==nullptr ? "%s" : cssFmt, cssFn==nullptr ? " " : cssFn); fileHdr = mem::printf(fileHdr,standAloneFmt, cssStr, styleStr==nullptr ? "" : styleStr, panZoomFl ? panZoomHdr : ""); svgHdr = mem::printf(svgHdr,"%s%s", standAloneFl ? fileHdr : "", svgFmt); fileHdr = mem::printf(fileHdr,svgHdr,svgWidth,svgHeight); mem::release(svgHdr); mem::release(cssStr); mem::release(styleStr); if((rc = file::open(fH,outFn,file::kWriteFl)) != kOkRC ) { rc = cwLogError(rc,"SVG file create failed for '%s'.",cwStringNullGuard(outFn)); goto errLabel; } if((rc = file::printf(fH,"%s",fileHdr)) != kOkRC ) goto errLabel; for(; e!=NULL; e=e->link) { char* dStyleStr = nullptr; if( e->css != nullptr ) dStyleStr = mem::printf(dStyleStr, "style=\"%s\" ",_print_css_attr_list(e->css->attrL,dStyleStr)); if( e->attrL != nullptr ) dStyleStr = _print_ele_attr_list(e->attrL,dStyleStr); const char* styleStr = dStyleStr==nullptr ? "" : dStyleStr; switch( e->typeId ) { case kRectTId: rc = file::printf(fH,"\n",e->x0,e->y0,e->x1-e->x0,e->y1-e->y0,styleStr); break; case kPixelTId: rc = file::printf(fH,"\n",e->x0,e->y0,e->x1-e->x0,e->y1-e->y0,styleStr); break; case kLineTId: rc = file::printf(fH,"\n",e->x0,e->y0,e->x1,e->y1,styleStr); break; case kPLineTId: rc = _write_pline(fH,e,styleStr); break; case kTextTId: rc = file::printf(fH,"%s\n",e->x0,e->y0, styleStr,e->text); break; } mem::release(dStyleStr); if( rc != kOkRC ) goto errLabel; } if( (rc = file::printf(fH,"\n")) != kOkRC ) { rc = cwLogError(kMemAllocFailRC,"File suffix write failed."); goto errLabel; } if( standAloneFl ) rc = file::printf(fH,"\n\n"); errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"SVG file write failed."); file::close(fH); mem::release(fileHdr); return rc; } cw::rc_t cw::svg::test( const cw::object_t* cfg ) { rc_t rc = kOkRC; handle_t h; const char* outFn = nullptr; const char* cssFn = nullptr; if((rc = cfg->getv("outHtmlFn",outFn, "outCssFn",cssFn)) != kOkRC ) { rc = cwLogError(rc,"SVG test parameter parsing failed."); goto errLabel; } if((rc = create(h)) != kOkRC ) { cwLogError(rc,"SVG Test failed on create."); goto errLabel; } else { double yV[] = { 0, 10, 30, 60, 90 }; double xV[] = { 0, 40, 60, 40, 10 }; unsigned yN = cwCountOf(yV); install_css(h,"#my_rect","fill-opacity",0.25,nullptr); rect(h, 0, 0, 100, 100, "fill", 0x7f7f7f, "rgb", "id", "my_rect", nullptr ); line(h, 0, 0, 100, 100, "stroke", 0xff0000, "rgb" ); line(h, 0,100, 100, 0, "stroke", 0x00ff00, "rgb", "stroke-width", 3, "px", "stroke-opacity", 0.5, nullptr ); pline(h, yV, yN, xV, "stroke", 0x0, "rgb", "fill-opacity", 0.25, nullptr ); text(h, 10, 10, "foo" ); float imgM[] = { 0.0f, 0.5f, 1.0f, 0.5f, 0.0f, 0.5f, 1.0f, 1.0f, 0.0f, 0.5f, 0.0f, 1.0f }; offset( h, 10, 200 ); image(h, imgM, 4, 3, 20, kInvGrayScaleColorMapId ); write(h,outFn, cssFn, kStandAloneFl, 10,10,10,10); } errLabel: if((rc = destroy(h)) != kOkRC ) rc = cwLogError(rc,"SVG destroy failed."); return rc; }