Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add link tags for pdfs #2405

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
==================
### Changed
### Added
* Support for links in PDFs
### Fixed


Expand Down
16 changes: 16 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,22 @@ ctx.addPage(400, 800)
ctx.fillText('Hello World 2', 50, 80)
```

It is possible to add hyperlinks use `.beginTag()` and `.closeTag()`:

```js
ctx.beginTag({name: 'Link', uri: 'https://google.com'})
ctx.font = '22px Helvetica'
ctx.fillText('Hello World', 50, 80)
ctx.closeTag()
```

Or with a defined rectangle:

```js
ctx.beginTag({name: 'Link', uri: 'https://google.com', rect: { x: 50, y: 80, width: 100, height: 20 }})
ctx.closeTag()
```

See also:

* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs
Expand Down
20 changes: 20 additions & 0 deletions examples/pdf-link-rect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const fs = require('fs')
const Canvas = require('..')

const canvas = Canvas.createCanvas(400, 200, 'pdf')
const ctx = canvas.getContext('2d')

let x = 50
let y = 80

ctx.beginTag({ name: 'Link', uri: 'https://google.com', rect: { x: 40, y: 70, width: 100, height: 50 } })
ctx.closeTag()

ctx.font = '22px Helvetica'
ctx.fillText('node-canvas pdf', x, y)

fs.writeFile('out.pdf', canvas.toBuffer(), function (err) {
if (err) throw err

console.log('created out.pdf')
})
19 changes: 19 additions & 0 deletions examples/pdf-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const fs = require('fs')
const Canvas = require('..')

const canvas = Canvas.createCanvas(400, 200, 'pdf')
const ctx = canvas.getContext('2d')

let y = 80
let x = 50

ctx.beginTag({ name: 'Link', uri: 'https://google.com' })
ctx.font = '22px Helvetica'
ctx.fillText('node-canvas pdf', x, y)
ctx.closeTag()

fs.writeFile('out.pdf', canvas.toBuffer(), function (err) {
if (err) throw err

console.log('created out.pdf')
})
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export class CanvasRenderingContext2D {
createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
beginTag(config: {name: 'Link', uri: string, rect?: {x: number, y: number, width: number, height: number}}): void;
closeTag(): void;
/**
* _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image,
* etc.) rendering quality.
Expand Down
107 changes: 106 additions & 1 deletion src/CanvasRenderingContext2d.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <string>
#include "Util.h"
#include <vector>
#include <iostream>

/*
* Rectangle arg assertions.
Expand Down Expand Up @@ -134,6 +135,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) {
InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method),
InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method),
InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method),
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method),
InstanceMethod<&Context2d::CloseTag>("closeTag", napi_default_method),
#endif
InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty),
InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty),
Expand Down Expand Up @@ -418,7 +423,7 @@ Context2d::fill(bool preserve) {
width = cairo_image_surface_get_width(patternSurface);
height = y2 - y1;
}

cairo_new_path(_context);
cairo_rectangle(_context, 0, 0, width, height);
cairo_clip(_context);
Expand Down Expand Up @@ -3352,3 +3357,103 @@ Context2d::Ellipse(const Napi::CallbackInfo& info) {
}
cairo_set_matrix(ctx, &save_matrix);
}

#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)

/*
* Open and close a link tag
*/

void
replaceAll( std::string &s, const std::string &search, const std::string &replace ) {
for( size_t pos = 0; ; pos += replace.length() ) {
// Locate the substring to replace
pos = s.find( search, pos );
if( pos == std::string::npos ) break;
// Replace by erasing and inserting
s.erase( pos, search.length() );
s.insert( pos, replace );
}
}

bool
containsOnlyASCII(const std::string& str) {
for (auto c: str) {
if (static_cast<unsigned char>(c) > 127) {
return false;
}
}
return true;
}

void
Context2d::BeginTag(const Napi::CallbackInfo& info) {
if (info.Length() < 1 || !info[0].IsObject()) {
Napi::TypeError::New(env, "config must be an object").ThrowAsJavaScriptException();
return;
}

Napi::Object config = info[0].As<Napi::Object>();

Napi::String nameValue;
if (!config.Get("name").UnwrapTo(&nameValue) || nameValue.IsUndefined()) {
Napi::TypeError::New(env, "config must have a name key").ThrowAsJavaScriptException();
return;
}
std::string name = nameValue.Utf8Value();
if (name != CAIRO_TAG_LINK) {
Napi::TypeError::New(env, "name must be 'Link'").ThrowAsJavaScriptException();
return;
}

Napi::String uriValue;
if (!config.Get("uri").UnwrapTo(&uriValue) || uriValue.IsUndefined()) {
Napi::TypeError::New(env, "config must have a uri key").ThrowAsJavaScriptException();
return;
}
std::string uri = uriValue.Utf8Value();
if (!containsOnlyASCII(uri)) {
Napi::TypeError::New(env, "uri must be ascii only").ThrowAsJavaScriptException();
return;
}

std::string rectAttr;
Napi::Object rect;
if (config.Get("rect").UnwrapTo(&rect) && !rect.IsUndefined()) {
if (!rect.IsObject()) {
Napi::TypeError::New(env, "rect must be an object").ThrowAsJavaScriptException();
return;
}

Napi::Number x, y, width, height;
if (!rect.Get("x").UnwrapTo(&x) || x.IsUndefined() ||
!rect.Get("y").UnwrapTo(&y) || y.IsUndefined() ||
!rect.Get("width").UnwrapTo(&width) || width.IsUndefined() ||
!rect.Get("height").UnwrapTo(&height) || height.IsUndefined()) {
Napi::TypeError::New(env, "rect must contain x, y, width, height").ThrowAsJavaScriptException();
return;
}

float xF = x.FloatValue(), yF = y.FloatValue(), widthF = width.FloatValue(), heightF = height.FloatValue();
if (xF <= 0 || yF <= 0 || widthF <= 0 || heightF <= 0) {
Napi::TypeError::New(env, "rect values must be positive").ThrowAsJavaScriptException();
return;
}

rectAttr = " rect=[" + std::to_string(xF) + " " + std::to_string(yF) + " " + std::to_string(widthF) + " " + std::to_string(heightF) + "]";
}

replaceAll(uri, "'", "\\'");
std::string attrs = "uri='" + uri + "'" + rectAttr;

cairo_t *ctx = context();
cairo_tag_begin(ctx, CAIRO_TAG_LINK, attrs.c_str());
}

void
Context2d::CloseTag(const Napi::CallbackInfo& info) {
cairo_t *ctx = context();
cairo_tag_end(ctx, CAIRO_TAG_LINK);
}

#endif
4 changes: 4 additions & 0 deletions src/CanvasRenderingContext2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ class Context2d : public Napi::ObjectWrap<Context2d> {
void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value);
void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value);
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
void BeginTag(const Napi::CallbackInfo& info);
void CloseTag(const Napi::CallbackInfo& info);
#endif
inline void setContext(cairo_t *ctx) { _context = ctx; }
inline cairo_t *context(){ return _context; }
inline Canvas *canvas(){ return _canvas; }
Expand Down
40 changes: 39 additions & 1 deletion test/canvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const {
parseFont,
registerFont,
Canvas,
deregisterAllFonts
deregisterAllFonts,
cairoVersion
} = require('../')

function assertApprox(actual, expected, tol) {
Expand Down Expand Up @@ -828,6 +829,11 @@ describe('Canvas', function () {
assertPixel(0xffff0000, 5, 0, 'first red pixel')
})
})

it('Canvas#toBuffer("application/pdf")', function () {
const buf = createCanvas(200, 200, 'pdf').toBuffer('application/pdf')
assert.equal('PDF', buf.slice(1, 4).toString())
})
})

describe('#toDataURL()', function () {
Expand Down Expand Up @@ -2073,4 +2079,36 @@ describe('Canvas', function () {
})
}
})

describe('Context2d#beingTag()/endTag()', function () {
before(function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
if (!('beginTag' in ctx)) {
this.skip()
}
})

it('generates a pdf', function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
ctx.beginTag({ name: 'Link', uri: 'tes\'t' })
ctx.strokeText('hello', 0, 0)
ctx.closeTag()
const buf = canvas.toBuffer('application/pdf')
assert.equal('PDF', buf.slice(1, 4).toString())
})

it('must be a link', function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
assert.throws(() => { ctx.beginTag({ name: 'other', uri: 'test' }) })
})

it('must be a ascii', function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
assert.throws(() => { ctx.beginTag({ name: 'Link', uri: 'має бути ascii' }) })
})
})
})
Loading