@jbrowse/capture 5.0.0-beta.11 → 5.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -2
- package/esm/annotationOverlay.d.ts +77 -0
- package/esm/annotationOverlay.js +766 -0
- package/esm/annotationSpec.d.ts +7 -0
- package/esm/annotationSpec.js +70 -0
- package/esm/annotations.d.ts +14 -0
- package/esm/annotations.js +68 -0
- package/esm/args.d.ts +5 -3
- package/esm/args.js +28 -13
- package/esm/bin.js +95 -22
- package/esm/capture.d.ts +31 -2
- package/esm/capture.js +162 -40
- package/esm/catalog.d.ts +11 -6
- package/esm/catalog.js +37 -14
- package/esm/dotplotAnchor.d.ts +4 -0
- package/esm/dotplotAnchor.js +104 -0
- package/esm/graphAnchor.d.ts +47 -0
- package/esm/graphAnchor.js +120 -0
- package/esm/index.d.ts +5 -2
- package/esm/index.js +2 -1
- package/esm/jsonArgs.d.ts +23 -0
- package/esm/jsonArgs.js +46 -0
- package/esm/poll.d.ts +9 -0
- package/esm/poll.js +23 -0
- package/esm/ready.d.ts +4 -4
- package/esm/ready.js +6 -2
- package/esm/sessionGate.d.ts +9 -3
- package/esm/sessionGate.js +15 -5
- package/esm/url.d.ts +6 -5
- package/esm/url.js +20 -5
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/esm/waits.d.ts +5 -4
- package/esm/waits.js +3 -5
- package/package.json +6 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Annotation } from './annotationOverlay.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Throw for each callout the overlay would draw nothing for, or draw at the
|
|
4
|
+
* page origin, so a spec fails before a browser launches rather than shipping a
|
|
5
|
+
* figure with the callout missing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertValidAnnotations(annotations: Annotation[]): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ANNOTATION_TYPES } from './annotationOverlay.js';
|
|
2
|
+
const ANCHOR_KEYS = new Set(Object.keys({
|
|
3
|
+
selector: 0,
|
|
4
|
+
text: 0,
|
|
5
|
+
graphNode: 0,
|
|
6
|
+
hLoc: 0,
|
|
7
|
+
vLoc: 0,
|
|
8
|
+
view: 0,
|
|
9
|
+
trackId: 0,
|
|
10
|
+
loc: 0,
|
|
11
|
+
fracY: 0,
|
|
12
|
+
alignX: 0,
|
|
13
|
+
alignY: 0,
|
|
14
|
+
dx: 0,
|
|
15
|
+
dy: 0,
|
|
16
|
+
fracX: 0,
|
|
17
|
+
}));
|
|
18
|
+
function unknownAnchorKeys(a) {
|
|
19
|
+
return [a.anchor, a.fromAnchor].flatMap(anchor => Object.keys(anchor ?? {}).filter(k => !ANCHOR_KEYS.has(k)));
|
|
20
|
+
}
|
|
21
|
+
const isNumber = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
22
|
+
function missing(a) {
|
|
23
|
+
const placed = !!a.anchor || (isNumber(a.x) && isNumber(a.y));
|
|
24
|
+
switch (a.type) {
|
|
25
|
+
case 'arrow': {
|
|
26
|
+
const head = !!a.anchor || isNumber(a.to?.x);
|
|
27
|
+
const tail = !!a.fromAnchor || isNumber(a.from?.x);
|
|
28
|
+
return head && tail ? undefined : 'an arrow needs a head and a tail';
|
|
29
|
+
}
|
|
30
|
+
case 'box':
|
|
31
|
+
return !!a.anchor || (placed && isNumber(a.width) && isNumber(a.height))
|
|
32
|
+
? undefined
|
|
33
|
+
: 'a box needs an anchor or x, y, width and height';
|
|
34
|
+
case 'circle':
|
|
35
|
+
return placed ? undefined : 'a circle needs an anchor or x and y';
|
|
36
|
+
case 'text':
|
|
37
|
+
return a.text && placed
|
|
38
|
+
? undefined
|
|
39
|
+
: 'a text needs text and an anchor or x and y';
|
|
40
|
+
case 'legend':
|
|
41
|
+
return a.entries?.length && placed
|
|
42
|
+
? undefined
|
|
43
|
+
: 'a legend needs entries and an anchor or x and y';
|
|
44
|
+
case 'trapezoid':
|
|
45
|
+
return a.anchor && a.fromAnchor
|
|
46
|
+
? undefined
|
|
47
|
+
: 'a trapezoid needs both anchor and fromAnchor';
|
|
48
|
+
default:
|
|
49
|
+
return `type "${a.type}" is not one of ${ANNOTATION_TYPES.join(', ')}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Throw for each callout the overlay would draw nothing for, or draw at the
|
|
54
|
+
* page origin, so a spec fails before a browser launches rather than shipping a
|
|
55
|
+
* figure with the callout missing.
|
|
56
|
+
*/
|
|
57
|
+
export function assertValidAnnotations(annotations) {
|
|
58
|
+
const problems = annotations.flatMap((a, i) => {
|
|
59
|
+
const unknown = a ? unknownAnchorKeys(a) : [];
|
|
60
|
+
const why = unknown.length > 0
|
|
61
|
+
? `anchor key(s) ${unknown.join(', ')} not recognized; the keys are ${[...ANCHOR_KEYS].join(', ')}`
|
|
62
|
+
: a
|
|
63
|
+
? missing(a)
|
|
64
|
+
: 'a callout must be an object';
|
|
65
|
+
return why ? [`annotation ${i}: ${why}`] : [];
|
|
66
|
+
});
|
|
67
|
+
if (problems.length > 0) {
|
|
68
|
+
throw new Error(`invalid annotations: ${problems.join('; ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Annotation } from './annotationOverlay.ts';
|
|
2
|
+
import type { Page } from 'puppeteer';
|
|
3
|
+
/** Remove the callout overlay a previous `drawAnnotations` left on the page. */
|
|
4
|
+
export declare function clearAnnotations(page: Page): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Draw callouts (arrows, boxes, labels, badges) over the page as one fixed SVG
|
|
7
|
+
* overlay, so the next screenshot includes them. Each anchor resolves against
|
|
8
|
+
* the live session — a locus through the view model, a graph node or dotplot
|
|
9
|
+
* cell through its view's layout, page chrome by selector or text — so nothing
|
|
10
|
+
* is a measured pixel. Throws when an anchor resolves to nothing or a callout
|
|
11
|
+
* lands outside the viewport, rather than writing a figure with a callout
|
|
12
|
+
* parked in the corner or missing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function drawAnnotations(page: Page, annotations: Annotation[]): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ANNOTATION_OVERLAY_ID, drawAnnotationOverlay, parseAnnotationLocus, } from './annotationOverlay.js';
|
|
2
|
+
import { assertValidAnnotations } from './annotationSpec.js';
|
|
3
|
+
import { dotplotAnchorRect } from './dotplotAnchor.js';
|
|
4
|
+
import { graphNodeRect } from './graphAnchor.js';
|
|
5
|
+
// Attach the parsed region so page context never parses strings, plus the
|
|
6
|
+
// viewport rect of the two anchor kinds that resolve out here rather than in
|
|
7
|
+
// the overlay: a graph node, and a dotplot's pair of axes. Both draw into a
|
|
8
|
+
// single canvas, so there is no element in the page for the overlay to measure
|
|
9
|
+
// and the view's own layout is the only thing that knows where they went.
|
|
10
|
+
//
|
|
11
|
+
// `wantBounds` is the same question for both kinds of anchor: does this callout
|
|
12
|
+
// WRAP the thing it names, or point at it? A force-directed graph node bends, so
|
|
13
|
+
// the centre of its bounding box can sit off the node and everything but a box
|
|
14
|
+
// wants a point ON it (a dotplot block's cell is a rectangle either way). A
|
|
15
|
+
// one-base locus is the same shape of question — the base's own column for a
|
|
16
|
+
// box, the zero-width position between two bases for a pill or an arrow head —
|
|
17
|
+
// which is what parseAnnotationLocus's `wrap` decides.
|
|
18
|
+
async function withRegion(page, anchor, wantBounds) {
|
|
19
|
+
if (!anchor) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const isDotplot = anchor.hLoc !== undefined || anchor.vLoc !== undefined;
|
|
23
|
+
return {
|
|
24
|
+
...anchor,
|
|
25
|
+
region: anchor.loc
|
|
26
|
+
? parseAnnotationLocus(anchor.loc, wantBounds)
|
|
27
|
+
: undefined,
|
|
28
|
+
rect: anchor.graphNode
|
|
29
|
+
? await graphNodeRect(page, anchor, wantBounds)
|
|
30
|
+
: isDotplot
|
|
31
|
+
? await dotplotAnchorRect(page, anchor)
|
|
32
|
+
: undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Remove the callout overlay a previous `drawAnnotations` left on the page. */
|
|
36
|
+
export async function clearAnnotations(page) {
|
|
37
|
+
await page.evaluate(id => {
|
|
38
|
+
document.getElementById(id)?.remove();
|
|
39
|
+
}, ANNOTATION_OVERLAY_ID);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Draw callouts (arrows, boxes, labels, badges) over the page as one fixed SVG
|
|
43
|
+
* overlay, so the next screenshot includes them. Each anchor resolves against
|
|
44
|
+
* the live session — a locus through the view model, a graph node or dotplot
|
|
45
|
+
* cell through its view's layout, page chrome by selector or text — so nothing
|
|
46
|
+
* is a measured pixel. Throws when an anchor resolves to nothing or a callout
|
|
47
|
+
* lands outside the viewport, rather than writing a figure with a callout
|
|
48
|
+
* parked in the corner or missing.
|
|
49
|
+
*/
|
|
50
|
+
export async function drawAnnotations(page, annotations) {
|
|
51
|
+
assertValidAnnotations(annotations);
|
|
52
|
+
await clearAnnotations(page);
|
|
53
|
+
const items = await Promise.all(annotations.map(async (a) => ({
|
|
54
|
+
...a,
|
|
55
|
+
// only a box wants the drawn bounds — of a graph node, or of the base a
|
|
56
|
+
// one-coordinate locus names; a ring/arrow/label wants a point on it, and
|
|
57
|
+
// an arrow's tail always does
|
|
58
|
+
anchor: await withRegion(page, a.anchor, a.type === 'box'),
|
|
59
|
+
fromAnchor: await withRegion(page, a.fromAnchor, false),
|
|
60
|
+
})));
|
|
61
|
+
const { unresolved, offFrame } = await page.evaluate(drawAnnotationOverlay, items, ANNOTATION_OVERLAY_ID);
|
|
62
|
+
if (unresolved.length > 0) {
|
|
63
|
+
throw new Error(`annotation anchors resolved to nothing: ${unresolved.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
if (offFrame.length > 0) {
|
|
66
|
+
throw new Error(`annotations drew outside the capture: ${offFrame.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/esm/args.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface ParsedArgs {
|
|
2
|
-
command: 'capture' | 'url' | 'list';
|
|
2
|
+
command: 'capture' | 'batch' | 'url' | 'list';
|
|
3
3
|
hub?: string;
|
|
4
4
|
config?: string;
|
|
5
5
|
assembly?: string;
|
|
@@ -11,15 +11,17 @@ export interface ParsedArgs {
|
|
|
11
11
|
out?: string;
|
|
12
12
|
width?: number;
|
|
13
13
|
height?: number;
|
|
14
|
-
|
|
14
|
+
dpr?: number;
|
|
15
15
|
timeout?: number;
|
|
16
|
+
concurrency?: number;
|
|
16
17
|
fullPage: boolean;
|
|
18
|
+
annotations?: string;
|
|
17
19
|
headed: boolean;
|
|
18
20
|
verbose: boolean;
|
|
19
21
|
help: boolean;
|
|
20
22
|
version: boolean;
|
|
21
23
|
allowUnsettled: boolean;
|
|
22
|
-
/** `list`'s hub and filter. Empty for the other commands. */
|
|
24
|
+
/** `list`'s hub and filter, or `batch`'s manifest. Empty for the other commands. */
|
|
23
25
|
positionals: string[];
|
|
24
26
|
}
|
|
25
27
|
/**
|
package/esm/args.js
CHANGED
|
@@ -13,9 +13,11 @@ const OPTIONS = {
|
|
|
13
13
|
track: { type: 'string', multiple: true, default: [] },
|
|
14
14
|
width: { type: 'string' },
|
|
15
15
|
height: { type: 'string' },
|
|
16
|
-
|
|
16
|
+
dpr: { type: 'string' },
|
|
17
17
|
timeout: { type: 'string' },
|
|
18
|
+
concurrency: { type: 'string' },
|
|
18
19
|
fullPage: { type: 'boolean', default: false },
|
|
20
|
+
annotations: { type: 'string' },
|
|
19
21
|
headed: { type: 'boolean', default: false },
|
|
20
22
|
verbose: { type: 'boolean', default: false },
|
|
21
23
|
help: { type: 'boolean', short: 'h', default: false },
|
|
@@ -24,6 +26,8 @@ const OPTIONS = {
|
|
|
24
26
|
};
|
|
25
27
|
const EVERY_COMMAND = ['help', 'version'];
|
|
26
28
|
const SUBCOMMAND_FLAGS = {
|
|
29
|
+
capture: new Set(Object.keys(OPTIONS).filter(f => f !== 'concurrency')),
|
|
30
|
+
batch: new Set(Object.keys(OPTIONS).filter(f => f !== 'out')),
|
|
27
31
|
url: new Set([
|
|
28
32
|
'hub',
|
|
29
33
|
'config',
|
|
@@ -47,6 +51,13 @@ function finite(name, raw) {
|
|
|
47
51
|
}
|
|
48
52
|
return n;
|
|
49
53
|
}
|
|
54
|
+
function positiveInt(name, raw) {
|
|
55
|
+
const n = positive(name, raw);
|
|
56
|
+
if (n !== undefined && !Number.isInteger(n)) {
|
|
57
|
+
throw new Error(`--${name} needs a whole number, got "${raw}"`);
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
50
61
|
function positive(name, raw) {
|
|
51
62
|
const n = finite(name, raw);
|
|
52
63
|
if (n !== undefined && n <= 0) {
|
|
@@ -61,31 +72,35 @@ function positive(name, raw) {
|
|
|
61
72
|
*/
|
|
62
73
|
export function parseArgs(argv) {
|
|
63
74
|
const [first, ...afterCommand] = argv;
|
|
64
|
-
const command = first === 'list' || first === 'url' ? first : 'capture';
|
|
75
|
+
const command = first === 'list' || first === 'url' || first === 'batch' ? first : 'capture';
|
|
65
76
|
const { values, positionals, tokens } = parseNodeArgs({
|
|
66
77
|
args: command === 'capture' ? argv : afterCommand,
|
|
67
78
|
options: OPTIONS,
|
|
68
|
-
allowPositionals: command === 'list',
|
|
79
|
+
allowPositionals: command === 'list' || command === 'batch',
|
|
69
80
|
tokens: true,
|
|
70
81
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
82
|
+
for (const token of tokens) {
|
|
83
|
+
if (token.kind === 'option' && !SUBCOMMAND_FLAGS[command].has(token.name)) {
|
|
84
|
+
throw new Error(command === 'capture'
|
|
85
|
+
? `${token.rawName} applies only to \`jb2capture batch\``
|
|
86
|
+
: `${token.rawName} does not apply to \`jb2capture ${command}\``);
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
|
-
const
|
|
89
|
+
const fromStdin = ['spec', 'session', 'annotations'].filter(flag => values[flag] === '-');
|
|
90
|
+
if (fromStdin.length > 1) {
|
|
91
|
+
throw new Error(`${fromStdin.map(f => `--${f}`).join(' and ')} cannot both read stdin (-)`);
|
|
92
|
+
}
|
|
93
|
+
const { track, width, height, dpr, timeout, concurrency, ...rest } = values;
|
|
80
94
|
return {
|
|
81
95
|
...rest,
|
|
82
96
|
command,
|
|
83
97
|
// copied, since node hands back the option table's own default array
|
|
84
98
|
tracks: [...track],
|
|
85
|
-
width:
|
|
86
|
-
height:
|
|
87
|
-
|
|
99
|
+
width: positiveInt('width', width),
|
|
100
|
+
height: positiveInt('height', height),
|
|
101
|
+
dpr: positive('dpr', dpr),
|
|
88
102
|
timeout: positive('timeout', timeout),
|
|
103
|
+
concurrency: positiveInt('concurrency', concurrency),
|
|
89
104
|
positionals,
|
|
90
105
|
};
|
|
91
106
|
}
|
package/esm/bin.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
2
|
import { parseArgs } from './args.js';
|
|
4
|
-
import { captureJBrowse } from './capture.js';
|
|
3
|
+
import { captureBatch, captureJBrowse } from './capture.js';
|
|
5
4
|
import { resolveAgainstConfig } from './catalog.js';
|
|
6
5
|
import { listHubAssemblies, listHubTracks, trackName } from './hub.js';
|
|
6
|
+
import { readAnnotations, readBatch, readJson, readSpec } from './jsonArgs.js';
|
|
7
7
|
import { PUBLIC_INSTANCE, jbrowseUrl } from './url.js';
|
|
8
8
|
import { version } from './version.js';
|
|
9
9
|
const HELP = `jb2capture — screenshot a live JBrowse 2 view, once it has finished drawing
|
|
10
10
|
|
|
11
11
|
USAGE
|
|
12
12
|
jb2capture [flags] --out <file.png> open a view, wait for it, screenshot it
|
|
13
|
+
jb2capture batch <json|path|-> [flags] screenshot many views from one browser
|
|
13
14
|
jb2capture url [flags] print the URL instead of launching a browser
|
|
14
15
|
jb2capture list [hub] [filter] list hosted assemblies, or one's trackIds
|
|
15
16
|
|
|
@@ -31,9 +32,13 @@ THE IMAGE
|
|
|
31
32
|
subcommand)
|
|
32
33
|
--width <px> viewport width (default 1400)
|
|
33
34
|
--height <px> viewport height (default 900)
|
|
34
|
-
--
|
|
35
|
+
--dpr <n> device pixel ratio (default 2)
|
|
35
36
|
--fullPage grow the image until every view fits, not just the
|
|
36
37
|
--height of the viewport
|
|
38
|
+
--annotations <json|path|->
|
|
39
|
+
callouts to draw over the view: a JSON array of arrows,
|
|
40
|
+
boxes and labels, each anchored to a locus, a graph
|
|
41
|
+
node, a dotplot cell or an element
|
|
37
42
|
|
|
38
43
|
WAITING
|
|
39
44
|
--timeout <ms> budget per wait stage (default 60000)
|
|
@@ -42,6 +47,16 @@ WAITING
|
|
|
42
47
|
|
|
43
48
|
Either fails the run rather than writing a frame that is not the picture.
|
|
44
49
|
|
|
50
|
+
BATCH
|
|
51
|
+
The manifest is a JSON array with one object per image. Each names its "out"
|
|
52
|
+
and may set hub, config, assembly, loc, tracks, spec, session, width, height,
|
|
53
|
+
dpr, fullPage and annotations; the flags above are the defaults for what an
|
|
54
|
+
object leaves out. Every image gets a page of its own in one shared browser.
|
|
55
|
+
A failed image is reported and the rest carry on; the exit code is 1 if any
|
|
56
|
+
failed.
|
|
57
|
+
|
|
58
|
+
--concurrency <n> pages open at once (default 4)
|
|
59
|
+
|
|
45
60
|
OTHER
|
|
46
61
|
--headed run with a visible browser window
|
|
47
62
|
--verbose print browser console output and uncaught errors
|
|
@@ -57,28 +72,14 @@ EXAMPLES
|
|
|
57
72
|
## find the trackIds first
|
|
58
73
|
jb2capture list hg38 conservation
|
|
59
74
|
|
|
75
|
+
## one image per locus, from one browser
|
|
76
|
+
jb2capture batch '[{"loc":"BRCA1","out":"brca1.png"},{"loc":"TP53","out":"tp53.png"}]' \\
|
|
77
|
+
--hub hg38 --track hg38-ncbiRefSeqCurated
|
|
78
|
+
|
|
60
79
|
## your own config, two loci side by side
|
|
61
80
|
jb2capture --config https://example.org/config.json --assembly mydata \\
|
|
62
81
|
--loc "chr3:25,325,000-25,361,000 chr10:58,716,500-58,718,500" -o two.png
|
|
63
82
|
`;
|
|
64
|
-
// inline JSON, `-` for stdin, or a path: the forms `jb2export --spec` reads
|
|
65
|
-
function readJson(flag, value) {
|
|
66
|
-
const text = value === '-'
|
|
67
|
-
? readFileSync(0, 'utf8')
|
|
68
|
-
: value.trimStart().startsWith('{')
|
|
69
|
-
? value
|
|
70
|
-
: readFileSync(value, 'utf8');
|
|
71
|
-
const parsed = JSON.parse(text);
|
|
72
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
73
|
-
throw new Error(`--${flag} must be a JSON object`);
|
|
74
|
-
}
|
|
75
|
-
return parsed;
|
|
76
|
-
}
|
|
77
|
-
// a bare view object is the one-view spec it would be wrapped in
|
|
78
|
-
function readSpec(value) {
|
|
79
|
-
const spec = readJson('spec', value);
|
|
80
|
-
return 'type' in spec && !('views' in spec) ? { views: [spec] } : spec;
|
|
81
|
-
}
|
|
82
83
|
function urlOptions(args) {
|
|
83
84
|
return {
|
|
84
85
|
hub: args.hub,
|
|
@@ -91,6 +92,71 @@ function urlOptions(args) {
|
|
|
91
92
|
instance: args.instance,
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
async function runBatch(args) {
|
|
96
|
+
const [manifest, ...extra] = args.positionals;
|
|
97
|
+
if (!manifest || extra.length) {
|
|
98
|
+
throw new Error('`jb2capture batch` takes one manifest: JSON, a path or -');
|
|
99
|
+
}
|
|
100
|
+
const defaults = {
|
|
101
|
+
...urlOptions(args),
|
|
102
|
+
width: args.width,
|
|
103
|
+
height: args.height,
|
|
104
|
+
dpr: args.dpr,
|
|
105
|
+
fullPage: args.fullPage,
|
|
106
|
+
annotations: args.annotations
|
|
107
|
+
? readAnnotations(args.annotations)
|
|
108
|
+
: undefined,
|
|
109
|
+
timeout: args.timeout,
|
|
110
|
+
allowUnsettled: args.allowUnsettled,
|
|
111
|
+
onConsole: args.verbose
|
|
112
|
+
? (text) => {
|
|
113
|
+
console.error(` [page] ${text}`);
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
116
|
+
};
|
|
117
|
+
const captures = readBatch(manifest).map(({ spec, session, annotations, ...entry }) => ({
|
|
118
|
+
...defaults,
|
|
119
|
+
// an entry that says what to open replaces the default session whole
|
|
120
|
+
...(spec || session ? { spec: undefined, session: undefined } : {}),
|
|
121
|
+
...entry,
|
|
122
|
+
...(spec
|
|
123
|
+
? { spec: typeof spec === 'string' ? readSpec(spec) : spec }
|
|
124
|
+
: {}),
|
|
125
|
+
...(session
|
|
126
|
+
? {
|
|
127
|
+
session: typeof session === 'string'
|
|
128
|
+
? readJson('session', session)
|
|
129
|
+
: session,
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
...(annotations
|
|
133
|
+
? {
|
|
134
|
+
annotations: typeof annotations === 'string'
|
|
135
|
+
? readAnnotations(annotations)
|
|
136
|
+
: annotations,
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
}));
|
|
140
|
+
const start = performance.now();
|
|
141
|
+
const results = await captureBatch(captures, {
|
|
142
|
+
headless: !args.headed,
|
|
143
|
+
concurrency: args.concurrency,
|
|
144
|
+
onResult: (result, index) => {
|
|
145
|
+
const label = `[${index + 1}/${captures.length}] ${result.out} ${(result.ms / 1000).toFixed(1)}s`;
|
|
146
|
+
if (result.ok) {
|
|
147
|
+
console.log(`wrote ${label}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.error(`FAILED ${label}: ${result.error.message}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const failed = results.filter(r => !r.ok).length;
|
|
155
|
+
console.log(`wrote ${results.length - failed}/${results.length} images in ${((performance.now() - start) / 1000).toFixed(1)}s`);
|
|
156
|
+
if (failed) {
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
94
160
|
async function runList(positionals) {
|
|
95
161
|
const [hub, filter] = positionals;
|
|
96
162
|
if (!hub) {
|
|
@@ -131,6 +197,10 @@ async function main() {
|
|
|
131
197
|
await runList(args.positionals);
|
|
132
198
|
return;
|
|
133
199
|
}
|
|
200
|
+
if (args.command === 'batch') {
|
|
201
|
+
await runBatch(args);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
134
204
|
if (args.command === 'url') {
|
|
135
205
|
console.log(jbrowseUrl(await resolveAgainstConfig(urlOptions(args))));
|
|
136
206
|
return;
|
|
@@ -143,8 +213,11 @@ async function main() {
|
|
|
143
213
|
out: args.out,
|
|
144
214
|
width: args.width,
|
|
145
215
|
height: args.height,
|
|
146
|
-
|
|
216
|
+
dpr: args.dpr,
|
|
147
217
|
fullPage: args.fullPage,
|
|
218
|
+
annotations: args.annotations
|
|
219
|
+
? readAnnotations(args.annotations)
|
|
220
|
+
: undefined,
|
|
148
221
|
headless: !args.headed,
|
|
149
222
|
timeout: args.timeout,
|
|
150
223
|
allowUnsettled: args.allowUnsettled,
|
package/esm/capture.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Annotation } from './annotationOverlay.ts';
|
|
1
2
|
import type { LaunchOptions } from './browser.ts';
|
|
2
3
|
import type { ReadyOptions, ReadyReport } from './ready.ts';
|
|
3
4
|
import type { JBrowseUrlOptions } from './url.ts';
|
|
@@ -6,8 +7,8 @@ export interface OpenOptions extends JBrowseUrlOptions, ReadyOptions, LaunchOpti
|
|
|
6
7
|
width?: number;
|
|
7
8
|
height?: number;
|
|
8
9
|
/** Device pixel ratio. Default 2, the density a figure usually wants. */
|
|
9
|
-
|
|
10
|
-
/** Called with each page console message that is not known GPU noise,
|
|
10
|
+
dpr?: number;
|
|
11
|
+
/** Called with each page console message that is not known GPU noise, each uncaught page error, and each failed or HTTP 4xx/5xx request. */
|
|
11
12
|
onConsole?: (text: string) => void;
|
|
12
13
|
}
|
|
13
14
|
export interface OpenResult extends ReadyReport {
|
|
@@ -33,6 +34,12 @@ export interface CaptureOptions extends OpenOptions {
|
|
|
33
34
|
* far the session runs past it, and the frame is waited for again.
|
|
34
35
|
*/
|
|
35
36
|
fullPage?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Callouts drawn over the settled view before the screenshot, in the shape
|
|
39
|
+
* the JBrowse docs figures use: arrows, boxes, labels and badges, each
|
|
40
|
+
* anchored to a locus, a graph node, a dotplot cell or an element.
|
|
41
|
+
*/
|
|
42
|
+
annotations?: Annotation[];
|
|
36
43
|
}
|
|
37
44
|
export interface CaptureResult extends ReadyReport {
|
|
38
45
|
url: string;
|
|
@@ -43,3 +50,25 @@ export interface CaptureResult extends ReadyReport {
|
|
|
43
50
|
* browser.
|
|
44
51
|
*/
|
|
45
52
|
export declare function captureJBrowse(options?: CaptureOptions): Promise<CaptureResult>;
|
|
53
|
+
export interface BatchOptions extends LaunchOptions {
|
|
54
|
+
/** Pages open at once. Default 4. */
|
|
55
|
+
concurrency?: number;
|
|
56
|
+
/** Called as each capture finishes, in completion order. */
|
|
57
|
+
onResult?: (result: BatchResult, index: number) => void;
|
|
58
|
+
}
|
|
59
|
+
export type BatchResult = {
|
|
60
|
+
out?: string;
|
|
61
|
+
ms: number;
|
|
62
|
+
} & (({
|
|
63
|
+
ok: true;
|
|
64
|
+
} & CaptureResult) | {
|
|
65
|
+
ok: false;
|
|
66
|
+
error: Error;
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Screenshot many sessions from one browser, each on a page of its own that is
|
|
70
|
+
* closed after its screenshot. A failed capture is reported in its result and
|
|
71
|
+
* the rest carry on; a browser that died is relaunched for the captures left.
|
|
72
|
+
* Results come back in the order of `captures`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function captureBatch(captures: CaptureOptions[], { concurrency, onResult, ...launch }?: BatchOptions): Promise<BatchResult[]>;
|