Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 04442f7

Browse files
mcfedrchearon
andcommittedJan 11, 2025·
Add link tags for pdfs
Co-Authored-By: Caleb Hearon <caleb@chearon.net>
1 parent 728e76c commit 04442f7

8 files changed

+141
-1
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build
44
test/images/*.png
55
examples/*.png
66
examples/*.jpg
7+
examples/*.pdf
78
testing
89
out.png
910
out.pdf

‎CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
1212
* `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.
1313

1414
### Added
15+
* Support for accessibility and links in PDFs
16+
1517
### Fixed
1618

1719
3.0.1

‎Readme.md

+20
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,26 @@ ctx.addPage(400, 800)
515515
ctx.fillText('Hello World 2', 50, 80)
516516
```
517517

518+
It is possible to add hyperlinks using `.beginTag()` and `.endTag()`:
519+
520+
```js
521+
ctx.beginTag('Link', "uri='https://google.com'")
522+
ctx.font = '22px Helvetica'
523+
ctx.fillText('Hello World', 50, 80)
524+
ctx.endTag('Link')
525+
```
526+
527+
Or with a defined rectangle:
528+
529+
```js
530+
ctx.beginTag('Link', "uri='https://google.com' rect=[50 80 100 20]")
531+
ctx.endTag('Link')
532+
```
533+
534+
Note that the syntax for attributes 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.
535+
536+
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.
537+
518538
See also:
519539

520540
* [Image#dataMode](#imagedatamode) for embedding JPEGs in PDFs

‎examples/pdf-link.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const Canvas = require('..')
4+
5+
const canvas = Canvas.createCanvas(400, 300, 'pdf')
6+
const ctx = canvas.getContext('2d')
7+
8+
ctx.beginTag('Link', 'uri=\'https://google.com\'')
9+
ctx.font = '22px Helvetica'
10+
ctx.fillText('Text link to Google', 110, 50)
11+
ctx.endTag('Link')
12+
13+
ctx.fillText('Rect link to node-canvas below!', 40, 180)
14+
15+
ctx.beginTag('Link', 'uri=\'https://github.com/Automattic/node-canvas\' rect=[0 200 400 100]')
16+
ctx.endTag('Link')
17+
18+
fs.writeFile(path.join(__dirname, 'pdf-link.pdf'), canvas.toBuffer(), function (err) {
19+
if (err) throw err
20+
})

‎index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ export class CanvasRenderingContext2D {
232232
createPattern(image: Canvas|Image, repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' | '' | null): CanvasPattern
233233
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
234234
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
235+
beginTag(tagName: string, attributes?: string): void;
236+
endTag(tagName: string): void;
235237
/**
236238
* _Non-standard_. Defaults to 'good'. Affects pattern (gradient, image,
237239
* etc.) rendering quality.

‎src/CanvasRenderingContext2d.cc

+55-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) {
135135
InstanceMethod<&Context2d::CreatePattern>("createPattern", napi_default_method),
136136
InstanceMethod<&Context2d::CreateLinearGradient>("createLinearGradient", napi_default_method),
137137
InstanceMethod<&Context2d::CreateRadialGradient>("createRadialGradient", napi_default_method),
138+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
139+
InstanceMethod<&Context2d::BeginTag>("beginTag", napi_default_method),
140+
InstanceMethod<&Context2d::EndTag>("endTag", napi_default_method),
141+
#endif
138142
InstanceAccessor<&Context2d::GetFormat>("pixelFormat", napi_default_jsproperty),
139143
InstanceAccessor<&Context2d::GetPatternQuality, &Context2d::SetPatternQuality>("patternQuality", napi_default_jsproperty),
140144
InstanceAccessor<&Context2d::GetImageSmoothingEnabled, &Context2d::SetImageSmoothingEnabled>("imageSmoothingEnabled", napi_default_jsproperty),
@@ -419,7 +423,7 @@ Context2d::fill(bool preserve) {
419423
width = cairo_image_surface_get_width(patternSurface);
420424
height = y2 - y1;
421425
}
422-
426+
423427
cairo_new_path(_context);
424428
cairo_rectangle(_context, 0, 0, width, height);
425429
cairo_clip(_context);
@@ -3348,3 +3352,53 @@ Context2d::Ellipse(const Napi::CallbackInfo& info) {
33483352
}
33493353
cairo_set_matrix(ctx, &save_matrix);
33503354
}
3355+
3356+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
3357+
3358+
void
3359+
Context2d::BeginTag(const Napi::CallbackInfo& info) {
3360+
std::string tagName = "";
3361+
std::string attributes = "";
3362+
3363+
if (info.Length() == 0) {
3364+
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
3365+
return;
3366+
} else {
3367+
if (!info[0].IsString()) {
3368+
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
3369+
return;
3370+
} else {
3371+
tagName = info[0].As<Napi::String>().Utf8Value();
3372+
}
3373+
3374+
if (info.Length() > 1) {
3375+
if (!info[1].IsString()) {
3376+
Napi::TypeError::New(env, "Attributes must be a string matching Cairo's attribute format").ThrowAsJavaScriptException();
3377+
return;
3378+
} else {
3379+
attributes = info[1].As<Napi::String>().Utf8Value();
3380+
}
3381+
}
3382+
}
3383+
3384+
cairo_tag_begin(_context, tagName.c_str(), attributes.c_str());
3385+
}
3386+
3387+
void
3388+
Context2d::EndTag(const Napi::CallbackInfo& info) {
3389+
if (info.Length() == 0) {
3390+
Napi::TypeError::New(env, "Tag name is required").ThrowAsJavaScriptException();
3391+
return;
3392+
}
3393+
3394+
if (!info[0].IsString()) {
3395+
Napi::TypeError::New(env, "Tag name must be a string.").ThrowAsJavaScriptException();
3396+
return;
3397+
}
3398+
3399+
std::string tagName = info[0].As<Napi::String>().Utf8Value();
3400+
3401+
cairo_tag_end(_context, tagName.c_str());
3402+
}
3403+
3404+
#endif

‎src/CanvasRenderingContext2d.h

+4
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ class Context2d : public Napi::ObjectWrap<Context2d> {
178178
void SetFont(const Napi::CallbackInfo& info, const Napi::Value& value);
179179
void SetTextBaseline(const Napi::CallbackInfo& info, const Napi::Value& value);
180180
void SetTextAlign(const Napi::CallbackInfo& info, const Napi::Value& value);
181+
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
182+
void BeginTag(const Napi::CallbackInfo& info);
183+
void EndTag(const Napi::CallbackInfo& info);
184+
#endif
181185
inline void setContext(cairo_t *ctx) { _context = ctx; }
182186
inline cairo_t *context(){ return _context; }
183187
inline Canvas *canvas(){ return _canvas; }

‎test/canvas.test.js

+37
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,11 @@ describe('Canvas', function () {
755755
assertPixel(0xffff0000, 5, 0, 'first red pixel')
756756
})
757757
})
758+
759+
it('Canvas#toBuffer("application/pdf")', function () {
760+
const buf = createCanvas(200, 200, 'pdf').toBuffer('application/pdf')
761+
assert.equal('PDF', buf.slice(1, 4).toString())
762+
})
758763
})
759764

760765
describe('#toDataURL()', function () {
@@ -2000,4 +2005,36 @@ describe('Canvas', function () {
20002005
})
20012006
}
20022007
})
2008+
2009+
describe('Context2d#beingTag()/endTag()', function () {
2010+
before(function () {
2011+
const canvas = createCanvas(20, 20, 'pdf')
2012+
const ctx = canvas.getContext('2d')
2013+
if (!('beginTag' in ctx)) {
2014+
this.skip()
2015+
}
2016+
})
2017+
2018+
it('generates a pdf', function () {
2019+
const canvas = createCanvas(20, 20, 'pdf')
2020+
const ctx = canvas.getContext('2d')
2021+
ctx.beginTag('Link', "uri='http://example.com'")
2022+
ctx.strokeText('hello', 0, 0)
2023+
ctx.endTag('Link')
2024+
const buf = canvas.toBuffer('application/pdf')
2025+
assert.equal('PDF', buf.slice(1, 4).toString())
2026+
})
2027+
2028+
it('requires tag argument', function () {
2029+
const canvas = createCanvas(20, 20, 'pdf')
2030+
const ctx = canvas.getContext('2d')
2031+
assert.throws(() => { ctx.beginTag() })
2032+
})
2033+
2034+
it('requires attributes to be a string', function () {
2035+
const canvas = createCanvas(20, 20, 'pdf')
2036+
const ctx = canvas.getContext('2d')
2037+
assert.throws(() => { ctx.beginTag('Link', {}) })
2038+
})
2039+
})
20032040
})

0 commit comments

Comments
 (0)
Please sign in to comment.