Skip to content

Commit

Permalink
Add link tags for pdfs
Browse files Browse the repository at this point in the history
Co-Authored-By: Caleb Hearon <caleb@chearon.net>
  • Loading branch information
mcfedr and chearon committed Jan 11, 2025
1 parent 728e76c commit 8ee12be
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
* `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed.

### Added
* Support for accessibility and links in PDFs

### Fixed

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

It is possible to add hyperlinks using `.beginTag()` and `.endTag()`:

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

Or with a defined rectangle:

```js
ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]")
ctx.endTag('Link')
```

Note that the syntax is unique to Cairo. See [cairo_tag_begin](https://www.cairographics.org/manual/cairo-Tags-and-Links.html#cairo-tag-begin) for the full documentation.

You can create areas on the canvas using the "cairo.dest" tag, and then link to them using the "Link" tag with the `dest=` attribute. You can also define PDF structure for accessibility by using tag names like "P", "H1", and "TABLE". The standard tags are defined in §14.8.4 of the [PDF 1.7](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf) specification.

See also:

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

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

ctx.beginTag('Link', 'uri=\'https://google.com\'')
ctx.font = '22px Helvetica'
ctx.fillText('Text link to Google', 110, 50)
ctx.endTag('Link')

ctx.fillText('Rect link to node-canvas below!', 40, 180)

ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]')
ctx.endTag('Link')

fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) {
if (err) throw err
})
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(tagName: string, attributes?: string): void;
endTag(tagName: string): void;
/**
* _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image,
* etc.) rendering quality.
Expand Down
58 changes: 57 additions & 1 deletion src/CanvasRenderingContext2d.cc
Original file line number Diff line number Diff line change
Expand Up @@ -135,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::EndTag>("endTag", 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 @@ -419,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 @@ -3348,3 +3352,55 @@ Context2d::Ellipse(const Napi::CallbackInfo& info) {
}
cairo_set_matrix(ctx, &save_matrix);
}

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

void
Context2d::BeginTag(const Napi::CallbackInfo& info) {
std::string tagName = "";
std::string attributes = "";

if (info.Length() == 0) {
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
return;
} else {
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
return;
} else {
tagName = info[0].As<Napi::String>().Utf8Value();
}

if (info.Length() > 1) {
if (!info[1].IsString()) {
Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException();
return;
} else {
attributes = info[1].As<Napi::String>().Utf8Value();
}
}
}

cairo_t *ctx = context();
cairo_tag_begin(ctx, tagName.c_str(), attributes.c_str());
}

void
Context2d::EndTag(const Napi::CallbackInfo& info) {
if (info.Length() == 0) {
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
return;
}

if (!info[0].IsString()) {
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
return;
}

std::string tagName = info[0].As<Napi::String>().Utf8Value();

cairo_t *ctx = context();
cairo_tag_end(ctx, tagName.c_str());
}

#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 EndTag(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 @@ -16,7 +16,8 @@ const {
loadImage,
registerFont,
Canvas,
deregisterAllFonts
deregisterAllFonts,
cairoVersion
} = require('../')

function assertApprox(actual, expected, tol) {
Expand Down Expand Up @@ -755,6 +756,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 @@ -2000,4 +2006,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('Link', "uri='http://example.com'")
ctx.strokeText('hello', 0, 0)
ctx.endTag('Link')
const buf = canvas.toBuffer('application/pdf')
assert.equal('PDF', buf.slice(1, 4).toString())
})

it('requires tag argument', function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
assert.throws(() => { ctx.beginTag() })
})

it('requires attributes to be a string', function () {
const canvas = createCanvas(20, 20, 'pdf')
const ctx = canvas.getContext('2d')
assert.throws(() => { ctx.beginTag('Link', {}) })
})
})
})

0 comments on commit 8ee12be

Please sign in to comment.