blob: 52f7abab882a61ed928fe6e37e960eaf8cc6e56c [file] [log] [blame]
/*
* R : A Computer Language for Statistical Data Analysis
* Copyright (C) 2007-11 The R Foundation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, a copy is available at
* https://www.R-project.org/Licenses/
*
* Modular Quartz device for macOS
*
* Partially based on code by Byron Ellis
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#if HAVE_AQUA
#include <Defn.h>
#include <Rinternals.h>
#define R_USE_PROTOTYPES 1
#include <R_ext/GraphicsEngine.h>
/* This sets ptr_QuartzBackend as a symbol in this file */
#define IN_AQUA_C 1
#include <R_ext/QuartzDevice.h>
#include "grDevices.h"
#include <CoreFoundation/CoreFoundation.h>
#define DEVQUARTZ_VERSION 1 /* first public Quartz API version */
#define QBE_NATIVE 1 /* either Cocoa or Carbon depending on the macOS version */
#define QBE_COCOA 2 /* internal Cocoa */
#define QBE_CARBON 3 /* internal Carbon */
#define QBE_BITMAP 4 /* bitmap file creating */
#define QBE_PDF 5 /* PDF file creating */
typedef struct moduleTypes_s {
const char *type;
const char *subst;
int qbe; /* Quartz back-end */
} quartz_module_t;
/* list of internally supported output modules */
const quartz_module_t quartz_modules[] = {
{ "", 0, QBE_NATIVE },
{ "native", 0, QBE_NATIVE },
{ "cocoa", 0, QBE_COCOA },
{ "carbon", 0, QBE_CARBON },
{ "pdf", 0, QBE_PDF },
{ "png", "public.png", QBE_BITMAP },
{ "jpeg", "public.jpeg", QBE_BITMAP },
{ "jpg", "public.jpeg", QBE_BITMAP },
{ "jpeg2000","public.jpeg-2000", QBE_BITMAP },
{ "tiff", "public.tiff", QBE_BITMAP },
{ "tif", "public.tiff", QBE_BITMAP },
{ "gif", "com.compuserve.gif", QBE_BITMAP },
{ "psd", "com.adobe.photoshop-image", QBE_BITMAP },
{ "bmp", "com.microsoft.bmp", QBE_BITMAP },
{ "sgi", "com.sgi.sgi-image", QBE_BITMAP },
{ "pict", "com.apple.pict", QBE_BITMAP },
{ 0, 0, 0} };
/* for compatibility with macOS <10.5 */
#ifndef CGFLOAT_DEFINED
typedef float CGFloat;
#define CGFLOAT_MIN FLT_MIN
#define CGFLOAT_MAX FLT_MAX
#define CGFLOAT_IS_DOUBLE 0
#define CGFLOAT_DEFINED 1
#endif
typedef struct QuartzSpecific_s {
double ps;
double scalex, scaley; /* resolution correction: px/pt ratio */
double width,height; /* size (in inches) */
double tscale; /* text scale (resolution independent,
i.e. it constitutes a text zoom factor */
int dirty; /* dirtly flag. Not acted upon by the Quartz
core, but QC sets it whenever a drawing
operation is performed (see detailed
description in R_ext/QuartzDevice.h) */
int gstate; /* gstate counter */
int async; /* asynchronous drawing (i.e. context was
not ready for an operation) */
int bg; /* background color */
int canvas; /* background color */
int antialias,smooth;/* smoothing flags (only aa makes any sense) */
int flags; /* additional QDFLAGs */
int holdlevel; /* hold level */
int redraw; /* redraw flag is set when replaying
and inhibits syncs on Mode */
CGRect clipRect; /* clipping rectangle */
pDevDesc dev; /* device structure holding this one */
CGFontRef font; /* currently used font */
void* userInfo; /* pointer to a module-dependent space */
/* callbacks - except for getCGContext all others are optional */
CGContextRef (*getCGContext)(QuartzDesc_t dev, void *userInfo);
int (*locatePoint)(QuartzDesc_t dev, void *userInfo, double *x, double *y);
void (*close)(QuartzDesc_t dev, void *userInfo);
void (*newPage)(QuartzDesc_t dev, void *userInfo, int flags);
void (*state)(QuartzDesc_t dev, void *userInfo, int state);
void* (*par)(QuartzDesc_t dev, void *userInfo, int set, const char *key, void *value);
void (*sync)(QuartzDesc_t dev, void *userInfo);
void* (*cap)(QuartzDesc_t dev, void*userInfo);
} QuartzDesc;
/* coordinates:
- R graphics (positions etc., usually points)
- real size (e.g. inches)
- display view (usually pixels)
bookkeeping:
- QuartzDevice.width/height: inches
- R GE size (.._Size): points
- physical (on-screen) coordinates : pixels
the current implementation uses points as plotting units (i.e. this is what
Quartz tells R), but the canvas is specified in pixels. The scalex/y factors
specify the conversion factor between pixels and points.
We are *not* using R's scaling facilities, because R doesn't work with
non-square pixels (e.g. circles become ellipses).
FIXME: yes it does -- ipr is a two-element array.
-- not entirely, because it uses text (e.g. "o") as symbols which is rendered
in 1:1 aspect ratio and thus is squished on displays with non-square pixels
(That being a bug in Quartz, then!)
Actually, dp not points are used.
*/
#pragma mark QuartzDevice API (for modules)
/* Update should be called when ps or tscale change.
Conservatively, it should be called on scale change, too, in case
we decide to abandon the CTM approach */
static void QuartzDevice_Update(QuartzDesc_t desc);
/* this function must be called after a new context is created.
it primes the context for drawing */
void QuartzDevice_ResetContext(QuartzDesc_t desc) {
QuartzDesc *qd = ((QuartzDesc*) desc);
qd->gstate = 0;
qd->dirty = 0;
if (qd->getCGContext) {
CGContextRef ctx = qd->getCGContext(qd, qd->userInfo);
if (ctx) {
CGContextSetAllowsAntialiasing(ctx, qd->antialias);
CGContextSetShouldSmoothFonts(ctx, qd->smooth);
CGContextScaleCTM(ctx, qd->scalex, qd->scaley);
CGContextSaveGState(ctx);
qd->gstate = 1;
}
}
}
/* Uses (e.g. in window title) seems to assume this is 1-based */
int QuartzDevice_DevNumber(QuartzDesc_t desc) {
return 1 + ndevNumber((((QuartzDesc*) desc)->dev));
}
double QuartzDevice_GetWidth(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->width; }
double QuartzDevice_GetHeight(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->height; }
void QuartzDevice_SetSize(QuartzDesc_t desc, double width, double height)
{
QuartzDesc *qd = ((QuartzDesc*) desc);
qd->width = width;
qd->height = height;
qd->dev->right = width*72.0;
qd->dev->bottom = height*72.0;
}
double QuartzDevice_GetScaledWidth(QuartzDesc_t desc) { QuartzDesc *qd=((QuartzDesc*) desc); return qd->scalex*qd->width*72.0; }
double QuartzDevice_GetScaledHeight(QuartzDesc_t desc) { QuartzDesc *qd=((QuartzDesc*) desc); return qd->scaley*qd->height*72.0; }
void QuartzDevice_SetScaledSize(QuartzDesc_t desc, double width, double height) {
QuartzDesc *qd=((QuartzDesc*) desc);
QuartzDevice_SetSize(desc, width/qd->scalex/72.0, height/qd->scaley/72.0);
}
double QuartzDevice_GetXScale(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->scalex; }
double QuartzDevice_GetYScale(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->scaley; }
void QuartzDevice_SetScale(QuartzDesc_t desc, double scalex, double scaley) {
((QuartzDesc*) desc)->scalex = scalex;
((QuartzDesc*) desc)->scaley = scaley;
QuartzDevice_Update(desc);
}
double QuartzDevice_GetTextScale(QuartzDesc_t desc) {
return ((QuartzDesc*) desc)->tscale;
}
void QuartzDevice_SetTextScale(QuartzDesc_t desc, double scale) {
((QuartzDesc*) desc)->tscale = scale;
QuartzDevice_Update(desc);
}
double QuartzDevice_GetPointSize(QuartzDesc_t desc) {
return ((QuartzDesc*) desc)->ps;
}
void QuartzDevice_SetPointSize(QuartzDesc_t desc, double ps) {
((QuartzDesc*) desc)->ps = ps;
QuartzDevice_Update(desc);
}
int QuartzDevice_GetDirty(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->dirty; }
void QuartzDevice_SetDirty(QuartzDesc_t desc,int dirty) { ((QuartzDesc*) desc)->dirty = dirty; }
int QuartzDevice_GetAntialias(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->antialias; }
void QuartzDevice_SetAntialias(QuartzDesc_t desc,int aa) {
QuartzDesc *qd = (QuartzDesc*) desc;
qd->antialias = aa;
if(NULL != qd->getCGContext)
CGContextSetAllowsAntialiasing( qd->getCGContext(qd, qd->userInfo), aa );
}
void QuartzDevice_Kill(QuartzDesc_t desc) {
pGEDevDesc dd = GEgetDevice(ndevNumber(((QuartzDesc*) desc)->dev));
if (dd) GEkillDevice(dd);
}
int QuartzDesc_GetFontSmooth(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->smooth; }
void QuartzDesc_SetFontSmooth(QuartzDesc_t desc, int fs) {
QuartzDesc *qd = (QuartzDesc*) desc;
qd->smooth = fs;
if(qd->getCGContext)
CGContextSetShouldSmoothFonts( qd->getCGContext(qd, qd->userInfo), fs);
}
int QuartzDevice_GetBackground(QuartzDesc_t desc) { return ((QuartzDesc*) desc)->bg; }
static void QuartzDevice_Update(QuartzDesc_t desc)
{
QuartzDesc *qd = (QuartzDesc*) desc;
pDevDesc dev= qd->dev;
/* pre-scaling happens in Quartz (using CTM), so scales should not be
reflected in R measurements. We tell R to use 72dpi which corresponds
to plotting in pt coordinates */
dev->cra[0] = 0.9*qd->ps*qd->tscale;
dev->cra[1] = 1.2*qd->ps*qd->tscale;
dev->ipr[0] = 1.0/72.0;
dev->ipr[1] = 1.0/72.0;
}
void QuartzDevice_Activate(QuartzDesc_t desc)
{
QuartzDesc *qd = (QuartzDesc*) desc;
if (qd) {
int n = ndevNumber(qd->dev);
selectDevice(n);
}
}
void QuartzDevice_ReplayDisplayList(QuartzDesc_t desc)
{
QuartzDesc *qd = (QuartzDesc*) desc;
int _dirty = qd->dirty;
pGEDevDesc gdd = desc2GEDesc(qd->dev);
qd->redraw = 1;
/* CHECK this */
if(gdd->displayList != R_NilValue) GEplayDisplayList(gdd);
qd->redraw = 0;
qd->dirty = _dirty; /* we do NOT change the dirty flag */
}
void* QuartzDevice_GetSnapshot(QuartzDesc_t desc, int last)
{
QuartzDesc *qd = (QuartzDesc*) desc;
pGEDevDesc gd = GEgetDevice(ndevNumber(qd->dev));
SEXP snap;
if (last)
snap = desc2GEDesc(qd->dev)->savedSnapshot;
else
snap = GEcreateSnapshot(gd);
if (R_NilValue == VECTOR_ELT(snap, 0))
snap = 0;
return (snap == R_NilValue) ? 0 : snap;
}
void QuartzDevice_RestoreSnapshot(QuartzDesc_t desc, void* snap)
{
QuartzDesc *qd = (QuartzDesc*) desc;
pGEDevDesc gd = GEgetDevice(ndevNumber(qd->dev));
if(NULL == snap) return; /*Aw, hell no!*/
PROTECT((SEXP)snap);
if(R_NilValue == VECTOR_ELT(snap,0))
warning("Tried to restore an empty snapshot?");
qd->redraw = 1;
GEplaySnapshot((SEXP)snap, gd);
qd->redraw = 0;
qd->dirty = 0; /* we reset the dirty flag */
UNPROTECT(1);
}
static int quartz_embedding = 0;
static void* QuartzDevice_SetParameter(QuartzDesc_t desc, const char *key, void *value)
{
if (desc) { /* backend-specific? pass it on */
QuartzDesc *qd = (QuartzDesc*) desc;
return (qd->par) ? qd->par(qd, qd->userInfo, 1, key, value) : NULL;
} else { /* global? try to handle it */
if (key) {
if (!streql(key, QuartzParam_EmbeddingFlags)) {
if (value) quartz_embedding = ((int*)value)[0];
return &quartz_embedding;
}
}
}
return NULL;
}
void setup_RdotApp(void)
{
int eflags = QP_Flags_CFLoop | QP_Flags_Cocoa | QP_Flags_Front;
QuartzDevice_SetParameter(NULL, QuartzParam_EmbeddingFlags, &eflags);
}
static void* QuartzDevice_GetParameter(QuartzDesc_t desc, const char *key)
{
if (desc) { /* backend-specific? pass it on */
QuartzDesc *qd = (QuartzDesc*) desc;
return (qd->par) ? qd->par(qd, qd->userInfo, 0, key, NULL) : NULL;
} else { /* global? try to handle it */
if (key) {
if (!streql(key, QuartzParam_EmbeddingFlags)) return &quartz_embedding;
}
}
return NULL;
}
#pragma mark RGD API Function Prototypes
static void RQuartz_Close(pDevDesc);
static void RQuartz_Activate(pDevDesc);
static void RQuartz_Deactivate(pDevDesc);
static void RQuartz_Size(double*, double*, double*, double*, pDevDesc);
static void RQuartz_NewPage(const pGEcontext, pDevDesc);
static int RQuartz_HoldFlush(pDevDesc, int);
static void RQuartz_Clip(double, double, double, double, pDevDesc);
static double RQuartz_StrWidth(const char*, const pGEcontext, pDevDesc);
static void RQuartz_Text(double, double, const char*, double, double, const pGEcontext, pDevDesc);
static void RQuartz_Rect(double, double, double, double, const pGEcontext, pDevDesc);
static void RQuartz_Raster(unsigned int *raster, int w, int h,
double x, double y, double width, double height,
double rot, Rboolean interpolate,
const pGEcontext gc, pDevDesc dd);
static SEXP RQuartz_Cap(pDevDesc dd);
static void RQuartz_Circle(double, double, double, const pGEcontext, pDevDesc);
static void RQuartz_Line(double, double, double, double, const pGEcontext, pDevDesc);
static void RQuartz_Polyline(int, double*, double*, const pGEcontext, pDevDesc);
static void RQuartz_Polygon(int, double*, double*, const pGEcontext, pDevDesc);
static void RQuartz_Path(double*, double*, int, int*, Rboolean, const pGEcontext, pDevDesc);
static Rboolean RQuartz_Locator(double*, double*, pDevDesc);
static void RQuartz_Mode(int mode, pDevDesc);
static void RQuartz_MetricInfo(int, const pGEcontext , double*, double*, double*, pDevDesc);
#pragma mark Quartz device implementation
void* QuartzDevice_Create(void *_dev, QuartzBackend_t *def)
{
pDevDesc dev = _dev;
dev->startfill = def->bg;
dev->startcol = R_RGB(0, 0, 0);
dev->startps = def->pointsize;
dev->startfont = 1;
dev->startlty = LTY_SOLID;
dev->startgamma= 1;
/* Set up some happy pointers */
dev->close = RQuartz_Close;
dev->activate = RQuartz_Activate;
dev->deactivate = RQuartz_Deactivate;
dev->size = RQuartz_Size;
dev->newPage = RQuartz_NewPage;
dev->clip = RQuartz_Clip;
dev->strWidth = RQuartz_StrWidth;
dev->text = RQuartz_Text;
dev->rect = RQuartz_Rect;
dev->raster = RQuartz_Raster;
dev->cap = RQuartz_Cap;
dev->circle = RQuartz_Circle;
dev->line = RQuartz_Line;
dev->polyline = RQuartz_Polyline;
dev->polygon = RQuartz_Polygon;
dev->path = RQuartz_Path;
dev->locator = RQuartz_Locator;
dev->mode = RQuartz_Mode;
dev->metricInfo = RQuartz_MetricInfo;
dev->holdflush = RQuartz_HoldFlush;
dev->hasTextUTF8 = TRUE;
dev->textUTF8 = RQuartz_Text;
dev->strWidthUTF8 = RQuartz_StrWidth;
dev->left = 0;
dev->top = 0;
/* Magic numbers from on high. */
dev->xCharOffset = 0.4900;
dev->yCharOffset = 0.3333;
dev->yLineBias = 0.20; /* This is .2 for PS/PDF devices... */
dev->canClip = TRUE;
dev->canHAdj = 2;
dev->canChangeGamma= FALSE;
dev->displayListOn = (def->flags & QDFLAG_DISPLAY_LIST) ? TRUE : FALSE;
dev->haveTransparency = 2;
dev->haveTransparentBg = 3; /* FIXME: depends on underlying device */
dev->haveRaster = 2;
dev->haveCapture = (def->cap) ? 2 : 1;
dev->haveLocator = (def->locatePoint) ? 2 : 1;
QuartzDesc *qd = calloc(1, sizeof(QuartzDesc));
qd->width = def->width;
qd->height = def->height;
qd->userInfo = def->userInfo;
qd->getCGContext=def->getCGContext;
qd->locatePoint= def->locatePoint;
qd->close = def->close;
qd->newPage = def->newPage;
qd->state = def->state;
qd->sync = def->sync;
qd->cap = def->cap;
qd->scalex = def->scalex;
qd->scaley = def->scaley;
qd->tscale = 1.0;
qd->ps = def->pointsize;
qd->bg = def->bg;
qd->canvas = def->canvas;
qd->antialias = (def->flags & QPFLAG_ANTIALIAS) ? 1 : 0;
qd->flags = def->flags;
qd->gstate = 0;
qd->font = NULL;
dev->deviceSpecific = qd;
qd->dev = dev;
QuartzDevice_Update(qd);
/* Re-set for bitmap devices later */
dev->right = def->width*72.0;
dev->bottom= def->height*72.0;
qd->clipRect = CGRectMake(0, 0, dev->right, dev->bottom);
qd->dirty = 0;
qd->redraw= 0;
qd->async = 0;
qd->holdlevel = 0;
return (QuartzDesc_t)qd;
}
static QuartzFunctions_t qfn = {
QuartzDevice_Create,
QuartzDevice_DevNumber,
QuartzDevice_Kill,
QuartzDevice_ResetContext,
QuartzDevice_GetWidth,
QuartzDevice_GetHeight,
QuartzDevice_SetSize,
QuartzDevice_GetScaledWidth,
QuartzDevice_GetScaledHeight,
QuartzDevice_SetScaledSize,
QuartzDevice_GetXScale,
QuartzDevice_GetYScale,
QuartzDevice_SetScale,
QuartzDevice_SetTextScale,
QuartzDevice_GetTextScale,
QuartzDevice_SetPointSize,
QuartzDevice_GetPointSize,
QuartzDevice_GetDirty,
QuartzDevice_SetDirty,
QuartzDevice_ReplayDisplayList,
QuartzDevice_GetSnapshot,
QuartzDevice_RestoreSnapshot,
QuartzDevice_GetAntialias,
QuartzDevice_SetAntialias,
QuartzDevice_GetBackground,
QuartzDevice_Activate,
QuartzDevice_SetParameter,
QuartzDevice_GetParameter
};
/* currrently unused: was used by R.app via aqua.c */
QuartzFunctions_t *getQuartzAPI() {
return &qfn;
}
/* old macOS versions has different names for some of the CGFont stuff */
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
#define CGFontCreateWithFontName CGFontCreateWithName
#define CGFontGetGlyphBBoxes CGFontGetGlyphBoundingBoxes
/* The following is a real pain. We have to work around bugs in CoreGraphics
and Apple's dyloader simultaneously so a 10.4 binary runs on 10.5 as well. */
typedef void (*RQFontGetGlyphsForUnichars_t)(CGFontRef a, const UniChar b[], CGGlyph c[], size_t d);
static RQFontGetGlyphsForUnichars_t RQFontGetGlyphsForUnichars;
#include <dlfcn.h> /* dynamically find the right entry point on initialization */
__attribute__((constructor)) static void RQ_init() {
void *r;
if ((r = dlsym(RTLD_NEXT, "CGFontGetGlyphsForUnichars")) || (r = dlsym(RTLD_NEXT, "CGFontGetGlyphsForUnicodes")) ||
(r = dlsym(RTLD_DEFAULT, "CGFontGetGlyphsForUnichars")) || (r = dlsym(RTLD_DEFAULT, "CGFontGetGlyphsForUnicodes")))
RQFontGetGlyphsForUnichars = (RQFontGetGlyphsForUnichars_t) r;
else
error("Cannot load CoreGraphics"); /* this should never be reached but I suppose it's better than a hidden segfault */
}
#define CGFontGetGlyphsForUnichars RQFontGetGlyphsForUnichars
/* and some missing declarations */
extern CGFontRef CGFontCreateWithName(CFStringRef);
extern bool CGFontGetGlyphAdvances(CGFontRef font, const CGGlyph glyphs[], size_t count, int advances[]);
extern int CGFontGetUnitsPerEm(CGFontRef font);
extern bool CGFontGetGlyphBBoxes(CGFontRef font, const CGGlyph glyphs[], size_t count, CGRect bboxes[]);
#else
extern void CGFontGetGlyphsForUnichars(CGFontRef, const UniChar[], CGGlyph[], size_t);
#endif
extern CGFontRef CGContextGetFont(CGContextRef);
#define DEVDESC pDevDesc dd
#define CTXDESC const pGEcontext gc, pDevDesc dd
#define DEVSPEC QuartzDesc *xd = (QuartzDesc*) dd->deviceSpecific; CGContextRef ctx = xd->getCGContext(xd, xd->userInfo)
#define DRAWSPEC QuartzDesc *xd = (QuartzDesc*) dd->deviceSpecific; CGContextRef ctx = xd->getCGContext(xd, xd->userInfo); xd->dirty = 1
#define XD QuartzDesc *xd = (QuartzDesc*) dd->deviceSpecific
#pragma mark Quartz Font Cache
/* Font lookup is expesive yet frequent. Therefore we cache all used ATS fonts (which are global to the app). */
typedef struct font_cache_entry_s {
ATSFontRef font;
char *family;
int face;
} font_cache_entry_t;
#define max_fonts_per_block 32
typedef struct font_cache_s {
font_cache_entry_t e[max_fonts_per_block];
int fonts;
struct font_cache_s *next;
} font_cache_t;
font_cache_t font_cache, *font_cache_tail = &font_cache;
static ATSFontRef RQuartz_CacheGetFont(const char *family, int face) {
font_cache_t *fc = &font_cache;
while (fc) {
int i = 0, j = fc->fonts;
while (i < j) {
if (face == fc->e[i].face && streql(family, fc->e[i].family))
return fc->e[i].font;
i++;
}
fc = fc->next;
}
return 0;
}
static void RQuartz_CacheAddFont(const char *family, int face, ATSFontRef font) {
if (font_cache_tail->fonts >= max_fonts_per_block)
font_cache_tail = font_cache_tail->next = (font_cache_t*) calloc(1, sizeof(font_cache_t));
{
int i = font_cache_tail->fonts;
font_cache_tail->e[i].font = font;
font_cache_tail->e[i].family = strdup(family);
font_cache_tail->e[i].face = face;
font_cache_tail->fonts++;
}
}
#ifdef UNUSED
static void RQuartz_CacheRelease() {
font_cache_t *fc = &font_cache;
while (fc) {
font_cache_t *next = fc->next;
int i = 0, j = fc->fonts;
while (i < j) free(fc->e[i++].family);
if (fc != &font_cache) free(fc);
fc = next;
}
font_cache.fonts = 0;
}
#endif
#pragma mark Device Implementation
/* mapping of virtual family names (e.g "serif") and face to real font names using .Quartzenv$.Quartz.Fonts list */
const char *RQuartz_LookUpFontName(int fontface, const char *fontfamily)
{
const char *mappedFont = 0;
SEXP ns, env, db, names;
PROTECT_INDEX index;
PROTECT(ns = R_FindNamespace(ScalarString(mkChar("grDevices"))));
PROTECT_WITH_INDEX(env = findVar(install(".Quartzenv"), ns), &index);
if(TYPEOF(env) == PROMSXP)
REPROTECT(env = eval(env,ns) ,index);
PROTECT(db = findVar(install(".Quartz.Fonts"), env));
PROTECT(names = getAttrib(db, R_NamesSymbol));
if (*fontfamily) {
int i;
for(i = 0; i < length(names); i++)
if(streql(fontfamily, CHAR(STRING_ELT(names, i)))) {
mappedFont = CHAR(STRING_ELT(VECTOR_ELT(db, i), fontface - 1));
break;
}
}
UNPROTECT(4);
return mappedFont;
}
/* get a font according to the current graphics context */
CGFontRef RQuartz_Font(CTXDESC)
{
const char *fontName = NULL, *fontFamily = gc->fontfamily;
ATSFontRef atsFont = 0;
int fontFace = gc->fontface;
if (fontFace < 1 || fontFace > 5) fontFace = 1; /* just being paranoid */
if (fontFace == 5)
fontName = "Symbol";
else
fontName = RQuartz_LookUpFontName(fontFace, fontFamily[0] ? fontFamily : "default");
if (fontName) {
atsFont = RQuartz_CacheGetFont(fontName, 0); /* face is 0 because we are passing a true font name */
if (!atsFont) { /* not in the cache, get it */
CFStringRef cfFontName = CFStringCreateWithCString(NULL, fontName, kCFStringEncodingUTF8);
atsFont = ATSFontFindFromName(cfFontName, kATSOptionFlagsDefault);
if (!atsFont)
atsFont = ATSFontFindFromPostScriptName(cfFontName, kATSOptionFlagsDefault);
CFRelease(cfFontName);
if (!atsFont) {
warning(_("font \"%s\" could not be found for family \"%s\""), fontName, fontFamily);
return NULL;
}
RQuartz_CacheAddFont(fontName, 0, atsFont);
}
} else { /* the real font name could not be looked up. We must use cache and/or find the right font by family and face */
if (!fontFamily[0]) fontFamily = "Arial";
/* Arial is the default, because Helvetica doesn't have Oblique
on 10.4 - maybe change later? */
atsFont = RQuartz_CacheGetFont(fontFamily, fontFace);
if (!atsFont) { /* not in the cache? Then we need to find the
proper font name from the family name and face */
/* as it turns out kATSFontFilterSelectorFontFamily is not
implemented in macOS (!!) so there is no way to query for a
font from a specific family. Therefore we have to use
text-matching heuristics ... very nasty ... */
char compositeFontName[256];
/* CFStringRef cfFontName; */
if (strlen(fontFamily) > 210) error(_("font family name is too long"));
while (!atsFont) { /* try different faces until exhausted or successful */
strcpy(compositeFontName, fontFamily);
if (fontFace == 2 || fontFace == 4) strcat(compositeFontName, " Bold");
if (fontFace == 3 || fontFace == 4) strcat(compositeFontName, " Italic");
CFStringRef cfFontName = CFStringCreateWithCString(NULL, compositeFontName, kCFStringEncodingUTF8);
atsFont = ATSFontFindFromName(cfFontName, kATSOptionFlagsDefault);
if (!atsFont) atsFont = ATSFontFindFromPostScriptName(cfFontName, kATSOptionFlagsDefault);
CFRelease(cfFontName);
if (!atsFont) {
if (fontFace == 1) { /* more guessing - fontFace == 1 may need Regular or Roman */
strcat(compositeFontName," Regular");
cfFontName = CFStringCreateWithCString(NULL, compositeFontName, kCFStringEncodingUTF8);
atsFont = ATSFontFindFromName(cfFontName, kATSOptionFlagsDefault);
CFRelease(cfFontName);
if (!atsFont) {
strcpy(compositeFontName, fontFamily);
strcat(compositeFontName," Roman");
cfFontName = CFStringCreateWithCString(NULL, compositeFontName, kCFStringEncodingUTF8);
atsFont = ATSFontFindFromName(cfFontName, kATSOptionFlagsDefault);
CFRelease(cfFontName);
}
} else if (fontFace == 3 || fontFace == 4) { /* Oblique is sometimes used instead of Italic (e.g. in Helvetica) */
strcpy(compositeFontName, fontFamily);
if (fontFace == 4) strcat(compositeFontName, " Bold");
strcat(compositeFontName," Oblique");
cfFontName = CFStringCreateWithCString(NULL, compositeFontName, kCFStringEncodingUTF8);
atsFont = ATSFontFindFromName(cfFontName, kATSOptionFlagsDefault);
CFRelease(cfFontName);
}
}
if (!atsFont) { /* try to fall back to a more plain face */
if (fontFace == 4) fontFace = 2;
else if (fontFace != 1) fontFace = 1;
else break;
atsFont = RQuartz_CacheGetFont(fontFamily, fontFace);
if (atsFont) break;
}
}
if (!atsFont)
warning(_("no font could be found for family \"%s\""), fontFamily);
else
RQuartz_CacheAddFont(fontFamily, fontFace, atsFont);
}
}
return CGFontCreateWithPlatformFont(&atsFont);
}
#define RQUARTZ_FILL (1)
#define RQUARTZ_STROKE (1<<1)
#define RQUARTZ_LINE (1<<2)
static void RQuartz_SetFont(CGContextRef ctx, const pGEcontext gc, QuartzDesc *xd) {
CGFontRef font = RQuartz_Font(gc, NULL);
if (font) {
CGContextSetFont(ctx, font);
if (font != xd->font) {
if (xd->font) CGFontRelease(xd->font);
xd->font = font;
}
}
CGContextSetFontSize(ctx, gc->cex * gc->ps);
}
/* pre-10.5 doesn't have kCGColorSpaceGenericRGB so fall back to kCGColorSpaceGenericRGB */
#if MAC_OS_X_VERSION_10_4 >= MAC_OS_X_VERSION_MAX_ALLOWED
#define kCGColorSpaceSRGB kCGColorSpaceGenericRGB
#endif
void RQuartz_Set(CGContextRef ctx,const pGEcontext gc,int flags) {
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
if(flags & RQUARTZ_FILL) {
int fill = gc->fill;
CGFloat fillColor[] = { R_RED(fill)/255.0,
R_GREEN(fill)/255.0,
R_BLUE(fill)/255.0,
R_ALPHA(fill)/255.0 };
CGColorRef fillColorRef = CGColorCreate(cs, fillColor);
CGContextSetFillColorWithColor(ctx, fillColorRef);
CGColorRelease(fillColorRef);
}
if(flags & RQUARTZ_STROKE) {
int stroke = gc->col;
CGFloat strokeColor[] = { R_RED(stroke)/255.0,
R_GREEN(stroke)/255.0,
R_BLUE(stroke)/255.0,
R_ALPHA(stroke)/255.0 };
CGColorRef strokeColorRef = CGColorCreate(cs, strokeColor);
CGContextSetStrokeColorWithColor(ctx, strokeColorRef);
CGColorRelease(strokeColorRef);
}
if(flags & RQUARTZ_LINE) {
CGFloat dashlist[8];
int i, ndash = 0;
int lty = gc->lty;
float lwd = (float)(gc->lwd * 0.75);
CGContextSetLineWidth(ctx, lwd);
for(i = 0; i < 8 && lty; i++) {
dashlist[ndash++] = (lwd >= 1 ? lwd : 1) * (lty & 15);
lty >>= 4;
}
CGContextSetLineDash(ctx, 0, dashlist, ndash);
CGLineCap cap = kCGLineCapButt;
switch(gc->lend) {
case GE_ROUND_CAP: cap = kCGLineCapRound; break;
case GE_BUTT_CAP: cap = kCGLineCapButt; break;
case GE_SQUARE_CAP: cap = kCGLineCapSquare; break;
}
CGContextSetLineCap(ctx,cap);
CGLineJoin join = kCGLineJoinRound;
switch(gc->ljoin) {
case GE_ROUND_JOIN: join = kCGLineJoinRound; break;
case GE_MITRE_JOIN: join = kCGLineJoinMiter; break;
case GE_BEVEL_JOIN: join = kCGLineJoinBevel; break;
}
CGContextSetLineJoin(ctx, join);
CGContextSetMiterLimit(ctx, gc->lmitre);
}
CGColorSpaceRelease(cs);
}
#define SET(X) RQuartz_Set(ctx, gc, (X))
#define NOCTX { xd->async = 1; return; }
#define NOCTXR(V) { xd->async = 1; return(V); }
static void RQuartz_Close(DEVDESC)
{
XD;
if (xd->close) xd->close(xd, xd->userInfo);
}
static void RQuartz_Activate(DEVDESC)
{
XD;
if (xd->state) xd->state(xd, xd->userInfo, 1);
}
static void RQuartz_Deactivate(DEVDESC)
{
XD;
if (xd->state) xd->state(xd, xd->userInfo, 0);
}
static void RQuartz_Size(double *left, double *right, double *bottom, double *top, DEVDESC)
{
XD;
*left = *top = 0;
*right = QuartzDevice_GetWidth(xd) * 72.0;
*bottom = QuartzDevice_GetHeight(xd) * 72.0;
}
static void RQuartz_NewPage(CTXDESC)
{
{
DRAWSPEC;
ctx = NULL;
if (xd->newPage) xd->newPage(xd, xd->userInfo, xd->redraw ? QNPF_REDRAW : 0);
}
{ /* we have to re-fetch the status *after* newPage since it may have changed it */
DRAWSPEC;
if (!ctx) NOCTX;
{
CGRect bounds = CGRectMake(0, 0,
QuartzDevice_GetScaledWidth(xd) * 72.0,
QuartzDevice_GetScaledHeight(xd) * 72.0);
/* reset the clipping region by restoring the base GC.
If there is no GC on the stack then the clipping region was never set. */
if (xd->gstate > 0) {
CGContextRestoreGState(ctx);
CGContextSaveGState(ctx);
/* no need to modify gstate since we don't modify the stack */
}
/* The logic is to paint the canvas then gc->fill.
(The canvas colour is set to 0 on non-screen devices.)
*/
if (R_ALPHA(xd->canvas) > 0 && !R_OPAQUE(gc->fill)) {
/* Paint the canvas colour. */
int savefill = gc->fill;
CGContextClearRect(ctx, bounds);
gc->fill = xd->canvas;
SET(RQUARTZ_FILL);
CGContextFillRect(ctx, bounds);
gc->fill = savefill;
}
SET(RQUARTZ_FILL); /* this will fill with gc->fill */
CGContextFillRect(ctx, bounds);
}
}
}
static int RQuartz_HoldFlush(DEVDESC, int level)
{
int ol;
XD;
/* FIXME: should we check for interactive? */
ol = xd->holdlevel;
xd->holdlevel += level;
if (xd->holdlevel < 0) xd->holdlevel = 0;
if (xd->holdlevel == 0) { /* flush */
/* trigger flush */
if (xd->sync)
xd->sync(xd, xd->userInfo);
else {
CGContextRef ctx = xd->getCGContext(xd, xd->userInfo);
if (ctx)
CGContextSynchronize(ctx);
}
} else if (ol == 0) { /* first hold */
/* could display a wait cursor or something ... */
}
return xd->holdlevel;
}
static void RQuartz_Clip(double x0, double x1, double y0, double y1, DEVDESC)
{
DRAWSPEC;
if (!ctx) NOCTX;
if(xd->gstate > 0) {
--xd->gstate;
CGContextRestoreGState(ctx);
}
CGContextSaveGState(ctx);
xd->gstate++;
if(x1 > x0) { double t = x1; x1 = x0;x0 = t; }
if(y1 > y0) { double t = y1; y1 = y0;y0 = t; }
xd->clipRect = CGRectMake(x0, y0, x1 - x0, y1 - y0);
CGContextClipToRect(ctx, xd->clipRect);
}
/* non-symbol text is sent in UTF-8 */
static CFStringRef text2unichar(CTXDESC, const char *text, UniChar **buffer, int *free)
{
CFStringRef str;
if(gc->fontface == 5)
str = CFStringCreateWithCString(NULL, text, kCFStringEncodingMacSymbol);
else {
str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8);
/* Try fallback Latin1 encoding if UTF8 doesn't work
-- should no longer be needed. */
if(!str)
CFStringCreateWithCString(NULL, text, kCFStringEncodingISOLatin1);
}
if (!str) return NULL;
*buffer = (UniChar*) CFStringGetCharactersPtr(str);
if (*buffer == NULL) {
CFIndex length = CFStringGetLength(str);
/* FIXME: check allocation */
*buffer = malloc(length * sizeof(UniChar));
CFStringGetCharacters(str, CFRangeMake(0, length), *buffer);
*free = 1;
}
return str;
}
static double RQuartz_StrWidth(const char *text, CTXDESC)
{
DEVSPEC;
if (!ctx) NOCTXR(strlen(text) * 10.0); /* for sanity reasons */
RQuartz_SetFont(ctx, gc, xd);
CGFontRef font = CGContextGetFont(ctx);
float aScale = (float)((gc->cex * gc->ps * xd->tscale) /
CGFontGetUnitsPerEm(font));
UniChar *buffer;
CGGlyph *glyphs;
int *advances;
int Free = 0, len;
CFStringRef str = text2unichar(gc, dd, text, &buffer, &Free);
if (!str) return 0.0; /* invalid text contents */
len = (int) CFStringGetLength(str);
/* FIXME: check allocations */
glyphs = malloc(sizeof(CGGlyph) * len);
advances = malloc(sizeof(int) * len);
CGFontGetGlyphsForUnichars(font, buffer, glyphs, len);
CGFontGetGlyphAdvances(font, glyphs, len, advances);
float width = 0.0; /* aScale*CGFontGetLeading(CGContextGetFont(ctx)); */
for(int i = 0; i < len; i++) width += aScale * advances[i];
free(advances);
free(glyphs);
if(Free) free(buffer);
CFRelease(str);
return width;
}
static void RQuartz_Text(double x, double y, const char *text, double rot, double hadj, CTXDESC)
{
DRAWSPEC;
if (!ctx) NOCTX;
/* A stupid hack because R isn't consistent. */
int fill = gc->fill;
gc->fill = gc->col;
SET(RQUARTZ_FILL | RQUARTZ_STROKE);
RQuartz_SetFont(ctx, gc, xd);
gc->fill = fill;
CGFontRef font = CGContextGetFont(ctx);
float aScale = (float) ((gc->cex * gc->ps * xd->tscale) /
CGFontGetUnitsPerEm(font));
UniChar *buffer;
CGGlyph *glyphs;
int Free = 0, len, i;
float width = 0.0;
CFStringRef str = text2unichar(gc, dd, text, &buffer, &Free);
if (!str) return; /* invalid text contents */
len = (int) CFStringGetLength(str);
/* FIXME: check allocations */
glyphs = malloc(sizeof(CGGlyph) * len);
CGFontGetGlyphsForUnichars(font, buffer, glyphs, len);
int *advances = malloc(sizeof(int) * len);
CGSize *g_adv = malloc(sizeof(CGSize) * len);
CGFontGetGlyphAdvances(font, glyphs, len, advances);
for(i =0 ; i < len; i++) {
width += advances[i] * aScale;
g_adv[i] = CGSizeMake(aScale * advances[i] * cos(-DEG2RAD*rot), aScale*advances[i]*sin(-DEG2RAD * rot));
}
free(advances);
CGContextSetTextMatrix(ctx,
CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, -1.0),
CGAffineTransformMakeRotation(-DEG2RAD * rot)));
double ax = (width * hadj) * cos(-DEG2RAD * rot);
double ay = (width * hadj) * sin(-DEG2RAD * rot);
/* double h = CGFontGetXHeight(CGContextGetFont(ctx))*aScale; */
CGContextSetTextPosition(ctx, x - ax, y - ay);
/* Rprintf("%s,%.2f %.2f (%.2f,%.2f) (%d,%f)\n",text,hadj,width,ax,ay,CGFontGetUnitsPerEm(CGContextGetFont(ctx)),CGContextGetFontSize(ctx)); */
CGContextShowGlyphsWithAdvances(ctx,glyphs, g_adv, len);
free(glyphs);
free(g_adv);
if(Free) free(buffer);
CFRelease(str);
}
static void RQuartz_Rect(double x0, double y0, double x1, double y1, CTXDESC)
{
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_FILL | RQUARTZ_STROKE | RQUARTZ_LINE);
if (xd->flags & QDFLAG_RASTERIZED) {
/* in the case of borderless rectangles snap them to pixels.
this solves issues with image() without introducing other artifacts.
other approaches (disabling anti-aliasing, drawing background first,
snapping rect with borders) don't work as well, because they have
unwanted visual side-effects. */
if (R_ALPHA(gc->fill) > 0 && R_ALPHA(gc->col) == 0) {
/* store original values in case we need to go back */
double ox0 = x0, ox1 = x1, oy0 = y0, oy1 = y1;
x0 = (round(x0 * xd->scalex)) / xd->scalex;
x1 = (round(x1 * xd->scalex)) / xd->scalex;
y0 = (round(y0 * xd->scaley)) / xd->scaley;
y1 = (round(y1 * xd->scaley)) / xd->scaley;
/* work-around for PR#13744 - make sure the width or height
does not drop to 0 because of aligning. */
if (x0 == x1 && (ox0 != ox1)) x1 += ox1 - ox0;
if (y0 == y1 && (oy0 != oy1)) y1 += oy1 - oy0;
}
}
CGContextBeginPath(ctx);
CGContextAddRect(ctx, CGRectMake(x0, y0, x1 - x0, y1 - y0));
CGContextDrawPath(ctx, kCGPathFillStroke);
}
static void RQuartz_Raster(unsigned int *raster, int w, int h,
double x, double y,
double width, double height,
double rot,
Rboolean interpolate,
const pGEcontext gc, pDevDesc dd)
{
DRAWSPEC;
if (!ctx) NOCTX;
CGDataProviderRef dp;
CGColorSpaceRef cs;
CGImageRef img;
/* Create a "data provider" containing the raster data */
dp = CGDataProviderCreateWithData(NULL, (void *) raster, 4*w*h, NULL);
cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
/* Create a quartz image from the data provider */
img = CGImageCreate(w, h,
8, /* bits per channel */
32, /* bits per pixel */
4*w, /* bytes per row */
cs, /* color space */
/* R uses AGBR which is so unusual (inverted RGBA) that it corresponds to endinness inverse(!) to the host with alpha last (=RGBA). */
#ifdef __BIG_ENDIAN__
kCGImageAlphaLast | kCGBitmapByteOrder32Little,
#else
kCGImageAlphaLast | kCGBitmapByteOrder32Big,
#endif
dp, /* data provider */
NULL,/* decode array */
1, /* interpolate (interpolation type below) */
kCGRenderingIntentDefault);
if (height < 0) {
y = y + height;
height = -height;
}
CGContextSaveGState(ctx);
/* Translate by height of image */
CGContextTranslateCTM(ctx, 0.0, height);
/* Flip vertical */
CGContextScaleCTM(ctx, 1.0, -1.0);
/* Translate to position */
CGContextTranslateCTM(ctx, x, -y);
/* Rotate */
CGContextRotateCTM(ctx, rot*M_PI/180.0);
/* Determine interpolation method */
if (interpolate)
CGContextSetInterpolationQuality(ctx, kCGInterpolationDefault);
else
CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);
/* Draw the quartz image */
CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), img);
CGContextRestoreGState(ctx);
/* Tidy up */
CGColorSpaceRelease(cs);
CGDataProviderRelease(dp);
CGImageRelease(img);
}
static SEXP RQuartz_Cap(pDevDesc dd)
{
SEXP raster = R_NilValue;
DRAWSPEC;
if (!ctx) NOCTXR(raster);
if (xd->cap)
raster = (SEXP) xd->cap(xd, xd->userInfo);
return raster;
}
static void RQuartz_Circle(double x, double y, double r, CTXDESC)
{
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_FILL | RQUARTZ_STROKE | RQUARTZ_LINE);
double r2 = 2.0*r;
CGContextBeginPath(ctx);
CGContextAddEllipseInRect(ctx,CGRectMake(x-r,y-r,r2,r2));
CGContextDrawPath(ctx,kCGPathFillStroke);
}
static void RQuartz_Line(double x1, double y1, double x2, double y2, CTXDESC)
{
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_STROKE | RQUARTZ_LINE);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, x1, y1);
CGContextAddLineToPoint(ctx, x2, y2);
CGContextStrokePath(ctx);
}
#define max_segments 100
static void RQuartz_Polyline(int n, double *x, double *y, CTXDESC)
{
if (n < 2) return;
int i = 0;
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_STROKE | RQUARTZ_LINE);
/* CGContextStrokeLineSegments turned out to be a bad idea due to
Leopard restarting dashes for each segment.
CGContextAddLineToPoint is fast enough.
The only remaining problem is that Quartz seems to restart
dashes at segment breakup points. We should make the segments
break-up an optional feature and possibly fix the underlying
problem (software rendering).
*/
while (i < n) {
int j = i + max_segments;
if (j > n) j = n;
CGContextBeginPath(ctx);
if (i) i--; /* start at the last point of the preceding chunk */
CGContextMoveToPoint(ctx, x[i], y[i]);
while(++i < j)
CGContextAddLineToPoint(ctx, x[i], y[i]);
CGContextStrokePath(ctx);
}
}
static void RQuartz_Polygon(int n, double *x, double *y, CTXDESC)
{
if (n < 2) return;
int i;
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_FILL | RQUARTZ_STROKE | RQUARTZ_LINE);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, x[0], y[0]);
for(i = 1; i < n; i++)
CGContextAddLineToPoint(ctx, x[i], y[i]);
CGContextClosePath(ctx);
CGContextDrawPath(ctx, kCGPathFillStroke);
}
static void RQuartz_Path(double *x, double *y,
int npoly, int* nper,
Rboolean winding,
CTXDESC)
{
int i, j, index;
DRAWSPEC;
if (!ctx) NOCTX;
SET(RQUARTZ_FILL | RQUARTZ_STROKE | RQUARTZ_LINE);
index = 0;
CGContextBeginPath(ctx);
for (i=0; i < npoly; i++) {
CGContextMoveToPoint(ctx, x[index], y[index]);
index++;
for(j=1; j < nper[i]; j++) {
CGContextAddLineToPoint(ctx, x[index], y[index]);
index++;
}
CGContextClosePath(ctx);
}
if (winding) {
CGContextDrawPath(ctx, kCGPathFillStroke);
} else {
CGContextDrawPath(ctx, kCGPathEOFillStroke);
}
}
static void RQuartz_Mode(int mode, DEVDESC)
{
DEVSPEC;
if (!ctx) NOCTX;
/* don't do anything in redraw as we can signal the end */
if (xd->redraw) return;
/* mode=0 -> drawing complete, signal sync */
if (mode == 0 && xd->holdlevel == 0) {
if (xd->sync)
xd->sync(xd, xd->userInfo);
else
CGContextSynchronize(ctx);
}
}
static void
RQuartz_MetricInfo(int c, const pGEcontext gc,
double *ascent, double *descent, double *width,
pDevDesc dd)
{
DRAWSPEC;
if (!ctx) { /* dummy data if we have no context, for sanity reasons */
*ascent = 10.0;
*descent= 2.0;
*width = 9.0;
NOCTX;
}
RQuartz_SetFont(ctx, gc, xd);
{
CGFontRef font = CGContextGetFont(ctx);
float aScale = (float)((gc->cex * gc->ps * xd->tscale) /
CGFontGetUnitsPerEm(font));
UniChar *buffer, single;
CGGlyph glyphs[8];
CFStringRef str = NULL;
int free_buffer = 0, len;
*width = *ascent = *descent = 0.0; /* data for bail-out cases */
if (c >= 0 && c <= ((mbcslocale && gc->fontface != 5) ? 127 : 255)) {
char text[2] = { (char)c, 0 };
str = text2unichar(gc, dd, text, &buffer, &free_buffer);
if(!str) return;
len = (int) CFStringGetLength(str);
if (len > 7) return; /* this is basically impossible,
but you never know */
} else {
single = (UniChar) ((c < 0) ? -c : c);
buffer = &single;
len = 1;
}
*width = 0.0;
CGFontGetGlyphsForUnichars(font, buffer, glyphs, len);
{
int i;
int advances[8];
CGRect bboxes[8];
CGFontGetGlyphAdvances(font, glyphs, len, advances);
CGFontGetGlyphBBoxes(font, glyphs, len, bboxes);
for(i = 0; i < len; i++)
*width += advances[i] * aScale;
*ascent = aScale * (bboxes[0].size.height + bboxes[0].origin.y);
*descent = -aScale * bboxes[0].origin.y;
}
if (free_buffer) free(buffer);
if (str) CFRelease(str);
}
}
static Rboolean RQuartz_Locator(double *x, double *y, DEVDESC)
{
Rboolean res;
DEVSPEC;
ctx = NULL;
if (!xd->locatePoint)
return FALSE;
res = xd->locatePoint(xd, xd->userInfo, x, y);
*x/=xd->scalex;
*y/=xd->scaley;
return res;
}
#pragma mark -
#pragma mark R Interface
#include "qdCocoa.h"
#include "qdBitmap.h"
#include "qdPDF.h"
/* disabled for now until we get to test in on 10.3 #include "qdCarbon.h" */
/* current fake */
QuartzDesc_t
QuartzCarbon_DeviceCreate(pDevDesc dd, QuartzFunctions_t *fn, QuartzParameters_t *par)
{
return NULL;
}
#define ARG(HOW,WHAT) HOW(CAR(WHAT));WHAT = CDR(WHAT)
/* C version of the Quartz call (experimental)
Quartz descriptor on success, NULL on failure.
If errorCode is not NULL, it will contain the error code on exit */
QuartzDesc_t
Quartz_C(QuartzParameters_t *par, quartz_create_fn_t q_create, int *errorCode)
{
if (!q_create || !par) {
if (errorCode) errorCode[0] = -4;
return NULL;
}
{
const void *vmax = vmaxget();
QuartzDesc_t qd = NULL;
R_GE_checkVersionOrDie(R_GE_version);
R_CheckDeviceAvailable();
{
const char *devname = "quartz_off_screen";
/* FIXME: check this allocation */
pDevDesc dev = calloc(1, sizeof(DevDesc));
if (!dev) {
if (errorCode) errorCode[0] = -2;
return NULL;
}
if (!(qd = q_create(dev, &qfn, par))) {
vmaxset(vmax);
free(dev);
if (errorCode) errorCode[0] = -3;
return NULL;
}
if(streql(par->type, "") || streql(par->type, "native")
|| streql(par->type, "cocoa") || streql(par->type, "carbon"))
devname = "quartz";
gsetVar(R_DeviceSymbol, mkString(devname), R_BaseEnv);
pGEDevDesc dd = GEcreateDevDesc(dev);
GEaddDevice(dd);
GEinitDisplayList(dd);
vmaxset(vmax);
}
return qd;
}
}
/* ARGS: type, file, width, height, ps, family, antialias,
title, bg, canvas, dpi */
SEXP Quartz(SEXP args)
{
SEXP tmps, bgs, canvass;
double width, height, ps;
Rboolean antialias;
int quartzpos, bg, canvas, module = 0;
double mydpi[2], *dpi = 0;
const char *type, *mtype = 0, *family, *title;
char *file = NULL;
QuartzDesc_t qd = NULL;
const void *vmax = vmaxget();
/* Get function arguments */
args = CDR(args); /* Skip the call */
if (TYPEOF(CAR(args)) != STRSXP || LENGTH(CAR(args)) < 1)
type = "";
else
type = CHAR(STRING_ELT(CAR(args), 0));
args = CDR(args);
/* we may want to support connections at some point, but not yet ... */
tmps = CAR(args); args = CDR(args);
if (isNull(tmps))
file = NULL;
else if (isString(tmps) && LENGTH(tmps) >= 1) {
const char *tmp = R_ExpandFileName(CHAR(STRING_ELT(tmps, 0)));
file = R_alloc(strlen(tmp) + 1, sizeof(char));
strcpy(file, tmp);
} else
error(_("invalid 'file' argument"));
width = ARG(asReal,args);
height = ARG(asReal,args);
ps = ARG(asReal,args);
family = CHAR(STRING_ELT(CAR(args), 0)); args = CDR(args);
antialias = ARG(asLogical,args);
title = CHAR(STRING_ELT(CAR(args), 0)); args = CDR(args);
bgs = CAR(args); args = CDR(args);
bg = RGBpar(bgs, 0);
canvass = CAR(args); args = CDR(args);
canvas = RGBpar(canvass, 0) | 0xff000000; /* force opaque */
tmps = CAR(args); args = CDR(args);
if (!isNull(tmps)) {
tmps = coerceVector(tmps, REALSXP);
if (LENGTH(tmps) > 0) {
dpi = mydpi;
mydpi[0] = REAL(tmps)[0];
if (LENGTH(tmps) > 1)
mydpi[1] = REAL(tmps)[1];
else
mydpi[1] = mydpi[0];
}
}
/* just in case someone passed NAs/NaNs */
if (dpi && (ISNAN(dpi[0]) || ISNAN(dpi[1]))) dpi=0;
if (ISNAN(width) || ISNAN(height) || width <= 0.0 || height <= 0.0)
error(_("invalid quartz() device size"));
if (type) {
const quartz_module_t *m = quartz_modules;
mtype = type;
while (m->type) {
if (!strcasecmp(type, m->type)) {
module = m->qbe;
if (m->subst) mtype = m->subst;
break;
}
m++;
}
if (!strncasecmp(type, "bitmap:", 7)) {
module = QBE_BITMAP;
mtype = mtype + 7;
}
}
quartzpos = 1;
R_GE_checkVersionOrDie(R_GE_version);
R_CheckDeviceAvailable();
BEGIN_SUSPEND_INTERRUPTS {
pDevDesc dev = calloc(1, sizeof(DevDesc));
if (!dev)
error(_("unable to create device description"));
QuartzParameters_t qpar = {
sizeof(qpar),
mtype, file, title,
-1.0, -1.0, width, height, ps,
family,
antialias ? QPFLAG_ANTIALIAS: 0,
-1, /* connection */
bg, canvas,
dpi
};
/* re-routed code has the first shot */
if (ptr_QuartzBackend)
qd = ptr_QuartzBackend(dev, &qfn, &qpar);
if (qd == NULL) { /* try internal modules next */
switch (module) {
case QBE_COCOA:
qd = QuartzCocoa_DeviceCreate(dev, &qfn, &qpar);
break;
case QBE_NATIVE:
/* native is essentially cocoa with carbon fall-back */
qd = QuartzCocoa_DeviceCreate(dev, &qfn, &qpar);
if (qd) break;
case QBE_CARBON:
qd = QuartzCarbon_DeviceCreate(dev, &qfn, &qpar);
break;
case QBE_PDF:
qpar.canvas = 0; /* so not used */
qd = QuartzPDF_DeviceCreate(dev, &qfn, &qpar);
break;
case QBE_BITMAP:
/* we need to set up the default file name here, where we
know the original type name. */
if (file == NULL) {
static char deffile[30];
snprintf(deffile, 30, "%s.%s", "Rplot%03d", type);
qpar.file = deffile;
}
qpar.canvas = 0; /* so not used */
qd = QuartzBitmap_DeviceCreate(dev, &qfn, &qpar);
break;
}
}
if (qd == NULL) {
vmaxset(vmax);
free(dev);
error(_("unable to create quartz() device target, given type may not be supported"));
}
const char *devname = "quartz_off_screen";
if(streql(type, "") || streql(type, "native") || streql(type, "cocoa")
|| streql(type, "carbon")) devname = "quartz";
SEXP f = PROTECT(mkString(devname));
if(file) setAttrib(f, install("filepath"), mkString(file));
gsetVar(R_DeviceSymbol, f, R_BaseEnv);
UNPROTECT(1);
pGEDevDesc dd = GEcreateDevDesc(dev);
GEaddDevice(dd);
GEinitDisplayList(dd);
} END_SUSPEND_INTERRUPTS;
vmaxset(vmax);
return R_NilValue;
}
#include <sys/sysctl.h>
static double cached_darwin_version = 0.0;
/* Darwin version X.Y maps to macOS version 10.(X - 4).Y */
static double darwin_version() {
char ver[32];
size_t len = sizeof(ver) - 1;
int mib[2] = { CTL_KERN, KERN_OSRELEASE };
if (cached_darwin_version > 0.0)
return cached_darwin_version;
sysctl(mib, 2, &ver, &len, 0, 0);
return (cached_darwin_version = atof(ver));
}
#include <mach/mach.h>
#include <servers/bootstrap.h>
/* even as of Darwin 9 there is no entry for bootstrap_info in bootrap headers */
extern kern_return_t bootstrap_info(mach_port_t , /* bootstrap port */
name_array_t*, mach_msg_type_number_t*, /* service */
name_array_t*, mach_msg_type_number_t*, /* server */
bool_array_t*, mach_msg_type_number_t*); /* active */
/* returns 1 if window server session service
(com.apple.windowserver.session) is present in the boostrap
namespace (pre-Lion) or when a current session is present, active
and there is no SSH_CONNECTION (Lion and later).
returns 0 if an error occurred or the service is not
present. For all practical purposes this returns 1 only if run
interactively via LS. Although ssh to a machine that has a running
session for the same user will allow a WS connection, this function
will still return 0 in that case.
NOTE: on macOS 10.5 we are currently NOT searching the parent
namespaces. This is currently OK, because the session service will
be registered in the session namespace which is the last in the
chain. However, this could change in the future.
*/
static int has_wss() {
int res = 0;
if (darwin_version() < 11.0) { /* before Lion we get reliable information from the bootstrap info */
kern_return_t kr;
mach_port_t self = mach_task_self();
mach_port_t bport = MACH_PORT_NULL;
kr = task_get_bootstrap_port(self, &bport);
if (kr == KERN_SUCCESS) {
kern_return_t kr;
name_array_t serviceNames;
mach_msg_type_number_t serviceNameCount;
name_array_t serverNames;
mach_msg_type_number_t serverNameCount;
bool_array_t active;
mach_msg_type_number_t activeCount;
serviceNames = NULL;
serverNames = NULL;
active = NULL;
kr = bootstrap_info(bport,
&serviceNames, &serviceNameCount,
&serverNames, &serverNameCount,
&active, &activeCount);
if (kr == KERN_SUCCESS) {
unsigned int i = 0;
while (i < serviceNameCount) {
if (!strcmp(serviceNames[i], "com.apple.windowserver.session")) {
res = 1;
break;
}
i++;
}
}
}
if (bport != MACH_PORT_NULL)
mach_port_deallocate(mach_task_self(), bport);
} else {
/* On macOS 10.7 (Lion) and higher two things changed:
a) there is no com.apple.windowserver.session anymore
so the above will fail
b) every process has now the full bootstrap info,
so in fact even remote connections will be able to
run on-screen tasks if the user is logged in
So we need to add some heuristics to decide when the user
actually wants Quartz ... */
/* check user's session */
CFDictionaryRef dict = CGSessionCopyCurrentDictionary();
if (dict) { /* allright, let's see if the session is current */
CFTypeRef obj = CFDictionaryGetValue(dict, CFSTR("kCGSSessionOnConsoleKey"));
if (obj && CFGetTypeID(obj) == CFBooleanGetTypeID()) {
/* even if this session is active, we don't use Quartz for SSH connections */
if (CFBooleanGetValue(obj) && (!getenv("SSH_CONNECTION") || getenv("SSH_CONNECTION")[0] == 0))
res = 1;
}
CFRelease(dict);
}
}
return res;
}
SEXP makeQuartzDefault() {
return ScalarLogical(has_wss());
}
#else
/* --- no AQUA support = no Quartz --- */
#include "grDevices.h"
#include <R_ext/QuartzDevice.h>
SEXP Quartz(SEXP args)
{
warning(_("Quartz device is not available on this platform"));
return R_NilValue;
}
SEXP makeQuartzDefault() {
return ScalarLogical(FALSE);
}
QuartzDesc_t
Quartz_C(QuartzParameters_t *par, quartz_create_fn_t q_create, int *errorCode)
{
if (errorCode) errorCode[0] = -1;
return NULL;
}
void *getQuartzAPI()
{
return NULL;
}
#endif