Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit da33bbe

Browse files
mcfedrchearon
andcommitted
Add link tags for pdfs
Co-Authored-By: Caleb Hearon <caleb@chearon.net>
1 parent 728e76c commit da33bbe

File tree

8 files changed

+141
-1
lines changed

8 files changed

+141
-1
lines changed

‎.gitignore

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 20 additions & 0 deletions
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

Lines changed: 20 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 55 additions & 1 deletion
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 37 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /