R/svg2swf.R

Defines functions svg2swf parseSVG

Documented in svg2swf

# Currently this function could only parse svg files created by the cairo
# graphics library, typically from svg() in the grDevices package (R >= 2.14.0
# required for Windows OS), and CairoSVG() in the Cairo package.
parseSVG = function(file.name)
{
    xmlParse = XML::xmlParse;
    newXMLNamespace = XML::newXMLNamespace;
    xmlRoot = XML::xmlRoot;
    getNodeSet = XML::getNodeSet;
    xmlAttrs = XML::xmlAttrs;
    xmlParent = XML::xmlParent;
    xmlName = XML::xmlName;
    xmlChildren = XML::xmlChildren;
    xmlSApply = XML::xmlSApply;
    
    svgFile = xmlParse(file.name);
    # Don't forget the name space!
    newXMLNamespace(xmlRoot(svgFile), "http://www.w3.org/2000/svg", "svg");
    # Find the first <g> child of <svg>
    pathRoot = getNodeSet(svgFile, "/svg:svg/svg:g");
    if(!length(pathRoot)) stop(sprintf("Failed in parsing file '%s'", file.name));
    pathRoot = pathRoot[[1]];
    
    # Default style for a <path> node
    defaultStyle = c("stroke" = "none",
                     "stroke-width" = "1",
                     "stroke-linecap" = "butt",
                     "stroke-linejoin" = "miter",
                     "stroke-miterlimit" = "4",
                     "stroke-opacity" = "1",
                     "fill" = "rgb(0%,0%,0%)",
                     "fill-rule" = "nonzero",
                     "fill-opacity" = "1");
    
    # Handle <path> style in named vector
    parseStyle = function(style)
    {
        if(is.null(style)) return(NULL);
        s = unlist(strsplit(style, ";"));
        val = strsplit(s, ":");
        result = sapply(val, function(x) x[2]);
        names(result) = sapply(val, function(x) x[1]);
        return(result);
    }
    # Update the attributes in "old" style with the values in "new"
    # "old" must contain "new"
    updateStyle = function(old, new)
    {
        if(is.null(new)) return(old);
        result = old;
        result[names(new)] = new;
        return(result);
    }
    # Iteratively update the style from parent nodes
    updateStyleUpward = function(node)
    {
        style = xmlAttrs(node)["style"];
        if(is.na(style)) style = NULL;
        style = parseStyle(style);
        style = updateStyle(defaultStyle, style);
        parentNode = xmlParent(node);
        # Recursively search the parent
        while(!is.null(parentNode))
        {
            parentStyle = xmlAttrs(parentNode)["style"];
            if(is.null(parentStyle) || is.na(parentStyle)) parentStyle = NULL;
            parentStyle = parseStyle(parentStyle);
            style = updateStyle(style, parentStyle);
            parentNode = xmlParent(parentNode);
        }
        return(style);
    }
    # Parse <path> and <use> nodes into structured lists
    #
    # <path style="" d="">   =====>   style=..., d=..., x=0, y=0
    #
    # <use xlink:href="#glyph0-0" x="63.046875" y="385.921875"/>
    # =====>
    # style=..., d=..., x=63.046875, y=385.921875
    #
    parseNode = function(node)
    {
        if(xmlName(node) == "use")
        {
            attrs = xmlAttrs(node);
            refID = sub("#", "", attrs["href"]);
            refPathNode = getNodeSet(svgFile, sprintf("//*[@id='%s']/svg:path", refID))[[1]];
            style = updateStyleUpward(refPathNode);
            style = updateStyle(style, updateStyleUpward(node));
            d = xmlAttrs(refPathNode)["d"];
            x = xmlAttrs(node)["x"];
            y = xmlAttrs(node)["y"];
        } else if(xmlName(node) == "path") {
            style = updateStyleUpward(node);
            d = xmlAttrs(node)["d"];
            x = y = 0;
        } else return(NULL);
        xy = as.numeric(c(x, y));
        names(d) = NULL;
        names(xy) = NULL;
        return(list(style = style, d = d, xy = xy));
    }
    # Flatten nodes
    # <g>
    #   <use />
    #   <use />
    #   <use />
    # </g>
    #
    # =====>
    #
    # <use />
    # <use />
    # <use />
    expandNode = function(node)
    {
        children = xmlChildren(node);
        res = if(!length(children)) node else children;
        return(res);
    }
    nodes = unlist(xmlSApply(pathRoot, expandNode));
    names(nodes) = NULL;
    paths = lapply(nodes, parseNode);
    path.is.null = sapply(paths, is.null);
    paths[path.is.null] = NULL;
    if(!length(paths)) stop("Unknown child node of '/svg/g'");
    return(paths);
}


#' Convert a sequence of SVG files to SWF file
#'
#' Given the file names of a sequence of SVG files, this function could
#' convert them into a Flash file (.swf).
#'
#' This function uses the XML package in R and a subset of librsvg
#' (\url{https://wiki.gnome.org/action/show/Projects/LibRsvg})
#' to parse the SVG file, and
#' uses the Ming library (\url{https://github.com/libming/libming}) to
#' implement the conversion. Currently this function supports SVG files
#' created by \code{\link[grDevices]{svg}()} in the \pkg{grDevices}
#' package, and \code{\link[Cairo]{CairoSVG}()} in the
#' \pkg{Cairo} package.
#' @param input the file names of the SVG files to be converted
#' @param output the name of the output SWF file
#' @param bgColor background color of the output SWF file
#' @param interval the time interval (in seconds) between animation frames
#' @return The name of the generated SWF file if successful.
#' @export
#' @author Yixuan Qiu <\url{https://statr.me}>
#' @examples \dontrun{
#' if(capabilities("cairo")) {
#'   olddir = setwd(tempdir())
#'   svg("Rplot%03d.svg", onefile = FALSE)
#'   set.seed(123)
#'   x = rnorm(5)
#'   y = rnorm(5)
#'   for(i in 1:100) {
#'       plot(x <- x + 0.1 * rnorm(5), y <- y + 0.1 * rnorm(5),
#'            xlim = c(-3, 3), ylim = c(-3, 3), col = "steelblue",
#'            pch = 16, cex = 2, xlab = "x", ylab = "y")
#'   }
#'   dev.off()
#'   output = svg2swf(sprintf("Rplot%03d.svg", 1:100), interval = 0.1)
#'   swf2html(output)
#'   setwd(olddir)
#' }
#' }
#'
svg2swf = function(input, output = "movie.swf", bgColor = "white", interval = 1)
{
    # Use XML package
    # if(!require(XML))
    #     stop("svg2swf() requires XML package");
    
    xmlParse = XML::xmlParse;
    xmlAttrs = XML::xmlAttrs;
    xmlRoot = XML::xmlRoot;
    
    if(!is.character(input))
        stop("'input' must be a character vector naming the input SVG files");
    
    bg = col2rgb(bgColor, alpha = FALSE);
    bg = as.integer(bg);
    
    if(!all(file.exists(input))) stop("one or more input files do not exist");
    
    filesData = lapply(input, parseSVG);
    firstFile = xmlParse(input[1]);
    size = xmlAttrs(xmlRoot(firstFile))["viewBox"];
    size = as.numeric(unlist(strsplit(size, " ")));
    
    outfile = normalizePath(output, mustWork = FALSE);
    .Call(svg2swf_c, filesData, outfile, size, bg, as.numeric(interval));
    
    message("SWF file created at ", outfile);
    invisible(output);
}
yixuan/R2SWF documentation built on March 6, 2024, 9:20 a.m.