Skip to content

Commit

Permalink
Add link tags for pdfs
Browse files Browse the repository at this point in the history
  • Loading branch information
mcfedr committed Aug 13, 2024
1 parent 7dfeb04 commit 1d21bc4
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 0 deletions.
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
9 changes: 9 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,15 @@ 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()
```

See also:

* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs
Expand Down
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}): void;
closeTag(): void;
/**
* _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image,
* etc.) rendering quality.
Expand Down
78 changes: 78 additions & 0 deletions src/CanvasRenderingContext2d.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) {
InstanceMethod<&Context2d::CreatePattern>("createPattern"),
InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient"),
InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient"),
#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"),
InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality"),
InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled"),
Expand Down Expand Up @@ -3354,3 +3358,77 @@ 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)) {
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)) {
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;
}

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

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
29 changes: 29 additions & 0 deletions test/canvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,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 @@ -2068,4 +2073,28 @@ describe('Canvas', function () {
})
}
})

describe('Context2d#beingTag()/endTag()', function () {
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' }) })
})
})
})

0 comments on commit 1d21bc4

Please sign in to comment.