├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── binding.gyp ├── index.js ├── license.md ├── package.json ├── readme.md ├── src ├── Autocrop.cc ├── Enums.cc ├── Enums.h ├── Rsvg.cc ├── Rsvg.h └── RsvgCairo.h └── test ├── .jshintrc ├── helpers └── chai.js ├── index.test.js └── old.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /experiments/ 3 | /node_modules/ 4 | npm-debug.log 5 | .npmrc 6 | *.DS_Store 7 | *._t_ 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "immed" : true, 7 | "indent" : 1, 8 | "latedef" : true, 9 | "newcap" : true, 10 | "noarg" : true, 11 | "nonew" : true, 12 | "quotmark" : true, 13 | "undef" : true, 14 | "unused" : true, 15 | "strict" : true, 16 | "trailing" : true, 17 | "maxdepth" : 4, 18 | "maxlen" : 100, 19 | "eqnull" : true 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - sudo apt-get update -qq 6 | - sudo apt-get install -qq librsvg2-dev 7 | - npm install -g grunt-cli 8 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | module.exports = function(grunt) { 5 | 6 | // Project configuration. 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | 10 | jshint: { 11 | options: { 12 | jshintrc: true 13 | }, 14 | gruntfile: { 15 | src: 'Gruntfile.js', 16 | }, 17 | lib: { 18 | src: ['index.js'] 19 | }, 20 | tests: { 21 | src: ['test/**/*.js'] 22 | } 23 | }, 24 | 25 | mochaTest: { 26 | test: { 27 | options: { 28 | globals: ['should'], 29 | timeout: 3000, 30 | ui: 'bdd', 31 | reporter: 'spec', 32 | colors: true, 33 | require: ['test/helpers/chai'] 34 | }, 35 | src: ['test/**/*.test.js'] 36 | } 37 | } 38 | }); 39 | 40 | // Tasks. 41 | grunt.loadNpmTasks('grunt-contrib-jshint'); 42 | grunt.loadNpmTasks('grunt-mocha-test'); 43 | 44 | // Default task. 45 | grunt.registerTask('default', ['jshint']); 46 | grunt.registerTask('test', ['jshint', 'mochaTest']); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "rsvg", 5 | "sources": [ 6 | "src/Rsvg.cc", 7 | "src/Enums.cc", 8 | "src/Autocrop.cc" 9 | ], 10 | "variables": { 11 | "packages": "librsvg-2.0 cairo-png cairo-pdf cairo-svg", 12 | "libraries": "= 0. 99 | * @deprecated since version 2.0 100 | * @member {number} 101 | */ 102 | Rsvg.prototype.dpiX = 90; 103 | 104 | // Define getter/setter for deprecated `dpiX` property. 105 | Object.defineProperty(Rsvg.prototype, 'dpiX', { 106 | configurable: true, 107 | enumerable: true, 108 | get: util.deprecate(function() { 109 | return this.handle.getDPIX(); 110 | }, 'Rsvg#dpiX: DPI does not affect rendering.'), 111 | set: util.deprecate(function(dpi) { 112 | this.handle.setDPIX(dpi); 113 | }, 'Rsvg#dpiX: DPI does not affect rendering.') 114 | }); 115 | 116 | /** 117 | * Vertical resolution. Allowed values: >= 0. 118 | * @deprecated since version 2.0 119 | * @member {number} 120 | */ 121 | Rsvg.prototype.dpiY = 90; 122 | 123 | // Define getter/setter for deprecated `dpiY` property. 124 | Object.defineProperty(Rsvg.prototype, 'dpiY', { 125 | configurable: true, 126 | enumerable: true, 127 | get: util.deprecate(function() { 128 | return this.handle.getDPIY(); 129 | }, 'Rsvg#dpiY: DPI does not affect rendering.'), 130 | set: util.deprecate(function(dpi) { 131 | this.handle.setDPIY(dpi); 132 | }, 'Rsvg#dpiY: DPI does not affect rendering.') 133 | }); 134 | 135 | /** 136 | * Image width. Always integer. 137 | * @readonly 138 | * @member {number} 139 | */ 140 | Rsvg.prototype.width = 0; 141 | 142 | // Define getter for `width` property. 143 | Object.defineProperty(Rsvg.prototype, 'width', { 144 | configurable: true, 145 | enumerable: true, 146 | get: function() { 147 | return this.handle.getWidth(); 148 | } 149 | }); 150 | 151 | /** 152 | * Image height. Always integer. 153 | * @readonly 154 | * @member {number} 155 | */ 156 | Rsvg.prototype.height = 0; 157 | 158 | // Define getter for `height` property. 159 | Object.defineProperty(Rsvg.prototype, 'height', { 160 | configurable: true, 161 | enumerable: true, 162 | get: function() { 163 | return this.handle.getHeight(); 164 | } 165 | }); 166 | 167 | /** 168 | * @see [Node.JS API]{@link 169 | * http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1} 170 | * @private 171 | */ 172 | Rsvg.prototype._write = function(data, encoding, callback) { 173 | try { 174 | this.handle.write(data); 175 | } catch (error) { 176 | this.lastErrorMessage = error.message; 177 | callback(new Error('Rsvg write failure: ' + error.message)); 178 | return; 179 | } 180 | 181 | callback(); 182 | }; 183 | 184 | /** 185 | * Get the DPI for the outgoing pixbuf. 186 | * 187 | * @deprecated since version 2.0 188 | * @returns {{x: number, y: number}} 189 | */ 190 | Rsvg.prototype.getDPI = util.deprecate(function() { 191 | return this.handle.getDPI(); 192 | }, 'Rsvg#getDPI(): DPI does not affect rendering.'); 193 | 194 | /** 195 | * Set the DPI for the outgoing pixbuf. Common values are 75, 90, and 300 DPI. 196 | * Passing null to x or y will reset the DPI to whatever the default value 197 | * happens to be (usually 90). You can set both x and y by specifying only the 198 | * first argument. 199 | * 200 | * @deprecated since version 2.0 201 | * @param {number} x - Horizontal resolution. 202 | * @param {number} [y] - Vertical resolution. Set to the same as X if left out. 203 | */ 204 | Rsvg.prototype.setDPI = util.deprecate(function(x, y) { 205 | this.handle.setDPI(x, y); 206 | }, 'Rsvg#setDPI(): DPI does not affect rendering.'); 207 | 208 | /** 209 | * Get the SVG's size or the size/position of a subelement if id is given. The 210 | * id must begin with "#". 211 | * 212 | * @param {string} [id] - Subelement to determine the size and position of. 213 | * @returns {{width: number, height: number, x: number, y: number}} 214 | */ 215 | Rsvg.prototype.dimensions = function(id) { 216 | return this.handle.dimensions(id); 217 | }; 218 | 219 | /** 220 | * Checks whether the subelement with given id exists in the SVG document. 221 | * 222 | * @param {string} id - Subelement to check existence of. 223 | * @returns {boolean} 224 | */ 225 | Rsvg.prototype.hasElement = function(id) { 226 | return this.handle.hasElement(id); 227 | }; 228 | 229 | /** 230 | * Find the drawing area, ie. the smallest area that has image content in the 231 | * SVG document. 232 | * 233 | * @returns {{width: number, height: number, x: number, y: number}} 234 | */ 235 | Rsvg.prototype.autocrop = function() { 236 | var area = this.handle.autocrop(); 237 | area.x = area.x.toFixed(3) * 1; 238 | area.y = area.y.toFixed(3) * 1; 239 | area.width = area.width.toFixed(3) * 1; 240 | area.height = area.height.toFixed(3) * 1; 241 | return area; 242 | }; 243 | 244 | /** 245 | * Base render method. Valid high-level formats are: png, pdf, svg, raw. You 246 | * can also specify the pixel structure of raw images: argb32 (default), rgb24, 247 | * a8, a1, rgb16_565, and rgb30 (only enabled for Cairo >= 1.12). You can read 248 | * more about the low-level pixel formats in the [Cairo Documentation]{@link 249 | * http://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t}. 250 | * 251 | * If the element property is given, only that subelement is rendered. 252 | * 253 | * The PNG format is the slowest of them all, since it takes time to encode the 254 | * image as a PNG buffer. 255 | * 256 | * @param {Object} [options] - Rendering options. 257 | * @param {string} [options.format] - One of the formats listed above. 258 | * @param {number} [options.width] - Output image width, should be an integer. 259 | * @param {number} [options.height] - Output image height, should be an integer. 260 | * @param {string} [options.id] - Subelement to render. 261 | * @returns {{data: Buffer, format: string, width: number, height: number}} 262 | */ 263 | Rsvg.prototype.render = function(options) { 264 | if (arguments.length > 1 || typeof(options) !== 'object') { 265 | return this._renderArgs.apply(this, arguments); 266 | } 267 | 268 | options = options || {}; 269 | 270 | return this.handle.render( 271 | options.width, 272 | options.height, 273 | options.format, 274 | options.id 275 | ); 276 | }; 277 | 278 | /** 279 | * @deprecated since version 2.0 280 | * @private 281 | */ 282 | Rsvg.prototype._renderArgs = util.deprecate(function(width, height, format, id) { 283 | return this.handle.render(width, height, format ? format.toLowerCase() : null, id); 284 | }, 'Rsvg#render(): Call render({ format, width, height, ... }) instead.'); 285 | 286 | /** 287 | * Render the SVG as a raw memory buffer image. This can be used to create an 288 | * image that is imported into other image libraries. This render method is 289 | * usually very fast. 290 | * 291 | * The pixel format is ARGB and each pixel is 4 bytes, ie. the buffer size is 292 | * width*height*4. There are no memory "spaces" between rows in the image, like 293 | * there can be when calling the base render method with pixel formats like A8. 294 | * 295 | * @deprecated since version 2.0 296 | * @param {number} width - Output image width, should be an integer. 297 | * @param {number} height - Output image height, should be an integer. 298 | * @param {string} [id] - Subelement to render. 299 | * @returns {{data: Buffer, format: string, pixelFormat: string, width: number, height: number}} 300 | */ 301 | Rsvg.prototype.renderRaw = util.deprecate(function(width, height, id) { 302 | return this.render({ 303 | format: 'raw', 304 | width: width, 305 | height: height, 306 | element: id 307 | }); 308 | }, 'Rsvg#renderRaw(): Call render({ format: "raw" }) instead.'); 309 | 310 | /** 311 | * Render the SVG as a PNG image. 312 | * 313 | * @deprecated since version 2.0 314 | * @param {number} width - Output image width, should be an integer. 315 | * @param {number} height - Output image height, should be an integer. 316 | * @param {string} [id] - Subelement to render. 317 | * @returns {{data: Buffer, format: string, width: number, height: number}} 318 | */ 319 | Rsvg.prototype.renderPNG = util.deprecate(function(width, height, id) { 320 | return this.render({ 321 | format: 'png', 322 | width: width, 323 | height: height, 324 | element: id 325 | }); 326 | }, 'Rsvg#renderPNG(): Call render({ format: "png" }) instead.'); 327 | 328 | /** 329 | * Render the SVG as a PDF document. 330 | * 331 | * @deprecated since version 2.0 332 | * @param {number} width - Output document width, should be an integer. 333 | * @param {number} height - Output document height, should be an integer. 334 | * @param {string} [id] - Subelement to render. 335 | * @returns {{data: Buffer, format: string, width: number, height: number}} 336 | */ 337 | Rsvg.prototype.renderPDF = util.deprecate(function(width, height, id) { 338 | return this.render({ 339 | format: 'pdf', 340 | width: width, 341 | height: height, 342 | element: id 343 | }); 344 | }, 'Rsvg#renderPDF(): Call render({ format: "pdf" }) instead.'); 345 | 346 | /** 347 | * Render the SVG as an SVG. This seems superfluous, but it can be used to 348 | * normalize the input SVG. However you can not be sure that the resulting SVG 349 | * file is smaller than the input. It's not a SVG compression engine. You can 350 | * be sure that the output SVG follows a more stringent structure. 351 | * 352 | * @deprecated since version 2.0 353 | * @param {number} width - Output document width, should be an integer. 354 | * @param {number} height - Output document height, should be an integer. 355 | * @param {string} [id] - Subelement to render. 356 | * @returns {{data: string, format: string, width: number, height: number}} 357 | */ 358 | Rsvg.prototype.renderSVG = util.deprecate(function(width, height, id) { 359 | return this.render({ 360 | format: 'svg', 361 | width: width, 362 | height: height, 363 | element: id 364 | }); 365 | }, 'Rsvg#renderSVG(): Call render({ format: "svg" }) instead.'); 366 | 367 | /** 368 | * String representation of this SVG render object. 369 | * @returns {string} 370 | */ 371 | Rsvg.prototype.toString = function() { 372 | var obj = {}; 373 | 374 | var baseURI = this.baseURI; 375 | if (baseURI) { 376 | obj.baseURI = baseURI; 377 | } 378 | 379 | obj.width = this.width; 380 | obj.height = this.height; 381 | 382 | return '{ [' + this.constructor.name + ']' + util.inspect(obj).slice(1); 383 | }; 384 | 385 | // Export the Rsvg object. 386 | exports.Rsvg = Rsvg; 387 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Bjarke Walling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsvg", 3 | "version": "0.2.5", 4 | "description": "Parse SVG files and render them as PNG, PDF, SVG, or raw memory buffer images.", 5 | "keywords": [ 6 | "svg", "render", "renders", "rendering", "graphic", 7 | "graphics", "image", "images", "librsvg", "cairo" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/walling/node-rsvg.git" 12 | }, 13 | "scripts": { 14 | "test": "grunt test" 15 | }, 16 | "devDependencies": { 17 | "chai": "~1.8.1", 18 | "grunt": "~0.4.1", 19 | "grunt-contrib-jshint": "~0.7.1", 20 | "grunt-mocha-test": "~0.7.0", 21 | "sinon": "~1.7.3", 22 | "sinon-chai": "~2.4.0" 23 | }, 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Node.JS Binding for LibRSVG 2 | 3 | **LibRSVG** is a SVG rendering library, which parses SVG files and renders them in various formats. The formats include: 4 | 5 | * PNG 6 | * PDF 7 | * SVG 8 | * Raw memory buffer image 9 | 10 | [![Build Status](https://travis-ci.org/walling/node-rsvg.png?branch=master)](https://travis-ci.org/walling/node-rsvg) 11 | 12 | 13 | ## Basic Usage 14 | 15 | Here is a simple example. Look in `index.js` for more documentation. 16 | 17 | ```javascript 18 | var Rsvg = require('rsvg').Rsvg; 19 | var fs = require('fs'); 20 | 21 | // Create SVG render instance. 22 | var svg = new Rsvg(); 23 | 24 | // When finishing reading SVG, render and save as PNG image. 25 | svg.on('finish', function() { 26 | console.log('SVG width: ' + svg.width); 27 | console.log('SVG height: ' + svg.height); 28 | fs.writeFile('tiger.png', svg.render({ 29 | format: 'png', 30 | width: 600, 31 | height: 400 32 | }).data); 33 | }); 34 | 35 | // Stream SVG file into render instance. 36 | fs.createReadStream('tiger.svg').pipe(svg); 37 | ``` 38 | 39 | 40 | ## Installation 41 | 42 | First install the LibRSVG library and header files. Usually you have to look for a *development* package version. You must also have a functioning build tool chain including `pkg-config`. You can find instructions for different operating systems below. After that, you simply run: 43 | 44 | ```bash 45 | npm install rsvg 46 | ``` 47 | 48 | Library versions known to work: 49 | 50 | * LibRSVG 2.26+ 51 | * Cairo 1.8.8+ 52 | 53 | #### Ubuntu: 54 | 55 | ```bash 56 | sudo apt-get install librsvg2-dev 57 | ``` 58 | 59 | #### RedHat / OpenSUSE: 60 | 61 | ```bash 62 | sudo yum install librsvg2-devel 63 | ``` 64 | 65 | #### Mac OS X: 66 | 67 | ```bash 68 | brew install librsvg 69 | ``` 70 | 71 | If, after installing LibRSVG through homebrew you are experiencing issues installing this module, try manually exporting the package config with this command: 72 | 73 | ```bash 74 | export PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig 75 | ``` 76 | 77 | Then try reinstalling this module. For further information, [see this thread](https://github.com/Homebrew/homebrew/issues/14123). 78 | 79 | #### Windows: 80 | 81 | N/A; pull requests are accepted! 82 | -------------------------------------------------------------------------------- /src/Autocrop.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "Rsvg.h" 3 | #include "RsvgCairo.h" 4 | #include 5 | #include 6 | 7 | using namespace v8; 8 | 9 | struct autocrop_region_t { 10 | double top; 11 | double bottom; 12 | double left; 13 | double right; 14 | }; 15 | 16 | static inline uint32_t pixel(uint8_t* data, int stride, int x, int y) { 17 | return *reinterpret_cast(data + stride * y + x * 4); 18 | } 19 | 20 | const uint32_t INVALID_COLOR = 0x00DEAD00; 21 | static uint32_t areaColor(uint8_t* data, int stride, int x0, int x1, int y0, int y1) { 22 | uint32_t color = pixel(data, stride, x0, y0); 23 | for (int x = x0; x <= x1; x++) { 24 | for (int y = y0; y <= y1; y++) { 25 | uint32_t current = pixel(data, stride, x, y); 26 | if (current != color) { 27 | return INVALID_COLOR; 28 | } 29 | } 30 | } 31 | return color; 32 | } 33 | 34 | static uint32_t findEdge(uint8_t* data, int stride, int width, int height, int xd, int yd) { 35 | int x0 = 0; 36 | int x1 = width - 1; 37 | int y0 = 0; 38 | int y1 = height - 1; 39 | 40 | if (xd > 0) { 41 | x1 = x0; 42 | } else if (xd < 0) { 43 | x0 = x1; 44 | } else if (yd > 0) { 45 | y1 = y0; 46 | } else if (yd < 0) { 47 | y0 = y1; 48 | } else { 49 | return 0; 50 | } 51 | 52 | x0 -= xd; 53 | x1 -= xd; 54 | y0 -= yd; 55 | y1 -= yd; 56 | uint32_t edge = INVALID_COLOR; 57 | while (true) { 58 | x0 += xd; 59 | x1 += xd; 60 | y0 += yd; 61 | y1 += yd; 62 | if (x0 < 0 || x1 < 0 || y0 < 0 || y1 < 0 || 63 | x0 >= width || x1 >= width || y0 >= height || y1 >= height) { 64 | x0 -= xd; 65 | x1 -= xd; 66 | y0 -= yd; 67 | y1 -= yd; 68 | break; 69 | } 70 | uint32_t color = areaColor(data, stride, x0, x1, y0, y1); 71 | if (edge == INVALID_COLOR) { 72 | edge = color; 73 | } 74 | if (color == INVALID_COLOR || color != edge) break; 75 | } 76 | 77 | return (xd != 0) ? x0 : y0; 78 | } 79 | 80 | static bool AutocropRecursive(RsvgHandle* handle, autocrop_region_t* region, int direction) { 81 | const int width = 100; 82 | const int height = 100; 83 | 84 | if (region->bottom - region->top < 0.0001 || 85 | region->right - region->left < 0.0001) { 86 | return true; 87 | } 88 | 89 | cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); 90 | cairo_t* cr = cairo_create(surface); 91 | 92 | cairo_scale(cr, width / (region->right - region->left), height / (region->bottom - region->top)); 93 | cairo_translate(cr, -region->left, -region->top); 94 | 95 | gboolean success = rsvg_handle_render_cairo(handle, cr); 96 | cairo_surface_flush(surface); 97 | 98 | cairo_status_t status = cairo_status(cr); 99 | if (status || !success) { 100 | cairo_destroy(cr); 101 | cairo_surface_destroy(surface); 102 | 103 | ThrowException(Exception::Error(String::New( 104 | status ? cairo_status_to_string(status) : "Failed to render image." 105 | ))); 106 | return false; 107 | } 108 | 109 | uint8_t* data = cairo_image_surface_get_data(surface); 110 | int stride = cairo_image_surface_get_stride(surface); 111 | 112 | if (areaColor(data, stride, 0, width - 1, 0, height - 1) != INVALID_COLOR) { 113 | cairo_destroy(cr); 114 | cairo_surface_destroy(surface); 115 | return true; 116 | } 117 | 118 | autocrop_region_t sub; 119 | 120 | if (direction == 1) { 121 | int top = findEdge(data, stride, width, height, 0, 1); 122 | sub.top = top; 123 | sub.bottom = top + 1; 124 | sub.left = 0; 125 | sub.right = width; 126 | cairo_device_to_user(cr, &sub.left, &sub.top); 127 | cairo_device_to_user(cr, &sub.right, &sub.bottom); 128 | success = AutocropRecursive(handle, &sub, direction); 129 | region->top = sub.top; 130 | } else if (direction == 2) { 131 | int bottom = findEdge(data, stride, width, height, 0, -1) + 1; 132 | sub.top = bottom - 1; 133 | sub.bottom = bottom; 134 | sub.left = 0; 135 | sub.right = width; 136 | cairo_device_to_user(cr, &sub.left, &sub.top); 137 | cairo_device_to_user(cr, &sub.right, &sub.bottom); 138 | success = AutocropRecursive(handle, &sub, direction); 139 | region->bottom = sub.bottom; 140 | } else if (direction == 3) { 141 | int left = findEdge(data, stride, width, height, 1, 0); 142 | sub.top = 0; 143 | sub.bottom = height; 144 | sub.left = left; 145 | sub.right = left + 1; 146 | cairo_device_to_user(cr, &sub.left, &sub.top); 147 | cairo_device_to_user(cr, &sub.right, &sub.bottom); 148 | success = AutocropRecursive(handle, &sub, direction); 149 | region->left = sub.left; 150 | } else if (direction == 4) { 151 | int right = findEdge(data, stride, width, height, -1, 0) + 1; 152 | sub.top = 0; 153 | sub.bottom = height; 154 | sub.left = right - 1; 155 | sub.right = right; 156 | cairo_device_to_user(cr, &sub.left, &sub.top); 157 | cairo_device_to_user(cr, &sub.right, &sub.bottom); 158 | success = AutocropRecursive(handle, &sub, direction); 159 | region->right = sub.right; 160 | } else { 161 | success = false; 162 | } 163 | 164 | cairo_destroy(cr); 165 | cairo_surface_destroy(surface); 166 | return success; 167 | } 168 | 169 | Handle Rsvg::Autocrop(const Arguments& args) { 170 | HandleScope scope; 171 | Rsvg* obj = node::ObjectWrap::Unwrap(args.This()); 172 | 173 | RsvgDimensionData dimensions = { 0, 0, 0, 0 }; 174 | rsvg_handle_get_dimensions(obj->_handle, &dimensions); 175 | autocrop_region_t area = { 0, dimensions.height, 0, dimensions.width }; 176 | 177 | if (AutocropRecursive(obj->_handle, &area, 1) && 178 | AutocropRecursive(obj->_handle, &area, 2) && 179 | AutocropRecursive(obj->_handle, &area, 3) && 180 | AutocropRecursive(obj->_handle, &area, 4)) { 181 | Handle dimensions = ObjectTemplate::New(); 182 | dimensions->Set("x", Number::New(area.left)); 183 | dimensions->Set("y", Number::New(area.top)); 184 | dimensions->Set("width", Number::New(area.right - area.left)); 185 | dimensions->Set("height", Number::New(area.bottom - area.top)); 186 | return scope.Close(dimensions->NewInstance()); 187 | } else { 188 | return scope.Close(Undefined()); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Enums.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "Enums.h" 3 | #include "RsvgCairo.h" 4 | #include 5 | 6 | // Support for old Cairo 1.8.8. 7 | #ifndef CAIRO_FORMAT_INVALID 8 | #define CAIRO_FORMAT_INVALID ((cairo_format_t) -1) 9 | #endif 10 | 11 | using namespace v8; 12 | 13 | render_format_t RenderFormatFromString(const char* formatString) { 14 | if (!formatString) { 15 | return RENDER_FORMAT_INVALID; 16 | } else if (std::strcmp(formatString, "raw") == 0) { 17 | return RENDER_FORMAT_RAW; 18 | } else if (std::strcmp(formatString, "png") == 0) { 19 | return RENDER_FORMAT_PNG; 20 | } else if (std::strcmp(formatString, "jpeg") == 0) { 21 | return RENDER_FORMAT_JPEG; 22 | } else if (std::strcmp(formatString, "pdf") == 0) { 23 | return RENDER_FORMAT_PDF; 24 | } else if (std::strcmp(formatString, "svg") == 0) { 25 | return RENDER_FORMAT_SVG; 26 | } else if (std::strcmp(formatString, "vips") == 0) { 27 | return RENDER_FORMAT_VIPS; 28 | } else { 29 | return RENDER_FORMAT_INVALID; 30 | } 31 | } 32 | 33 | Handle RenderFormatToString(render_format_t format) { 34 | const char* formatString = 35 | format == RENDER_FORMAT_RAW ? "raw" : 36 | format == RENDER_FORMAT_PNG ? "png" : 37 | format == RENDER_FORMAT_JPEG ? "jpeg" : 38 | format == RENDER_FORMAT_PDF ? "pdf" : 39 | format == RENDER_FORMAT_SVG ? "svg" : 40 | format == RENDER_FORMAT_VIPS ? "vips" : 41 | NULL; 42 | 43 | return formatString ? String::New(formatString) : Null(); 44 | } 45 | 46 | cairo_format_t CairoFormatFromString(const char* formatString) { 47 | if (!formatString) { 48 | return CAIRO_FORMAT_INVALID; 49 | } else if (std::strcmp(formatString, "argb32") == 0) { 50 | return CAIRO_FORMAT_ARGB32; 51 | } else if (std::strcmp(formatString, "rgb24") == 0) { 52 | return CAIRO_FORMAT_RGB24; 53 | } else if (std::strcmp(formatString, "a8") == 0) { 54 | return CAIRO_FORMAT_A8; 55 | } else if (std::strcmp(formatString, "a1") == 0) { 56 | return CAIRO_FORMAT_A1; 57 | } else if (std::strcmp(formatString, "rgb16_565") == 0) { 58 | return (cairo_format_t) CAIRO_FORMAT_RGB16_565; 59 | #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 12, 0) 60 | } else if (std::strcmp(formatString, "rgb30") == 0) { 61 | return CAIRO_FORMAT_RGB30; 62 | #endif 63 | } else { 64 | return CAIRO_FORMAT_INVALID; 65 | } 66 | } 67 | 68 | Handle CairoFormatToString(cairo_format_t format) { 69 | const char* formatString = 70 | format == CAIRO_FORMAT_ARGB32 ? "argb32" : 71 | format == CAIRO_FORMAT_RGB24 ? "rgb24" : 72 | format == CAIRO_FORMAT_A8 ? "a8" : 73 | format == CAIRO_FORMAT_A1 ? "a1" : 74 | format == CAIRO_FORMAT_RGB16_565 ? "rgb16_565" : 75 | #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 12, 0) 76 | format == CAIRO_FORMAT_RGB30 ? "rgb30" : 77 | #endif 78 | NULL; 79 | 80 | return formatString ? String::New(formatString) : Null(); 81 | } 82 | -------------------------------------------------------------------------------- /src/Enums.h: -------------------------------------------------------------------------------- 1 | #ifndef __ENUMS_H__ 2 | #define __ENUMS_H__ 3 | 4 | #include 5 | #include 6 | 7 | typedef enum { 8 | RENDER_FORMAT_INVALID = -1, 9 | RENDER_FORMAT_RAW = 0, 10 | RENDER_FORMAT_PNG = 1, 11 | RENDER_FORMAT_JPEG = 2, 12 | RENDER_FORMAT_PDF = 3, 13 | RENDER_FORMAT_SVG = 4, 14 | RENDER_FORMAT_VIPS = 5 15 | } render_format_t; 16 | 17 | render_format_t RenderFormatFromString(const char* formatString); 18 | v8::Handle RenderFormatToString(render_format_t format); 19 | cairo_format_t CairoFormatFromString(const char* formatString); 20 | v8::Handle CairoFormatToString(cairo_format_t format); 21 | 22 | #endif /*__ENUMS_H__*/ -------------------------------------------------------------------------------- /src/Rsvg.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "Rsvg.h" 3 | #include "RsvgCairo.h" 4 | #include "Enums.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace v8; 12 | using namespace node; 13 | 14 | cairo_status_t GetDataChunks(void* closure, const unsigned char* chunk, unsigned int length) { 15 | std::string* data = reinterpret_cast(closure); 16 | data->append(reinterpret_cast(chunk), length); 17 | return CAIRO_STATUS_SUCCESS; 18 | } 19 | 20 | Persistent Rsvg::constructor; 21 | 22 | Rsvg::Rsvg(RsvgHandle* const handle) : _handle(handle) {} 23 | 24 | Rsvg::~Rsvg() { 25 | g_object_unref(G_OBJECT(_handle)); 26 | } 27 | 28 | void Rsvg::Init(Handle exports) { 29 | 30 | #if !GLIB_CHECK_VERSION(2, 36, 0) 31 | // Initialize GObject types. 32 | g_type_init(); 33 | #endif 34 | 35 | // Prepare constructor template. 36 | Local tpl = FunctionTemplate::New(New); 37 | tpl->SetClassName(String::NewSymbol("Rsvg")); 38 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 39 | // Add methods to prototype. 40 | Local prototype = tpl->PrototypeTemplate(); 41 | prototype->Set("getBaseURI", FunctionTemplate::New(GetBaseURI)->GetFunction()); 42 | prototype->Set("setBaseURI", FunctionTemplate::New(SetBaseURI)->GetFunction()); 43 | prototype->Set("getDPI", FunctionTemplate::New(GetDPI)->GetFunction()); 44 | prototype->Set("setDPI", FunctionTemplate::New(SetDPI)->GetFunction()); 45 | prototype->Set("getDPIX", FunctionTemplate::New(GetDPIX)->GetFunction()); 46 | prototype->Set("setDPIX", FunctionTemplate::New(SetDPIX)->GetFunction()); 47 | prototype->Set("getDPIY", FunctionTemplate::New(GetDPIY)->GetFunction()); 48 | prototype->Set("setDPIY", FunctionTemplate::New(SetDPIY)->GetFunction()); 49 | prototype->Set("getWidth", FunctionTemplate::New(GetWidth)->GetFunction()); 50 | prototype->Set("getHeight", FunctionTemplate::New(GetHeight)->GetFunction()); 51 | prototype->Set("write", FunctionTemplate::New(Write)->GetFunction()); 52 | prototype->Set("close", FunctionTemplate::New(Close)->GetFunction()); 53 | prototype->Set("dimensions", FunctionTemplate::New(Dimensions)->GetFunction()); 54 | prototype->Set("hasElement", FunctionTemplate::New(HasElement)->GetFunction()); 55 | prototype->Set("autocrop", FunctionTemplate::New(Autocrop)->GetFunction()); 56 | prototype->Set("render", FunctionTemplate::New(Render)->GetFunction()); 57 | // Export class. 58 | constructor = Persistent::New(tpl->GetFunction()); 59 | exports->Set(String::New("Rsvg"), constructor); 60 | } 61 | 62 | Handle Rsvg::New(const Arguments& args) { 63 | HandleScope scope; 64 | 65 | if (args.IsConstructCall()) { 66 | // Invoked as constructor: `new Rsvg(...)` 67 | RsvgHandle* handle; 68 | if (Buffer::HasInstance(args[0])) { 69 | const guint8* buffer = 70 | reinterpret_cast(Buffer::Data(args[0])); 71 | gsize length = Buffer::Length(args[0]); 72 | 73 | GError* error = NULL; 74 | handle = rsvg_handle_new_from_data(buffer, length, &error); 75 | 76 | if (error) { 77 | ThrowException(Exception::Error(String::New(error->message))); 78 | g_error_free(error); 79 | return scope.Close(Undefined()); 80 | } 81 | } else { 82 | handle = rsvg_handle_new(); 83 | } 84 | // Error handling. 85 | if (!handle) { 86 | ThrowException(Exception::Error(String::New( 87 | "Unable to create RsvgHandle instance." 88 | ))); 89 | return scope.Close(Undefined()); 90 | } 91 | // Create object. 92 | Rsvg* obj = new Rsvg(handle); 93 | obj->Wrap(args.This()); 94 | return scope.Close(args.This()); 95 | } else { 96 | // Invoked as plain function `Rsvg(...)`, turn into construct call. 97 | const int argc = 1; 98 | Local argv[argc] = { args[0] }; 99 | return scope.Close(constructor->NewInstance(argc, argv)); 100 | } 101 | } 102 | 103 | Handle Rsvg::GetBaseURI(const Arguments& args) { 104 | return GetStringProperty(args, "base-uri"); 105 | } 106 | 107 | Handle Rsvg::SetBaseURI(const Arguments& args) { 108 | return SetStringProperty(args, "base-uri"); 109 | } 110 | 111 | Handle Rsvg::GetDPI(const Arguments& args) { 112 | HandleScope scope; 113 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 114 | gdouble dpiX = 0; 115 | gdouble dpiY = 0; 116 | g_object_get( 117 | G_OBJECT(obj->_handle), 118 | "dpi-x", &dpiX, 119 | "dpi-y", &dpiY, 120 | NULL 121 | ); 122 | 123 | Handle dpi = ObjectTemplate::New(); 124 | dpi->Set("x", Number::New(dpiX)); 125 | dpi->Set("y", Number::New(dpiY)); 126 | 127 | return scope.Close(dpi->NewInstance()); 128 | } 129 | 130 | Handle Rsvg::SetDPI(const Arguments& args) { 131 | HandleScope scope; 132 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 133 | 134 | gdouble x = args[0]->NumberValue(); 135 | if (std::isnan(x)) { 136 | x = 0; 137 | } 138 | 139 | gdouble y = x; 140 | if (args[1]->IsNumber()) { 141 | y = args[1]->NumberValue(); 142 | if (std::isnan(y)) { 143 | y = 0; 144 | } 145 | } 146 | 147 | rsvg_handle_set_dpi_x_y(obj->_handle, x, y); 148 | return scope.Close(Undefined()); 149 | } 150 | 151 | Handle Rsvg::GetDPIX(const Arguments& args) { 152 | return GetNumberProperty(args, "dpi-x"); 153 | } 154 | 155 | Handle Rsvg::SetDPIX(const Arguments& args) { 156 | return SetNumberProperty(args, "dpi-x"); 157 | } 158 | 159 | Handle Rsvg::GetDPIY(const Arguments& args) { 160 | return GetNumberProperty(args, "dpi-y"); 161 | } 162 | 163 | Handle Rsvg::SetDPIY(const Arguments& args) { 164 | return SetNumberProperty(args, "dpi-y"); 165 | } 166 | 167 | Handle Rsvg::GetWidth(const Arguments& args) { 168 | return GetIntegerProperty(args, "width"); 169 | } 170 | 171 | Handle Rsvg::GetHeight(const Arguments& args) { 172 | return GetIntegerProperty(args, "height"); 173 | } 174 | 175 | Handle Rsvg::Write(const Arguments& args) { 176 | HandleScope scope; 177 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 178 | if (Buffer::HasInstance(args[0])) { 179 | const guchar* buffer = 180 | reinterpret_cast(Buffer::Data(args[0])); 181 | gsize length = Buffer::Length(args[0]); 182 | 183 | GError* error = NULL; 184 | gboolean success = rsvg_handle_write(obj->_handle, buffer, length, &error); 185 | 186 | if (error) { 187 | ThrowException(Exception::Error(String::New(error->message))); 188 | g_error_free(error); 189 | } else if (!success) { 190 | ThrowException(Exception::Error(String::New("Failed to write data."))); 191 | } 192 | } else { 193 | ThrowException(Exception::TypeError(String::New("Invalid argument: buffer"))); 194 | } 195 | return scope.Close(Undefined()); 196 | } 197 | 198 | Handle Rsvg::Close(const Arguments& args) { 199 | HandleScope scope; 200 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 201 | 202 | GError* error = NULL; 203 | gboolean success = rsvg_handle_close(obj->_handle, &error); 204 | 205 | if (error) { 206 | ThrowException(Exception::Error(String::New(error->message))); 207 | g_error_free(error); 208 | } else if (!success) { 209 | ThrowException(Exception::Error(String::New("Failed to close."))); 210 | } 211 | return scope.Close(Undefined()); 212 | } 213 | 214 | Handle Rsvg::Dimensions(const Arguments& args) { 215 | HandleScope scope; 216 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 217 | 218 | const char* id = NULL; 219 | String::Utf8Value idArg(args[0]); 220 | if (!(args[0]->IsUndefined() || args[0]->IsNull())) { 221 | id = *idArg; 222 | if (!id) { 223 | ThrowException(Exception::TypeError(String::New("Invalid argument: id"))); 224 | return scope.Close(Undefined()); 225 | } 226 | } 227 | 228 | RsvgPositionData _position = { 0, 0 }; 229 | RsvgDimensionData _dimensions = { 0, 0, 0, 0 }; 230 | 231 | gboolean hasPosition = rsvg_handle_get_position_sub(obj->_handle, &_position, id); 232 | gboolean hasDimensions = rsvg_handle_get_dimensions_sub(obj->_handle, &_dimensions, id); 233 | 234 | if (hasPosition || hasDimensions) { 235 | Handle dimensions = ObjectTemplate::New(); 236 | if (hasPosition) { 237 | dimensions->Set("x", Integer::New(_position.x)); 238 | dimensions->Set("y", Integer::New(_position.y)); 239 | } 240 | if (hasDimensions) { 241 | dimensions->Set("width", Integer::New(_dimensions.width)); 242 | dimensions->Set("height", Integer::New(_dimensions.height)); 243 | } 244 | return scope.Close(dimensions->NewInstance()); 245 | } else { 246 | return scope.Close(Null()); 247 | } 248 | } 249 | 250 | Handle Rsvg::HasElement(const Arguments& args) { 251 | HandleScope scope; 252 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 253 | 254 | const char* id = NULL; 255 | String::Utf8Value idArg(args[0]); 256 | if (!(args[0]->IsUndefined() || args[0]->IsNull())) { 257 | id = *idArg; 258 | if (!id) { 259 | ThrowException(Exception::TypeError(String::New("Invalid argument: id"))); 260 | return scope.Close(Undefined()); 261 | } 262 | } 263 | 264 | gboolean exists = rsvg_handle_has_sub(obj->_handle, id); 265 | return scope.Close(Boolean::New(exists)); 266 | } 267 | 268 | Handle Rsvg::Render(const Arguments& args) { 269 | HandleScope scope; 270 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 271 | 272 | int width = args[0]->Int32Value(); 273 | int height = args[1]->Int32Value(); 274 | 275 | if (width <= 0) { 276 | ThrowException(Exception::RangeError(String::New("Expected width > 0."))); 277 | return scope.Close(Undefined()); 278 | } 279 | if (height <= 0) { 280 | ThrowException(Exception::RangeError(String::New("Expected height > 0."))); 281 | return scope.Close(Undefined()); 282 | } 283 | 284 | String::Utf8Value formatArg(args[2]); 285 | const char* formatString = *formatArg; 286 | render_format_t renderFormat = RenderFormatFromString(formatString); 287 | cairo_format_t pixelFormat = CAIRO_FORMAT_INVALID; 288 | if (renderFormat == RENDER_FORMAT_RAW || 289 | renderFormat == RENDER_FORMAT_PNG) { 290 | pixelFormat = CAIRO_FORMAT_ARGB32; 291 | } else if (renderFormat == RENDER_FORMAT_JPEG) { 292 | ThrowException(Exception::Error(String::New("Format not supported: JPEG"))); 293 | return scope.Close(Undefined()); 294 | } else if ( 295 | renderFormat == RENDER_FORMAT_SVG || 296 | renderFormat == RENDER_FORMAT_PDF) { 297 | pixelFormat = CAIRO_FORMAT_INVALID; 298 | } else if (renderFormat == RENDER_FORMAT_VIPS) { 299 | ThrowException(Exception::Error(String::New("Format not supported: VIPS"))); 300 | return scope.Close(Undefined()); 301 | } else { 302 | renderFormat = RENDER_FORMAT_RAW; 303 | pixelFormat = CairoFormatFromString(formatString); 304 | if (pixelFormat == CAIRO_FORMAT_INVALID) { 305 | ThrowException(Exception::RangeError(String::New("Invalid argument: format"))); 306 | return scope.Close(Undefined()); 307 | } 308 | } 309 | 310 | const char* id = NULL; 311 | String::Utf8Value idArg(args[3]); 312 | if (!(args[3]->IsUndefined() || args[3]->IsNull())) { 313 | id = *idArg; 314 | if (!id) { 315 | ThrowException(Exception::TypeError(String::New("Invalid argument: id"))); 316 | return scope.Close(Undefined()); 317 | } 318 | if (!rsvg_handle_has_sub(obj->_handle, id)) { 319 | ThrowException(Exception::RangeError(String::New( 320 | "SVG element with given id does not exists." 321 | ))); 322 | return scope.Close(Undefined()); 323 | } 324 | } 325 | 326 | RsvgPositionData position = { 0, 0 }; 327 | RsvgDimensionData dimensions = { 0, 0, 0, 0 }; 328 | 329 | if (!rsvg_handle_get_position_sub(obj->_handle, &position, id)) { 330 | ThrowException(Exception::Error(String::New( 331 | "Could not get position of SVG element with given id." 332 | ))); 333 | return scope.Close(Undefined()); 334 | } 335 | 336 | if (!rsvg_handle_get_dimensions_sub(obj->_handle, &dimensions, id)) { 337 | ThrowException(Exception::Error(String::New( 338 | "Could not get dimensions of SVG element or whole image." 339 | ))); 340 | return scope.Close(Undefined()); 341 | } 342 | if (dimensions.width <= 0 || dimensions.height <= 0) { 343 | ThrowException(Exception::Error(String::New( 344 | "Got invalid dimensions of SVG element or whole image." 345 | ))); 346 | return scope.Close(Undefined()); 347 | } 348 | 349 | std::string data; 350 | cairo_surface_t* surface; 351 | 352 | if (renderFormat == RENDER_FORMAT_SVG) { 353 | surface = cairo_svg_surface_create_for_stream(GetDataChunks, &data, width, height); 354 | cairo_svg_surface_restrict_to_version(surface, CAIRO_SVG_VERSION_1_1); 355 | } else if (renderFormat == RENDER_FORMAT_PDF) { 356 | surface = cairo_pdf_surface_create_for_stream(GetDataChunks, &data, width, height); 357 | #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 10, 0) 358 | cairo_pdf_surface_restrict_to_version(surface, CAIRO_PDF_VERSION_1_4); 359 | #endif 360 | } else { 361 | surface = cairo_image_surface_create(pixelFormat, width, height); 362 | } 363 | 364 | cairo_t* cr = cairo_create(surface); 365 | // printf( 366 | // "%s: (%d, %d) %dx%d, render: %dx%d\n", 367 | // id ? id : "SVG", 368 | // position.x, 369 | // position.y, 370 | // dimensions.width, 371 | // dimensions.height, 372 | // width, 373 | // height 374 | // ); 375 | double scaleX = double(width) / double(dimensions.width); 376 | double scaleY = double(height) / double(dimensions.height); 377 | // printf("scale=%.3fx%.3f\n", scaleX, scaleY); 378 | double scale = MIN(scaleX, scaleY); 379 | int bboxWidth = round(scale * dimensions.width); 380 | int bboxHeight = round(scale * dimensions.height); 381 | int bboxX = (width - bboxWidth) / 2; 382 | int bboxY = (height - bboxHeight) / 2; 383 | // printf("bbox=(%d, %d) %dx%d\n", bboxX, bboxY, bboxWidth, bboxHeight); 384 | cairo_translate(cr, bboxX, bboxY); 385 | cairo_scale(cr, scale, scale); 386 | cairo_translate(cr, -position.x, -position.y); 387 | 388 | gboolean success; 389 | if (id) { 390 | success = rsvg_handle_render_cairo_sub(obj->_handle, cr, id); 391 | } else { 392 | success = rsvg_handle_render_cairo(obj->_handle, cr); 393 | } 394 | cairo_surface_flush(surface); 395 | 396 | cairo_status_t status = cairo_status(cr); 397 | if (status || !success) { 398 | cairo_destroy(cr); 399 | cairo_surface_destroy(surface); 400 | 401 | ThrowException(Exception::Error(String::New( 402 | status ? cairo_status_to_string(status) : "Failed to render image." 403 | ))); 404 | return scope.Close(Undefined()); 405 | } 406 | 407 | int stride = -1; 408 | if (renderFormat == RENDER_FORMAT_RAW) { 409 | stride = cairo_image_surface_get_stride(surface); 410 | data.append( 411 | reinterpret_cast(cairo_image_surface_get_data(surface)), 412 | stride * height 413 | ); 414 | } else if (renderFormat == RENDER_FORMAT_PNG) { 415 | cairo_surface_write_to_png_stream(surface, GetDataChunks, &data); 416 | } 417 | 418 | cairo_destroy(cr); 419 | cairo_surface_destroy(surface); 420 | 421 | if (renderFormat == RENDER_FORMAT_RAW && 422 | pixelFormat == CAIRO_FORMAT_ARGB32 && 423 | stride != width * 4) { 424 | ThrowException(Exception::Error(String::New( 425 | "Rendered with invalid stride (byte size of row) for ARGB32 format." 426 | ))); 427 | return scope.Close(Undefined()); 428 | } 429 | 430 | Handle image = ObjectTemplate::New(); 431 | if (renderFormat == RENDER_FORMAT_SVG) { 432 | image->Set("data", String::New(data.c_str())); 433 | } else { 434 | image->Set("data", Buffer::New(data.c_str(), data.length())->handle_); 435 | } 436 | 437 | image->Set("format", RenderFormatToString(renderFormat)); 438 | if (pixelFormat != CAIRO_FORMAT_INVALID) { 439 | image->Set("pixelFormat", CairoFormatToString(pixelFormat)); 440 | } 441 | image->Set("width", Integer::New(width)); 442 | image->Set("height", Integer::New(height)); 443 | if (stride != -1) { 444 | image->Set("stride", Integer::New(stride)); 445 | } 446 | return scope.Close(image->NewInstance()); 447 | } 448 | 449 | Handle Rsvg::GetStringProperty(const Arguments& args, const char* property) { 450 | HandleScope scope; 451 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 452 | gchar* value = NULL; 453 | g_object_get(G_OBJECT(obj->_handle), property, &value, NULL); 454 | Handle result(value ? String::New(value) : Null()); 455 | if (value) { 456 | g_free(value); 457 | } 458 | return scope.Close(result); 459 | } 460 | 461 | Handle Rsvg::SetStringProperty(const Arguments& args, const char* property) { 462 | HandleScope scope; 463 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 464 | gchar* value = NULL; 465 | String::Utf8Value arg0(args[0]); 466 | if (!(args[0]->IsNull() || args[0]->IsUndefined())) { 467 | value = *arg0; 468 | } 469 | g_object_set(G_OBJECT(obj->_handle), property, value, NULL); 470 | return scope.Close(Undefined()); 471 | } 472 | 473 | Handle Rsvg::GetNumberProperty(const Arguments& args, const char* property) { 474 | HandleScope scope; 475 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 476 | gdouble value = 0; 477 | g_object_get(G_OBJECT(obj->_handle), property, &value, NULL); 478 | return scope.Close(Number::New(value)); 479 | } 480 | 481 | Handle Rsvg::SetNumberProperty(const Arguments& args, const char* property) { 482 | HandleScope scope; 483 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 484 | gdouble value = args[0]->NumberValue(); 485 | if (std::isnan(value)) { 486 | value = 0; 487 | } 488 | g_object_set(G_OBJECT(obj->_handle), property, value, NULL); 489 | return scope.Close(Undefined()); 490 | } 491 | 492 | Handle Rsvg::GetIntegerProperty(const Arguments& args, const char* property) { 493 | HandleScope scope; 494 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 495 | gint value = 0; 496 | g_object_get(G_OBJECT(obj->_handle), property, &value, NULL); 497 | return scope.Close(Integer::New(value)); 498 | } 499 | 500 | Handle Rsvg::SetIntegerProperty(const Arguments& args, const char* property) { 501 | HandleScope scope; 502 | Rsvg* obj = ObjectWrap::Unwrap(args.This()); 503 | gint value = args[0]->Int32Value(); 504 | g_object_set(G_OBJECT(obj->_handle), property, value, NULL); 505 | return scope.Close(Undefined()); 506 | } 507 | 508 | NODE_MODULE(rsvg, Rsvg::Init) 509 | -------------------------------------------------------------------------------- /src/Rsvg.h: -------------------------------------------------------------------------------- 1 | #ifndef __RSVG_H__ 2 | #define __RSVG_H__ 3 | 4 | #include 5 | #include 6 | 7 | class Rsvg : public node::ObjectWrap { 8 | public: 9 | static void Init(v8::Handle exports); 10 | 11 | private: 12 | explicit Rsvg(RsvgHandle* const handle); 13 | ~Rsvg(); 14 | 15 | static v8::Handle New(const v8::Arguments& args); 16 | static v8::Handle GetBaseURI(const v8::Arguments& args); 17 | static v8::Handle SetBaseURI(const v8::Arguments& args); 18 | static v8::Handle GetDPI(const v8::Arguments& args); 19 | static v8::Handle SetDPI(const v8::Arguments& args); 20 | static v8::Handle GetDPIX(const v8::Arguments& args); 21 | static v8::Handle SetDPIX(const v8::Arguments& args); 22 | static v8::Handle GetDPIY(const v8::Arguments& args); 23 | static v8::Handle SetDPIY(const v8::Arguments& args); 24 | static v8::Handle GetWidth(const v8::Arguments& args); 25 | static v8::Handle GetHeight(const v8::Arguments& args); 26 | static v8::Handle Write(const v8::Arguments& args); 27 | static v8::Handle Close(const v8::Arguments& args); 28 | static v8::Handle Dimensions(const v8::Arguments& args); 29 | static v8::Handle HasElement(const v8::Arguments& args); 30 | static v8::Handle Autocrop(const v8::Arguments& args); 31 | static v8::Handle Render(const v8::Arguments& args); 32 | static v8::Handle GetStringProperty(const v8::Arguments& args, const char* property); 33 | static v8::Handle SetStringProperty(const v8::Arguments& args, const char* property); 34 | static v8::Handle GetNumberProperty(const v8::Arguments& args, const char* property); 35 | static v8::Handle SetNumberProperty(const v8::Arguments& args, const char* property); 36 | static v8::Handle GetIntegerProperty(const v8::Arguments& args, const char* property); 37 | static v8::Handle SetIntegerProperty(const v8::Arguments& args, const char* property); 38 | static v8::Persistent constructor; 39 | RsvgHandle* const _handle; 40 | }; 41 | 42 | #endif /*__RSVG_H__*/ -------------------------------------------------------------------------------- /src/RsvgCairo.h: -------------------------------------------------------------------------------- 1 | #ifndef __RSVGCAIRO_H__ 2 | #define __RSVGCAIRO_H__ 3 | 4 | #include 5 | 6 | // Hack to ignore warning message. It's deprecated to include the rsvg-cairo.h 7 | // file directly, but we need to do this in order to support older versions. 8 | #define __RSVG_RSVG_H_INSIDE__ 9 | #include 10 | #undef __RSVG_RSVG_H_INSIDE__ 11 | 12 | #endif /*__RSVGCAIRO_H__*/ -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "immed" : true, 7 | "indent" : 1, 8 | "latedef" : true, 9 | "newcap" : true, 10 | "noarg" : true, 11 | "nonew" : true, 12 | "quotmark" : true, 13 | "undef" : true, 14 | "unused" : true, 15 | "strict" : true, 16 | "trailing" : true, 17 | "maxdepth" : 4, 18 | "maxlen" : 80, 19 | "eqnull" : true, 20 | 21 | /* CHAI.JS ASSERTIONS */ 22 | "expr" : true, 23 | 24 | "globals" : { 25 | /* MOCHA.JS */ 26 | "describe" : false, 27 | "it" : false, 28 | "before" : false, 29 | "beforeEach" : false, 30 | "after" : false, 31 | "afterEach" : false, 32 | /* CHAI.JS */ 33 | "should" : false, 34 | /* RSVG CASES */ 35 | "rsvg" : false 36 | } 37 | } -------------------------------------------------------------------------------- /test/helpers/chai.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.use(require('sinon-chai')); 5 | chai.should(); 6 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Writable = require('stream').Writable; 4 | var sinon = require('sinon'); 5 | var Rsvg = require('..').Rsvg; 6 | 7 | describe('Rsvg', function() { 8 | 9 | describe('Rsvg()', function() { 10 | it('can be constructed with a SVG', function() { 11 | // Use buffer. 12 | var svg = new Rsvg(new Buffer('')); 13 | svg.should.be.an.instanceof(Rsvg); 14 | svg.width.should.equal(5); 15 | svg.height.should.equal(7); 16 | 17 | // Use string. 18 | svg = new Rsvg(''); 19 | svg.width.should.equal(3); 20 | svg.height.should.equal(8); 21 | 22 | // Not writable anymore 23 | var onerror = sinon.spy(); 24 | svg.on('error', onerror); 25 | svg.write(''); 26 | onerror.should.have.been.calledOnce; 27 | onerror.lastCall.args.should.have.length(1); 28 | onerror.lastCall.args[0].should.match(/write failure/i); 29 | }); 30 | 31 | it('can be a writable stream', function() { 32 | // Default constructor. 33 | var svg = new Rsvg(); 34 | svg.should.be.an.instanceof(Writable); 35 | svg.width.should.equal(0); 36 | svg.height.should.equal(0); 37 | 38 | // Write to stream and basic test. 39 | svg.write('').should.be.true; 40 | svg.width.should.equal(4); 41 | svg.height.should.equal(6); 42 | 43 | // Stream options. 44 | svg = new Rsvg({ highWaterMark: 16 }); 45 | svg.write('').should.be.false; 46 | }); 47 | 48 | it('emits load event when ready', function(done) { 49 | // Stream. 50 | var onload = sinon.spy(); 51 | var svg = new Rsvg(); 52 | svg.on('load', onload); 53 | svg.write(''); 54 | svg.write(''); 55 | onload.should.not.have.been.called; 56 | svg.end(); 57 | onload.should.have.been.calledOnce; 58 | onload.should.have.been.calledWithExactly(); 59 | 60 | // Constructed with SVG. 61 | onload = sinon.spy(); 62 | svg = new Rsvg(''); 63 | svg.on('load', onload); 64 | onload.should.not.have.been.called; 65 | process.nextTick(function() { 66 | onload.should.have.been.calledOnce; 67 | onload.should.have.been.calledWithExactly(); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('gives an error for invalid SVG content', function() { 73 | // Stream. 74 | var onerror = sinon.spy(); 75 | var svg = new Rsvg(); 76 | svg.on('error', onerror); 77 | svg.write('this is not a SVG file'); 78 | onerror.should.have.been.calledOnce; 79 | svg.write('this is not a SVG file'); 80 | onerror.should.have.been.calledTwice; 81 | onerror.lastCall.args.should.have.length(1); 82 | onerror.lastCall.args[0].should.match(/write failure/); 83 | 84 | // Constructed with SVG. 85 | function loadInvalidSVG() { 86 | /*jshint -W031 */ 87 | new Rsvg('this is not a SVG file'); 88 | /*jshint +W031 */ 89 | } 90 | loadInvalidSVG.should.throw(/load failure/i); 91 | 92 | // Regression test on 2014-01-16. 93 | onerror = sinon.spy(); 94 | svg = new Rsvg(); 95 | svg.on('error', onerror); 96 | svg.end('invalid'); 97 | onerror.should.have.been.calledOnce; 98 | onerror.lastCall.args.should.have.length(1); 99 | onerror.lastCall.args[0].should.match(/write failure/); 100 | }); 101 | }); 102 | 103 | describe('baseURI', function() { 104 | it('allows to reference external SVGs'); 105 | }); 106 | 107 | describe('width', function() { 108 | it('gives the horizontal size', function() { 109 | new Rsvg('').width.should.equal(314); 110 | new Rsvg('').width.should.equal(257); 111 | }); 112 | }); 113 | 114 | describe('height', function() { 115 | it('gives the vertical size', function() { 116 | new Rsvg('').height.should.equal(413); 117 | new Rsvg('').height.should.equal(752); 118 | }); 119 | }); 120 | 121 | describe('dimensions()', function() { 122 | it('gives the size of the whole image', function() { 123 | new Rsvg('').dimensions().should.deep.equal({ 124 | x: 0, 125 | y: 0, 126 | width: 314, 127 | height: 257 128 | }); 129 | new Rsvg('').dimensions().should.deep.equal({ 130 | x: 0, 131 | y: 0, 132 | width: 17, 133 | height: 19 134 | }); 135 | }); 136 | 137 | it('gives the size and position of specific elements', function() { 138 | var svg = new Rsvg(); 139 | svg.write(''); 140 | svg.write(''); 141 | svg.write(''); 142 | svg.write(''); 143 | svg.write(''); 144 | svg.end(); 145 | 146 | svg.dimensions().should.deep.equal({ 147 | x: 0, 148 | y: 0, 149 | width: 12, 150 | height: 10 151 | }); 152 | svg.dimensions(null).should.deep.equal(svg.dimensions()); 153 | 154 | svg.dimensions('#r1').should.deep.equal({ 155 | x: -2, 156 | y: 3, 157 | width: 7, 158 | height: 5 159 | }); 160 | svg.dimensions('#r2').should.deep.equal({ 161 | x: 8, 162 | y: 4, 163 | width: 4, 164 | height: 6 165 | }); 166 | svg.dimensions('#circ').should.deep.equal({ 167 | x: 5, 168 | y: 0, 169 | width: 6, 170 | height: 6 171 | }); 172 | }); 173 | }); 174 | 175 | describe('hasElement()', function() { 176 | it('determines whether an element with the given ID exists', function() { 177 | var svg = new Rsvg(); 178 | svg.write(''); 179 | svg.write(''); 180 | svg.write(''); 181 | svg.write(''); 182 | svg.write(''); 183 | svg.end(); 184 | 185 | svg.hasElement().should.be.false; 186 | svg.hasElement(null).should.be.false; 187 | svg.hasElement('#r1').should.be.true; 188 | svg.hasElement('#r2').should.be.true; 189 | svg.hasElement('#circ').should.be.true; 190 | svg.hasElement('#foo').should.be.false; 191 | svg.hasElement('r1').should.be.false; 192 | }); 193 | }); 194 | 195 | describe('autocrop()', function() { 196 | it('finds the drawing area of the SVG'); 197 | }); 198 | 199 | describe('render()', function() { 200 | it('does nothing for an empty SVG document'); 201 | it('renders as a raw memory buffer'); 202 | it('renders as a PNG image'); 203 | it('renders as a PDF document'); 204 | it('renders as a PDF document'); 205 | it('renders in various image sizes'); 206 | it('can render specific SVG elements'); 207 | it('can render a specific area [future]'); 208 | it('can trim the image to a given rect [future]'); 209 | it('can resize to fit inside box [future]'); 210 | it('can resize to fit outside box [future]'); 211 | it('can resize while ignoring aspect ratio [future]'); 212 | it('can resize with a focus point [future]'); 213 | it('can add a background color [future]'); 214 | }); 215 | 216 | describe('toString()', function() { 217 | it('gives a string representation', function() { 218 | var svg = new Rsvg(); 219 | svg.toString().should.equal('{ [Rsvg] width: 0, height: 0 }'); 220 | 221 | svg = new Rsvg(''); 222 | svg.toString().should.equal('{ [Rsvg] width: 3, height: 7 }'); 223 | }); 224 | }); 225 | 226 | }); 227 | -------------------------------------------------------------------------------- /test/old.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var Rsvg = require('..').Rsvg; 5 | 6 | describe('Rsvg (deprecated features)', function() { 7 | 8 | describe('dpiX', function() { 9 | it('is deprecated', function() { 10 | // TODO: Can it be tested? 11 | }); 12 | 13 | it('is default 90', function() { 14 | new Rsvg().dpiX.should.equal(90); 15 | }); 16 | }); 17 | 18 | describe('dpiY', function() { 19 | it('is deprecated', function() { 20 | // TODO: Can it be tested? 21 | }); 22 | 23 | it('is default 90', function() { 24 | new Rsvg().dpiY.should.equal(90); 25 | }); 26 | }); 27 | 28 | describe('getDPI()', function() { 29 | it('is deprecated', function() { 30 | new Rsvg().getDPI.name.should.equal('deprecated'); 31 | }); 32 | 33 | it('is default 90', function() { 34 | var dpi = new Rsvg().getDPI(); 35 | dpi.should.have.property('x', 90); 36 | dpi.should.have.property('y', 90); 37 | }); 38 | }); 39 | 40 | describe('setDPI()', function() { 41 | it('is deprecated', function() { 42 | new Rsvg().setDPI.name.should.equal('deprecated'); 43 | }); 44 | 45 | it('can set a single resolution', function() { 46 | var svg = new Rsvg(); 47 | svg.setDPI(300); 48 | svg.getDPI().should.deep.equal({ x: 300, y: 300 }); 49 | svg.dpiX.should.equal(300); 50 | svg.dpiY.should.equal(300); 51 | }); 52 | 53 | it('can set a distinct vertical/horizontal resolution', function() { 54 | var svg = new Rsvg(); 55 | svg.setDPI(120, 180); 56 | svg.getDPI().should.deep.equal({ x: 120, y: 180 }); 57 | svg.dpiX.should.equal(120); 58 | svg.dpiY.should.equal(180); 59 | }); 60 | 61 | it('defaults to 90 DPI', function() { 62 | var svg = new Rsvg(); 63 | svg.setDPI(120, 180); 64 | svg.setDPI(); 65 | svg.getDPI().should.deep.equal({ x: 90, y: 90 }); 66 | svg.dpiX.should.equal(90); 67 | svg.dpiY.should.equal(90); 68 | }); 69 | }); 70 | 71 | describe('render() called with: width, height, format, id', function() { 72 | it('is deprecated', function() { 73 | new Rsvg()._renderArgs.name.should.equal('deprecated'); 74 | }); 75 | 76 | it('renders an element in the given format and resolution', function() { 77 | var svg = new Rsvg(); 78 | var underlyingRender = svg.handle.render = sinon.spy(); 79 | 80 | svg.render(300, 400, 'PNG'); 81 | underlyingRender.should.have.been.calledOnce; 82 | underlyingRender.should.have.been.calledWithExactly( 83 | 300, 84 | 400, 85 | 'png', 86 | undefined 87 | ); 88 | 89 | svg.render(900, 900, 'RAW', '#el'); 90 | underlyingRender.should.have.been.calledTwice; 91 | underlyingRender.should.have.been.calledWithExactly(900, 900, 'raw', '#el'); 92 | }); 93 | }); 94 | 95 | describe('renderRaw()', function() { 96 | it('is deprecated', function() { 97 | new Rsvg().renderRaw.name.should.equal('deprecated'); 98 | }); 99 | 100 | it('renders as a raw memory buffer', function() { 101 | var svg = new Rsvg(); 102 | var render = svg.render = sinon.spy(); 103 | 104 | svg.renderRaw(300, 400); 105 | render.should.have.been.calledOnce; 106 | render.should.have.been.calledWithExactly({ 107 | format: 'raw', 108 | width: 300, 109 | height: 400, 110 | element: undefined 111 | }); 112 | 113 | svg.renderRaw(900, 900, '#path1'); 114 | render.should.have.been.calledTwice; 115 | render.should.have.been.calledWithExactly({ 116 | format: 'raw', 117 | width: 900, 118 | height: 900, 119 | element: '#path1' 120 | }); 121 | }); 122 | }); 123 | 124 | describe('renderPNG()', function() { 125 | it('is deprecated', function() { 126 | new Rsvg().renderPNG.name.should.equal('deprecated'); 127 | }); 128 | 129 | it('renders as a PNG image', function() { 130 | var svg = new Rsvg(); 131 | var render = svg.render = sinon.spy(); 132 | 133 | svg.renderPNG(300, 400); 134 | render.should.have.been.calledOnce; 135 | render.should.have.been.calledWithExactly({ 136 | format: 'png', 137 | width: 300, 138 | height: 400, 139 | element: undefined 140 | }); 141 | 142 | svg.renderPNG(900, 900, '#path1'); 143 | render.should.have.been.calledTwice; 144 | render.should.have.been.calledWithExactly({ 145 | format: 'png', 146 | width: 900, 147 | height: 900, 148 | element: '#path1' 149 | }); 150 | }); 151 | }); 152 | 153 | describe('renderPDF()', function() { 154 | it('is deprecated', function() { 155 | new Rsvg().renderPDF.name.should.equal('deprecated'); 156 | }); 157 | 158 | it('renders as a PDF document', function() { 159 | var svg = new Rsvg(); 160 | var render = svg.render = sinon.spy(); 161 | 162 | svg.renderPDF(300, 400); 163 | render.should.have.been.calledOnce; 164 | render.should.have.been.calledWithExactly({ 165 | format: 'pdf', 166 | width: 300, 167 | height: 400, 168 | element: undefined 169 | }); 170 | 171 | svg.renderPDF(900, 900, '#path1'); 172 | render.should.have.been.calledTwice; 173 | render.should.have.been.calledWithExactly({ 174 | format: 'pdf', 175 | width: 900, 176 | height: 900, 177 | element: '#path1' 178 | }); 179 | }); 180 | }); 181 | 182 | describe('renderSVG()', function() { 183 | it('is deprecated', function() { 184 | new Rsvg().renderSVG.name.should.equal('deprecated'); 185 | }); 186 | 187 | it('renders as a SVG document', function() { 188 | var svg = new Rsvg(); 189 | var render = svg.render = sinon.spy(); 190 | 191 | svg.renderSVG(300, 400); 192 | render.should.have.been.calledOnce; 193 | render.should.have.been.calledWithExactly({ 194 | format: 'svg', 195 | width: 300, 196 | height: 400, 197 | element: undefined 198 | }); 199 | 200 | svg.renderSVG(900, 900, '#path1'); 201 | render.should.have.been.calledTwice; 202 | render.should.have.been.calledWithExactly({ 203 | format: 'svg', 204 | width: 900, 205 | height: 900, 206 | element: '#path1' 207 | }); 208 | }); 209 | }); 210 | 211 | }); 212 | --------------------------------------------------------------------------------