diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java index daea6bc6818c09f22a3cffa04a728973c4ade270..1f297581c409dd3eb1c7e4bf0c1f3cf691fefcd5 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java @@ -25,6 +25,7 @@ package com.oracle.truffle.r.library.fastrGrid; import com.oracle.truffle.r.library.fastrGrid.DisplayList.LGetDisplayListElement; import com.oracle.truffle.r.library.fastrGrid.DisplayList.LInitDisplayList; import com.oracle.truffle.r.library.fastrGrid.DisplayList.LSetDisplayListOn; +import com.oracle.truffle.r.library.fastrGrid.grDevices.DevCairo; import com.oracle.truffle.r.library.fastrGrid.grDevices.DevCurr; import com.oracle.truffle.r.library.fastrGrid.grDevices.DevHoldFlush; import com.oracle.truffle.r.library.fastrGrid.grDevices.DevOff; @@ -59,6 +60,8 @@ public final class FastRGridExternalLookup { return DevOff.create(); case "PDF": return new IgnoredGridExternal(RNull.instance); + case "devCairo": + return new DevCairo(); default: return null; } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java index f00b510ec41d04dc1eb86bce7076a6c0e8cb5ee8..10f8a4d797edbf09fda291719df551e85554a539 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java @@ -100,6 +100,10 @@ public final class GridContext { public void closeDevice(int which) throws DeviceCloseException { assert which >= 0 && which < devices.size(); devices.get(which).device.close(); + removeDevice(which); + } + + public void removeDevice(int which) { RGridGraphicsAdapter.removeDevice(which); devices.remove(which); if (currentDeviceIdx >= which) { @@ -107,6 +111,10 @@ public final class GridContext { } } + public GridDevice getDevice(int index) { + return devices.get(index).device; + } + /** * Runs arbitrary function from 'fastrGrid.R' file and returns its result. */ diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/DrawingContext.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/DrawingContext.java index 2986d9ffba8f4133eafbfc4dc56672905d80965f..dcfb747b4f7be4595bc795e2b31b08c1fd74d353 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/DrawingContext.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/DrawingContext.java @@ -53,6 +53,14 @@ public interface DrawingContext { assert num > 0 && num <= SYMBOL.ordinal() + 1; return values()[num - 1]; } + + public boolean isBold() { + return this == BOLD || this == BOLDITALIC; + } + + public boolean isItalic() { + return this == ITALIC || this == BOLDITALIC; + } } enum GridLineJoin { @@ -132,7 +140,8 @@ public interface DrawingContext { String getFontFamily(); /** - * Gets the height of a line in multiplies of the base line height. + * Gets the height of a text line in multiplies of the base line height. This is typically not a + * concern of devices, since they always receive single line strings for drawing. */ double getLineHeight(); @@ -140,4 +149,18 @@ public interface DrawingContext { * The fill color of shapes. */ GridColor getFillColor(); + + static boolean areSame(DrawingContext ctx1, DrawingContext ctx2) { + return ctx1 == ctx2 || (ctx1.getColor().equals(ctx2.getColor()) && + ctx1.getLineEnd() == ctx2.getLineEnd() && + ctx1.getLineJoin() == ctx2.getLineJoin() && + ctx1.getLineType() == ctx2.getLineType() && + ctx1.getLineHeight() == ctx2.getLineHeight() && + ctx1.getFontStyle() == ctx2.getFontStyle() && + ctx1.getFontSize() == ctx2.getFontSize() && + ctx1.getFontFamily().equals(ctx2.getFontFamily()) && + ctx1.getLineWidth() == ctx2.getLineWidth() && + ctx1.getLineMitre() == ctx2.getLineMitre() && + ctx1.getFillColor().equals(ctx2.getFillColor())); + } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java index 2ea6e4468e66e623d119819c643716f1ae1541a8..0515ded9b5bf94c49d13a8092c7063d0fbaee6bf 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java @@ -81,7 +81,7 @@ public interface GridDevice { void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius); /** - * Prints a string with left bottom corner at given position rotates by given angle anti clock + * Prints a string with left bottom corner at given position rotated by given angle anti clock * wise, the centre of the rotation should be the bottom left corer. */ void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text); diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/SVGDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/SVGDevice.java new file mode 100644 index 0000000000000000000000000000000000000000..702a586e3c65000cf14534ba168f8908c0064c73 --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/SVGDevice.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device; + +import static com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GRID_LINE_BLANK; +import static com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.INCH_TO_POINTS_FACTOR; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.util.Collections; + +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridFontStyle; +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridLineEnd; +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridLineJoin; +import com.oracle.truffle.r.runtime.RInternalError; + +public class SVGDevice implements GridDevice { + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.000"); + private final StringBuilder data = new StringBuilder(1024); + private final String filename; + private final double width; + private final double height; + + private DrawingContext cachedCtx; + + public SVGDevice(String filename, double width, double height) { + this.filename = filename; + this.width = width; + this.height = height; + } + + public String getContents() { + return data.toString(); + } + + @Override + public void openNewPage() { + // We stay compatible with GnuR: opening new page wipes out what has been drawn without + // saving it anywhere. + data.setLength(0); + cachedCtx = null; + append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"); + // we could use real inches, but that makes the output different to GnuR and other formats + // (jpg, ...), which use conversion 70px ~ 1in + append("<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='%.3fpx' height='%.3fpx' viewBox='0 0 %.3f %.3f'>", width * 70d, height * 70d, + width, + height); + } + + @Override + public void close() throws DeviceCloseException { + if (cachedCtx != null) { + // see #appendStyle + append("</g>"); + } + append("</svg>"); + try { + Files.write(Paths.get(filename), Collections.singleton(data.toString()), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new DeviceCloseException(e); + } + } + + @Override + public void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) { + appendStyle(ctx); + append("<rect vector-effect='non-scaling-stroke' x='%.3f' y='%.3f' width='%.3f' height='%.3f'", leftX, transY(bottomY + height), width, height); + if (rotationAnticlockWise != 0) { + append("transform='rotate(%.3f %.3f,%.3f)'", toDegrees(rotationAnticlockWise), (leftX + width / 2.), transY(bottomY + height / 2.)); + } + data.append("/>"); // end of 'rect' tag + } + + @Override + public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + drawPoly(ctx, x, y, startIndex, length, "style='fill:transparent'"); + } + + @Override + public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + drawPoly(ctx, x, y, startIndex, length, ""); + } + + @Override + public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { + appendStyle(ctx); + append("<circle vector-effect='non-scaling-stroke' cx='%.3f' cy='%.3f' r='%.3f'/>", centerX, transY(centerY), radius); + } + + @Override + public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { + appendStyle(ctx); + append("<text x='%.3f' y='%.3f' textLength='%.3f' lengthAdjust='spacingAndGlyphs' ", leftX, transY(bottomY), getStringWidth(ctx, text)); + // SVG interprets the "fill" as the color of the text + data.append("style='").append(getStyleColor("fill", ctx.getColor())).append(";stroke:transparent'"); + if (rotationAnticlockWise != 0) { + append(" transform='rotate(%.3f %.3f,%.3f)'", toDegrees(rotationAnticlockWise), leftX, transY(bottomY)); + } + data.append(">").append(text).append("</text>"); + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public double getStringWidth(DrawingContext ctx, String text) { + // The architecture of the GridDevice and grid package requires the devices be able to + // calculate the width of given string, this way one can e.g. create a box around text. SVG + // supports this by means of "textLength" attribute, which allows us to force text to have + // specified width. So we approximate the width of given text in the calculation below and + // then force the text to actually have such width if it ever gets displayed by #drawString. + double factor = 0.5; // empirically chosen + if (ctx.getFontStyle() == GridFontStyle.BOLD || ctx.getFontStyle() == GridFontStyle.BOLDITALIC) { + factor = 0.62; + } + double letterWidth = (ctx.getFontSize() / INCH_TO_POINTS_FACTOR); + double result = factor * (double) text.length() * letterWidth; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == 'w' || c == 'm') { + result += letterWidth * 0.2; + } else if (c == 'z') { + result += letterWidth * 0.1; + } + } + return result; + } + + @Override + public double getStringHeight(DrawingContext ctx, String text) { + // we need height without ascent/descent of letters that are not in the string, this is + // empirically tested calculation + return 0.8 * (ctx.getFontSize() / INCH_TO_POINTS_FACTOR); + } + + private void drawPoly(DrawingContext ctx, double[] x, double[] y, int startIndex, int length, String attributes) { + appendStyle(ctx); + data.append("<polyline vector-effect='non-scaling-stroke' points='"); + for (int i = 0; i < length; i++) { + data.append(DECIMAL_FORMAT.format(x[i + startIndex])); + data.append(','); + data.append(DECIMAL_FORMAT.format(transY(y[i + startIndex]))); + data.append(' '); + } + data.append("' ").append(attributes).append(" />"); + } + + private void appendStyle(DrawingContext ctx) { + if (cachedCtx == null || !DrawingContext.areSame(cachedCtx, ctx)) { + if (cachedCtx != null) { + append("</g>"); // close the previous style definition + } + append("<g style='"); + appendStyleUncached(ctx); + append("'>"); + } + cachedCtx = ctx; + } + + private void appendStyleUncached(DrawingContext ctx) { + byte[] lineType = ctx.getLineType(); + if (lineType == GRID_LINE_BLANK) { + append("stroke:transparent"); + } else { + append(getStyleColor("stroke", ctx.getColor())); + } + data.append(';').append(getStyleColor("fill", ctx.getFillColor())); + data.append(";stroke-width:").append(ctx.getLineWidth()); + if (lineType != DrawingContext.GRID_LINE_SOLID && lineType != DrawingContext.GRID_LINE_BLANK) { + data.append(";stroke-dasharray:"); + for (int i = 0; i < lineType.length; i++) { + data.append(lineType[i]); + if (i != lineType.length - 1) { + data.append(','); + } + } + } + data.append(";stroke-linejoin:").append(getSVGLineJoin(ctx.getLineJoin())); + data.append(";stroke-linecap:").append(getSVGLineCap(ctx.getLineEnd())); + if (ctx.getLineJoin() == GridLineJoin.MITRE) { + data.append(";stroke-miterlimit:").append(ctx.getLineMitre()); + } + data.append(";font-size:").append(ctx.getFontSize() / INCH_TO_POINTS_FACTOR).append("px"); + if (!ctx.getFontFamily().isEmpty()) { + // Font-family strings 'mono', 'sans', and 'serif' are OK for us + data.append(";font-family:").append(ctx.getFontFamily()); + } + if (ctx.getFontStyle().isBold()) { + data.append(";font-weight:bold"); + } + if (ctx.getFontStyle().isItalic()) { + data.append(";font-style:italic"); + } + } + + private static String getSVGLineCap(GridLineEnd lineEnd) { + switch (lineEnd) { + case ROUND: + return "round"; + case BUTT: + return "butt"; + case SQUARE: + return "square"; + default: + throw RInternalError.shouldNotReachHere("Unexpected value of GridLineEnd enum."); + } + } + + private static String getSVGLineJoin(GridLineJoin lineJoin) { + switch (lineJoin) { + case ROUND: + return "round"; + case MITRE: + return "miter"; + case BEVEL: + return "bevel"; + default: + throw RInternalError.shouldNotReachHere("Unexpected value of GridLineJoin enum."); + } + } + + private static String getStyleColor(String prefix, GridColor color) { + return String.format("%s:rgb(%d,%d,%d);%s-opacity:%.3f", prefix, color.getRed(), color.getGreen(), color.getBlue(), prefix, (double) color.getAlpha() / 255d); + } + + private void append(String fmt, Object... args) { + data.append(String.format(fmt + "\n", args)); + } + + private double transY(double y) { + return (height - y); + } + + private static double toDegrees(double rotationAnticlockWise) { + return (180. / Math.PI) * -rotationAnticlockWise; + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCairo.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCairo.java new file mode 100644 index 0000000000000000000000000000000000000000..6ae4427041d6d613254cd87fdb1f0ba971e8d5ad --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCairo.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.grDevices; + +import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.device.SVGDevice; +import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; +import com.oracle.truffle.r.runtime.RError.Message; +import com.oracle.truffle.r.runtime.RRuntime; +import com.oracle.truffle.r.runtime.data.RArgsValuesAndNames; +import com.oracle.truffle.r.runtime.data.RNull; + +public class DevCairo extends RExternalBuiltinNode { + static { + Casts.noCasts(DevCairo.class); + } + + @Override + protected Object call(RArgsValuesAndNames args) { + if (args.getLength() < 4) { + throw error(Message.ARGUMENTS_REQUIRED_COUNT, args.getLength(), "devCairo", 4); + } + + String filename = RRuntime.asString(args.getArgument(0)); + int witdh = RRuntime.asInteger(args.getArgument(2)); + int height = RRuntime.asInteger(args.getArgument(2)); + if (RRuntime.isNA(witdh) || RRuntime.isNA(height) || RRuntime.isNA(filename) || filename.isEmpty()) { + throw error(Message.INVALID_ARG_TYPE); + } + + GridContext.getContext().setCurrentDevice("svg", new SVGDevice(filename, (double) witdh / 72., (double) height / 72.)); + return RNull.instance; + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java index e0d57b606a07af5bb4a1a457f0c0a90d5d50dffe..87f99d3561440d6dfad00f35ca162e63d7daa7b0 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java @@ -25,7 +25,9 @@ package com.oracle.truffle.r.library.fastrGrid.grDevices; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException; +import com.oracle.truffle.r.library.fastrGrid.device.SVGDevice; import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; import com.oracle.truffle.r.runtime.RError; import com.oracle.truffle.r.runtime.RError.Message; @@ -45,10 +47,17 @@ public abstract class DevOff extends RExternalBuiltinNode.Arg1 { @TruffleBoundary public Object devOff(int whichR) { GridContext ctx = GridContext.getContext(); - int which = whichR - 1; // convert to Java index + int which = Math.abs(whichR) - 1; // convert to Java index if (which < 0 || which >= ctx.getDevicesSize()) { throw RError.error(RError.NO_CALLER, Message.GENERIC, "Wrong device number."); } + + // FastR specific special handling for SVG device, when the index is negative, return the + // SVG code + if (whichR < 0) { + return closeSvgDevice(ctx, which); + } + try { ctx.closeDevice(which); } catch (DeviceCloseException e) { @@ -56,4 +65,15 @@ public abstract class DevOff extends RExternalBuiltinNode.Arg1 { } return RNull.instance; } + + private String closeSvgDevice(GridContext ctx, int which) { + GridDevice dev = ctx.getDevice(which); + ctx.removeDevice(which); + if ((dev instanceof SVGDevice)) { + return ((SVGDevice) dev).getContents(); + } else { + warning(Message.GENERIC, "The device was not SVG device."); + return ""; + } + } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R index 2e551b4a1a6ea52155d5066ce195421440c700ed..aad9c69ae671c971c18b81e89920be6f54cc66b5 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R @@ -27,6 +27,14 @@ eval(expression({ awt <- function(width = NULL, height = NULL, graphicsObj = NULL) { .External2(grDevices:::C_X11, ".FASTR.AWT", width, height, graphicsObj) } + # Allows to get the SVG code from SVG device, it also closes the device, + # but the contents are not saved to the given file. + svg.off <- function(which = dev.cur()) { + if (which == 1) { + stop("cannot shut down device 1 (the null device)") + } + .External(C_devoff, as.integer(-which)) + } # GnuR version only works with "X11cairo" device. Our version of savePlot # works with "awt" device and "X11cairo", which is for us only alias for # "awt". Moreover, we only support formats that awt supports.