illustration-search 0.0.1

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/dist/index.cjs ADDED
@@ -0,0 +1,1084 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CONSTANTS: () => CONSTANTS,
24
+ DEFAULT_LICENSE_ALLOWLIST: () => DEFAULT_LICENSE_ALLOWLIST,
25
+ IllustrationError: () => IllustrationError,
26
+ MissingCredentialError: () => MissingCredentialError,
27
+ ProviderError: () => ProviderError,
28
+ RIGHTS_FIELDS: () => RIGHTS_FIELDS,
29
+ RateLimitError: () => RateLimitError,
30
+ SOURCE_RECORDS: () => SOURCE_RECORDS,
31
+ UnknownSourceError: () => UnknownSourceError,
32
+ defaultSources: () => defaultSources,
33
+ defineSource: () => defineSource,
34
+ getSource: () => getSource,
35
+ imageResultSchema: () => imageResultSchema,
36
+ licenseAllowlist: () => licenseAllowlist,
37
+ listSources: () => listSources,
38
+ makeParamTranslator: () => makeParamTranslator,
39
+ makeResult: () => makeResult,
40
+ normalizeLicense: () => normalizeLicense,
41
+ openverse: () => openverse,
42
+ perPageFor: () => perPageFor,
43
+ pexels: () => pexels,
44
+ pixabay: () => pixabay,
45
+ registerSource: () => registerSource,
46
+ search: () => search,
47
+ searchSource: () => searchSource,
48
+ sourceRecord: () => sourceRecord,
49
+ wikimedia: () => wikimedia
50
+ });
51
+ module.exports = __toCommonJS(index_exports);
52
+
53
+ // src/errors.ts
54
+ var IllustrationError = class extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = new.target.name;
58
+ }
59
+ };
60
+ var UnknownSourceError = class extends IllustrationError {
61
+ constructor(source, known) {
62
+ super(`Unknown source ${JSON.stringify(source)}. Registered sources: ${known.join(", ")}.`);
63
+ this.source = source;
64
+ }
65
+ source;
66
+ };
67
+ var MissingCredentialError = class extends IllustrationError {
68
+ constructor(provider, opts = {}) {
69
+ const where = opts.consoleUrl ? ` Get one at ${opts.consoleUrl}.` : "";
70
+ const env = opts.envVar ? ` (the Python side reads ${opts.envVar})` : "";
71
+ super(
72
+ `${provider} needs an API key: pass it as \`credentials: { ${provider}: "\u2026" }\`${env}.${where}`
73
+ );
74
+ this.provider = provider;
75
+ }
76
+ provider;
77
+ };
78
+ var ProviderError = class extends IllustrationError {
79
+ constructor(provider, message, status = null) {
80
+ super(`${provider}: ${message}${status !== null ? ` (HTTP ${status})` : ""}`);
81
+ this.provider = provider;
82
+ this.status = status;
83
+ }
84
+ provider;
85
+ status;
86
+ };
87
+ var RateLimitError = class extends ProviderError {
88
+ };
89
+
90
+ // src/generated/constants.ts
91
+ var CONSTANTS = {
92
+ "canonical_params": [
93
+ "orientation",
94
+ "size",
95
+ "safe",
96
+ "license_type",
97
+ "color",
98
+ "content_type"
99
+ ],
100
+ "default_license_allowlist": [
101
+ "by",
102
+ "by-sa",
103
+ "cc0",
104
+ "pdm",
105
+ "pexels-license",
106
+ "pixabay-license"
107
+ ],
108
+ "defaults": {
109
+ "http_timeout_s": 30,
110
+ "max_pages": 25,
111
+ "n": 10,
112
+ "sources": [
113
+ "openverse"
114
+ ]
115
+ },
116
+ "license_aliases": {
117
+ "cc-0": "cc0",
118
+ "cc-pdm": "pdm",
119
+ "cc-publicdomain": "pdm",
120
+ "cc-zero": "cc0",
121
+ "pd": "pdm",
122
+ "pdm-owner": "pdm",
123
+ "public-domain": "pdm",
124
+ "public-domain-mark": "pdm",
125
+ "publicdomain": "pdm",
126
+ "zero": "cc0"
127
+ },
128
+ "license_normalization_cases": [
129
+ {
130
+ "input": null,
131
+ "output": null
132
+ },
133
+ {
134
+ "input": "",
135
+ "output": null
136
+ },
137
+ {
138
+ "input": " ",
139
+ "output": null
140
+ },
141
+ {
142
+ "input": "by-sa",
143
+ "output": "by-sa"
144
+ },
145
+ {
146
+ "input": "BY",
147
+ "output": "by"
148
+ },
149
+ {
150
+ "input": " BY ",
151
+ "output": "by"
152
+ },
153
+ {
154
+ "input": "cc0",
155
+ "output": "cc0"
156
+ },
157
+ {
158
+ "input": "CC0 1.0",
159
+ "output": "cc0"
160
+ },
161
+ {
162
+ "input": "cc-0",
163
+ "output": "cc0"
164
+ },
165
+ {
166
+ "input": "cc-zero",
167
+ "output": "cc0"
168
+ },
169
+ {
170
+ "input": "cc-by-sa-4.0",
171
+ "output": "by-sa"
172
+ },
173
+ {
174
+ "input": "CC BY-SA 4.0",
175
+ "output": "by-sa"
176
+ },
177
+ {
178
+ "input": "cc-by-nc-nd-2.0",
179
+ "output": "by-nc-nd"
180
+ },
181
+ {
182
+ "input": "cc_by_3.0",
183
+ "output": "by"
184
+ },
185
+ {
186
+ "input": "Pexels License",
187
+ "output": "pexels-license"
188
+ },
189
+ {
190
+ "input": "Pixabay License",
191
+ "output": "pixabay-license"
192
+ },
193
+ {
194
+ "input": "public domain",
195
+ "output": "pdm"
196
+ },
197
+ {
198
+ "input": "Public Domain Mark",
199
+ "output": "pdm"
200
+ },
201
+ {
202
+ "input": "pdm",
203
+ "output": "pdm"
204
+ },
205
+ {
206
+ "input": "cc-pdm",
207
+ "output": "pdm"
208
+ },
209
+ {
210
+ "input": "sampling+",
211
+ "output": "sampling+"
212
+ },
213
+ {
214
+ "input": "by-nc",
215
+ "output": "by-nc"
216
+ },
217
+ {
218
+ "input": "CC-BY-SA-v2.5",
219
+ "output": "by-sa"
220
+ }
221
+ ],
222
+ "provider_console_urls": {
223
+ "pexels": "https://www.pexels.com/api/new/",
224
+ "pixabay": "https://pixabay.com/api/docs/"
225
+ },
226
+ "provider_env_vars": {
227
+ "pexels": "PEXELS_API_KEY",
228
+ "pixabay": "PIXABAY_API_KEY"
229
+ },
230
+ "restriction_tokens": [
231
+ "nc",
232
+ "nd",
233
+ "sampling"
234
+ ],
235
+ "rights_fields": [
236
+ "license",
237
+ "license_url",
238
+ "attribution",
239
+ "source_page_url",
240
+ "author",
241
+ "author_url",
242
+ "cacheable"
243
+ ]
244
+ };
245
+
246
+ // src/licensing.ts
247
+ var ALIASES = CONSTANTS.license_aliases;
248
+ var DEFAULT_LICENSE_ALLOWLIST = CONSTANTS.default_license_allowlist;
249
+ var VERSION_SUFFIX = /[-_ ]v?\d+(?:\.\d+)*$/;
250
+ var CC_PREFIX = /^cc[-_ ]/;
251
+ var SEPARATORS = /[\s_]+/g;
252
+ function normalizeLicense(value) {
253
+ if (!value || !value.trim()) return null;
254
+ let code = value.trim().toLowerCase().replace(SEPARATORS, "-");
255
+ if (Object.hasOwn(ALIASES, code)) return ALIASES[code];
256
+ code = code.replace(VERSION_SUFFIX, "");
257
+ if (Object.hasOwn(ALIASES, code)) return ALIASES[code];
258
+ code = code.replace(CC_PREFIX, "");
259
+ return (Object.hasOwn(ALIASES, code) ? ALIASES[code] : code) || null;
260
+ }
261
+ function licenseAllowlist(results, opts = {}) {
262
+ const allowed = /* @__PURE__ */ new Set();
263
+ for (const code of opts.allow ?? DEFAULT_LICENSE_ALLOWLIST) {
264
+ const normalized = normalizeLicense(code);
265
+ if (normalized !== null) allowed.add(normalized);
266
+ }
267
+ const kept = [];
268
+ for (const result of results) {
269
+ const code = normalizeLicense(result.license);
270
+ if (code !== null && allowed.has(code)) kept.push(result);
271
+ }
272
+ return kept;
273
+ }
274
+
275
+ // src/generated/image-result.ts
276
+ var import_zod = require("zod");
277
+ var imageResultSchema = import_zod.z.object({ "attribution": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).describe("Ready-to-render attribution sentence.").default(null), "author": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "author_url": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "avg_color": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).describe("Dominant-color hint where available.").default(null), "cacheable": import_zod.z.boolean().describe("May the bytes be downloaded/cached to our server?").default(false), "description": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).describe("Alt text / longer description.").default(null), "height": import_zod.z.union([import_zod.z.number().int(), import_zod.z.null()]).default(null), "id": import_zod.z.string().describe("Provider-native identifier."), "license": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).describe("License code or name (e.g. 'by-sa').").default(null), "license_url": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "provider": import_zod.z.string().describe("Source/provider name, e.g. 'openverse'."), "query": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).describe("Canonical query that produced this hit.").default(null), "raw": import_zod.z.record(import_zod.z.string(), import_zod.z.any()).describe("Untranslated provider payload.").default({}), "score": import_zod.z.union([import_zod.z.number(), import_zod.z.null()]).describe("Reserved for Layer-2 rerank; None at Layer 1.").default(null), "source_page_url": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "tags": import_zod.z.array(import_zod.z.string()).default([]), "thumbnail_url": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "title": import_zod.z.union([import_zod.z.string(), import_zod.z.null()]).default(null), "url": import_zod.z.string().describe("Full-resolution image URL."), "width": import_zod.z.union([import_zod.z.number().int(), import_zod.z.null()]).default(null) }).describe("One normalized image hit from any provider.\n\nThe first eight fields plus ``cacheable`` are the cross-provider minimum the\ndesign guarantees; the rest are populated when a provider supplies them.");
278
+
279
+ // src/generated/sources.ts
280
+ var SOURCE_RECORDS = [
281
+ {
282
+ "auth": {
283
+ "format": null,
284
+ "kind": "none",
285
+ "name": null
286
+ },
287
+ "console_url": null,
288
+ "endpoint": "https://api.openverse.org/v1/images/",
289
+ "env_var": null,
290
+ "fixed_params": {},
291
+ "info": {
292
+ "default_cacheable": true,
293
+ "description": "800M+ CC / public-domain images aggregated from 50+ sources.",
294
+ "homepage": "https://openverse.org",
295
+ "license_note": "Per-result CC/PD license; preserve attribution for BY-family.",
296
+ "name": "openverse",
297
+ "rate_limit": "anonymous: 20/min, 200/day",
298
+ "requires_key": false,
299
+ "tags": [
300
+ "open",
301
+ "cc",
302
+ "public-domain",
303
+ "breadth"
304
+ ]
305
+ },
306
+ "max_per_page": 20,
307
+ "min_per_page": 1,
308
+ "name": "openverse",
309
+ "page_param": "page",
310
+ "params": {
311
+ "color": null,
312
+ "content_type": {
313
+ "choices": [
314
+ "illustration",
315
+ "photo"
316
+ ],
317
+ "coerce": true,
318
+ "name": "category"
319
+ },
320
+ "license_type": {
321
+ "choices": null,
322
+ "coerce": false,
323
+ "name": "license_type"
324
+ },
325
+ "orientation": {
326
+ "choices": null,
327
+ "coerce": true,
328
+ "name": "aspect_ratio"
329
+ },
330
+ "safe": {
331
+ "choices": null,
332
+ "coerce": true,
333
+ "name": "mature"
334
+ },
335
+ "size": {
336
+ "choices": [
337
+ "large",
338
+ "medium",
339
+ "small"
340
+ ],
341
+ "coerce": false,
342
+ "name": "size"
343
+ }
344
+ },
345
+ "per_page_param": "page_size",
346
+ "query_param": "q"
347
+ },
348
+ {
349
+ "auth": {
350
+ "format": "{key}",
351
+ "kind": "header",
352
+ "name": "Authorization"
353
+ },
354
+ "console_url": "https://www.pexels.com/api/new/",
355
+ "endpoint": "https://api.pexels.com/v1/search",
356
+ "env_var": "PEXELS_API_KEY",
357
+ "fixed_params": {},
358
+ "info": {
359
+ "default_cacheable": true,
360
+ "description": "Curated high-quality stock photos under the Pexels License.",
361
+ "homepage": "https://www.pexels.com",
362
+ "license_note": "Single Pexels License (commercial OK); credit photographer + backlink.",
363
+ "name": "pexels",
364
+ "rate_limit": "200/hour, 20,000/month",
365
+ "requires_key": true,
366
+ "tags": [
367
+ "stock",
368
+ "photos",
369
+ "keyed"
370
+ ]
371
+ },
372
+ "max_per_page": 80,
373
+ "min_per_page": 1,
374
+ "name": "pexels",
375
+ "page_param": "page",
376
+ "params": {
377
+ "color": {
378
+ "choices": null,
379
+ "coerce": false,
380
+ "name": "color"
381
+ },
382
+ "content_type": null,
383
+ "license_type": null,
384
+ "orientation": {
385
+ "choices": [
386
+ "landscape",
387
+ "portrait",
388
+ "square"
389
+ ],
390
+ "coerce": false,
391
+ "name": "orientation"
392
+ },
393
+ "safe": null,
394
+ "size": {
395
+ "choices": [
396
+ "large",
397
+ "medium",
398
+ "small"
399
+ ],
400
+ "coerce": false,
401
+ "name": "size"
402
+ }
403
+ },
404
+ "per_page_param": "per_page",
405
+ "query_param": "query"
406
+ },
407
+ {
408
+ "auth": {
409
+ "format": "{key}",
410
+ "kind": "query",
411
+ "name": "key"
412
+ },
413
+ "console_url": "https://pixabay.com/api/docs/",
414
+ "endpoint": "https://pixabay.com/api/",
415
+ "env_var": "PIXABAY_API_KEY",
416
+ "fixed_params": {},
417
+ "info": {
418
+ "default_cacheable": true,
419
+ "description": "Free commercial-use images; license permits caching + self-hosting.",
420
+ "homepage": "https://pixabay.com",
421
+ "license_note": "Single Pixabay Content License; download-to-own-server (no hotlink).",
422
+ "name": "pixabay",
423
+ "rate_limit": "100/60s (raisable); responses must be cached 24h",
424
+ "requires_key": true,
425
+ "tags": [
426
+ "stock",
427
+ "free-commercial",
428
+ "keyed",
429
+ "cacheable"
430
+ ]
431
+ },
432
+ "max_per_page": 200,
433
+ "min_per_page": 3,
434
+ "name": "pixabay",
435
+ "page_param": "page",
436
+ "params": {
437
+ "color": {
438
+ "choices": null,
439
+ "coerce": false,
440
+ "name": "colors"
441
+ },
442
+ "content_type": {
443
+ "choices": [
444
+ "illustration",
445
+ "photo",
446
+ "vector"
447
+ ],
448
+ "coerce": false,
449
+ "name": "image_type"
450
+ },
451
+ "license_type": null,
452
+ "orientation": {
453
+ "choices": [
454
+ "landscape",
455
+ "portrait",
456
+ "square"
457
+ ],
458
+ "coerce": true,
459
+ "name": "orientation"
460
+ },
461
+ "safe": {
462
+ "choices": null,
463
+ "coerce": true,
464
+ "name": "safesearch"
465
+ },
466
+ "size": {
467
+ "choices": [
468
+ "large",
469
+ "medium",
470
+ "small"
471
+ ],
472
+ "coerce": true,
473
+ "name": "min_width"
474
+ }
475
+ },
476
+ "per_page_param": "per_page",
477
+ "query_param": "q"
478
+ },
479
+ {
480
+ "auth": {
481
+ "format": null,
482
+ "kind": "none",
483
+ "name": null
484
+ },
485
+ "console_url": null,
486
+ "endpoint": "https://commons.wikimedia.org/w/api.php",
487
+ "env_var": null,
488
+ "fixed_params": {
489
+ "action": "query",
490
+ "format": "json",
491
+ "formatversion": "2",
492
+ "generator": "search",
493
+ "gsrnamespace": "6",
494
+ "iiprop": "url|extmetadata|size|mime|user",
495
+ "iiurlwidth": "320",
496
+ "prop": "imageinfo"
497
+ },
498
+ "info": {
499
+ "default_cacheable": true,
500
+ "description": "140M+ free media files with deep per-file metadata; no key.",
501
+ "homepage": "https://commons.wikimedia.org",
502
+ "license_note": "Per-file license varies (CC/PD; some share-alike) \u2014 gate for commercial use.",
503
+ "name": "wikimedia",
504
+ "rate_limit": "etiquette-bound (descriptive User-Agent required)",
505
+ "requires_key": false,
506
+ "tags": [
507
+ "open",
508
+ "historical",
509
+ "editorial",
510
+ "breadth",
511
+ "no-key"
512
+ ]
513
+ },
514
+ "max_per_page": 50,
515
+ "min_per_page": 1,
516
+ "name": "wikimedia",
517
+ "page_param": "page",
518
+ "params": {
519
+ "color": null,
520
+ "content_type": null,
521
+ "license_type": null,
522
+ "orientation": null,
523
+ "safe": null,
524
+ "size": null
525
+ },
526
+ "per_page_param": "page_size",
527
+ "query_param": "gsrsearch"
528
+ }
529
+ ];
530
+
531
+ // src/translation.ts
532
+ function makeParamTranslator(paramMap) {
533
+ return (canonical) => {
534
+ const native = {};
535
+ const dropped = [];
536
+ for (const [key, value] of Object.entries(canonical)) {
537
+ if (value === null || value === void 0) continue;
538
+ const spec = Object.hasOwn(paramMap, key) ? paramMap[key] : null;
539
+ if (spec === null || spec === void 0) {
540
+ dropped.push(key);
541
+ continue;
542
+ }
543
+ const applied = applySpec(key, value, spec);
544
+ if (applied === null) {
545
+ dropped.push(key);
546
+ continue;
547
+ }
548
+ native[applied.name] = applied.value;
549
+ }
550
+ return { native, dropped };
551
+ };
552
+ }
553
+ function applySpec(key, value, spec) {
554
+ if (typeof spec === "string") return { name: spec, value };
555
+ if (typeof spec === "function") return { name: key, value: spec(value) };
556
+ const name = spec.name ?? key;
557
+ if (spec.choices !== void 0) {
558
+ const choices = spec.choices instanceof Set ? spec.choices : new Set(spec.choices);
559
+ if (typeof value !== "string" || !choices.has(value)) return null;
560
+ }
561
+ return { name, value: spec.coerce ? spec.coerce(value) : value };
562
+ }
563
+
564
+ // src/source.ts
565
+ function sourceRecord(name) {
566
+ const record = SOURCE_RECORDS.find((r) => r.name === name);
567
+ if (!record) {
568
+ throw new Error(
569
+ `No exported record for source ${JSON.stringify(name)}: register it on the Python side and run \`illustration export-schema\`.`
570
+ );
571
+ }
572
+ return record;
573
+ }
574
+ function defineSource(name, hooks) {
575
+ const record = sourceRecord(name);
576
+ const pageParams = hooks.pageParams?.bind(hooks) ?? ((page, perPage) => ({
577
+ [record.page_param]: page,
578
+ [record.per_page_param]: perPage
579
+ }));
580
+ const queryParams = hooks.queryParams?.bind(hooks) ?? ((query, page, perPage) => ({
581
+ [record.query_param]: query,
582
+ ...pageParams(page, perPage)
583
+ }));
584
+ return {
585
+ ...hooks,
586
+ name,
587
+ record,
588
+ translate: makeParamTranslator(hooks.paramMap),
589
+ pageParams,
590
+ queryParams
591
+ };
592
+ }
593
+ function required(item, key) {
594
+ const value = item[key];
595
+ if (value === void 0 || value === null) throw new Error(`item has no ${key}`);
596
+ return value;
597
+ }
598
+ function makeResult(fields) {
599
+ const defined = {};
600
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) defined[k] = v;
601
+ return imageResultSchema.parse(defined);
602
+ }
603
+ function perPageFor(record, n) {
604
+ return Math.max(record.min_per_page, Math.min(n, record.max_per_page));
605
+ }
606
+ async function searchSource(source, query, opts = {}) {
607
+ if (!query) throw new Error("query must be a non-empty string");
608
+ const n = opts.n ?? CONSTANTS.defaults.n;
609
+ const { record } = source;
610
+ const apiKey = opts.auth === "transport" ? null : opts.apiKey ?? null;
611
+ if (record.info.requires_key && !apiKey && opts.auth !== "transport") {
612
+ throw new MissingCredentialError(source.name, {
613
+ envVar: record.env_var,
614
+ consoleUrl: record.console_url
615
+ });
616
+ }
617
+ const { native } = source.translate(opts.canonical ?? {});
618
+ Object.assign(native, opts.nativeParams ?? {});
619
+ const results = [];
620
+ const perPage = perPageFor(record, n);
621
+ let page = 1;
622
+ while (results.length < n && page <= CONSTANTS.defaults.max_pages) {
623
+ const params = {
624
+ ...record.fixed_params,
625
+ ...native,
626
+ ...source.queryParams(query, page, perPage)
627
+ };
628
+ const response = await get(source, params, { ...opts, apiKey });
629
+ const items = Array.from(source.items(response));
630
+ if (items.length === 0) break;
631
+ for (const item of items) {
632
+ const normalized = safeNormalize(source, item, query);
633
+ if (normalized !== null) {
634
+ results.push(normalized);
635
+ if (results.length >= n) break;
636
+ }
637
+ }
638
+ if (items.length < perPage) break;
639
+ page += 1;
640
+ }
641
+ return results.slice(0, n);
642
+ }
643
+ function safeNormalize(source, item, query) {
644
+ try {
645
+ return source.normalize(item, query);
646
+ } catch {
647
+ return null;
648
+ }
649
+ }
650
+ function requestUrl(source, params, apiKey) {
651
+ const url = new URL(source.record.endpoint);
652
+ const merged = { ...params };
653
+ const { auth } = source.record;
654
+ if (auth.kind === "query" && apiKey && auth.name) {
655
+ merged[auth.name] = (auth.format ?? "{key}").replace("{key}", apiKey);
656
+ }
657
+ for (const [key, value] of Object.entries(merged)) {
658
+ if (value === null || value === void 0) continue;
659
+ url.searchParams.set(key, String(value));
660
+ }
661
+ return url;
662
+ }
663
+ function requestHeaders(source, apiKey, userAgent) {
664
+ const headers = new Headers({ Accept: "application/json" });
665
+ if (userAgent) headers.set("Api-User-Agent", userAgent);
666
+ const { auth } = source.record;
667
+ if (auth.kind === "header" && apiKey && auth.name) {
668
+ headers.set(auth.name, (auth.format ?? "{key}").replace("{key}", apiKey));
669
+ }
670
+ return headers;
671
+ }
672
+ async function get(source, params, opts) {
673
+ const doFetch = opts.fetch ?? globalThis.fetch;
674
+ let response;
675
+ try {
676
+ response = await doFetch(requestUrl(source, params, opts.apiKey), {
677
+ method: "GET",
678
+ headers: requestHeaders(source, opts.apiKey, opts.userAgent),
679
+ signal: opts.signal
680
+ });
681
+ } catch (e) {
682
+ throw new ProviderError(source.name, `request failed: ${e.message ?? e}`);
683
+ }
684
+ const { status } = response;
685
+ if (status === 429) throw new RateLimitError(source.name, "rate limit exceeded", 429);
686
+ if (status === 401 || status === 403) {
687
+ throw new ProviderError(source.name, "authentication failed (check API key)", status);
688
+ }
689
+ if (status >= 400) throw new ProviderError(source.name, await shortBody(response), status);
690
+ let decoded;
691
+ try {
692
+ decoded = await response.json();
693
+ } catch {
694
+ throw new ProviderError(source.name, "response was not valid JSON", status);
695
+ }
696
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
697
+ throw new ProviderError(source.name, `unexpected response type: ${Array.isArray(decoded) ? "array" : typeof decoded}`, status);
698
+ }
699
+ return decoded;
700
+ }
701
+ async function shortBody(response) {
702
+ try {
703
+ const text = (await response.text()).trim();
704
+ return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text || `HTTP ${response.status}`;
705
+ } catch {
706
+ return `HTTP ${response.status}`;
707
+ }
708
+ }
709
+
710
+ // src/providers/openverse.ts
711
+ var ORIENTATION_TO_ASPECT = {
712
+ landscape: "wide",
713
+ portrait: "tall",
714
+ square: "square"
715
+ };
716
+ var CONTENT_TYPE_TO_CATEGORY = {
717
+ photo: "photograph",
718
+ illustration: "illustration"
719
+ };
720
+ var openverse = defineSource("openverse", {
721
+ paramMap: {
722
+ orientation: {
723
+ name: "aspect_ratio",
724
+ coerce: (o) => ORIENTATION_TO_ASPECT[String(o)] ?? o
725
+ },
726
+ size: { name: "size", choices: ["large", "medium", "small"] },
727
+ safe: { name: "mature", coerce: (safe) => !safe },
728
+ license_type: "license_type",
729
+ // content_type: Openverse `category` has no 'vector' → only photo/illustration
730
+ content_type: {
731
+ name: "category",
732
+ choices: ["photo", "illustration"],
733
+ coerce: (ct) => CONTENT_TYPE_TO_CATEGORY[String(ct)]
734
+ },
735
+ color: null
736
+ // Openverse has no color filter (explicitly unsupported)
737
+ },
738
+ items(response) {
739
+ return response.results ?? [];
740
+ },
741
+ normalize(item, query) {
742
+ const tags = (item.tags ?? []).filter((t) => typeof t === "object" && t !== null && !!t.name).map((t) => String(t.name));
743
+ const title = item.title ?? null;
744
+ return makeResult({
745
+ provider: "openverse",
746
+ id: String(required(item, "id")),
747
+ url: String(required(item, "url")),
748
+ thumbnail_url: item.thumbnail ?? null,
749
+ width: item.width ?? null,
750
+ height: item.height ?? null,
751
+ title,
752
+ description: title,
753
+ // Openverse exposes no separate description
754
+ tags,
755
+ license: item.license ?? null,
756
+ license_url: item.license_url ?? null,
757
+ attribution: item.attribution ?? null,
758
+ source_page_url: item.foreign_landing_url ?? null,
759
+ author: item.creator ?? null,
760
+ author_url: item.creator_url ?? null,
761
+ cacheable: true,
762
+ // CC / public-domain media is cacheable (preserve attribution)
763
+ avg_color: null,
764
+ query,
765
+ raw: { ...item }
766
+ });
767
+ }
768
+ });
769
+
770
+ // src/providers/pexels.ts
771
+ var PEXELS_LICENSE = "Pexels License";
772
+ var PEXELS_LICENSE_URL = "https://www.pexels.com/license/";
773
+ var pexels = defineSource("pexels", {
774
+ paramMap: {
775
+ orientation: { name: "orientation", choices: ["landscape", "portrait", "square"] },
776
+ size: { name: "size", choices: ["large", "medium", "small"] },
777
+ // `safe` is a no-op (curated corpus); `license_type` has no equivalent (single licence).
778
+ safe: null,
779
+ license_type: null,
780
+ color: "color",
781
+ // a named or #hex colour
782
+ content_type: null
783
+ // photos only
784
+ },
785
+ items(response) {
786
+ return response.photos ?? [];
787
+ },
788
+ normalize(item, query) {
789
+ const src = item.src ?? {};
790
+ const photographer = item.photographer ?? null;
791
+ const url = src.original || src.large2x || src.large || null;
792
+ if (!url) throw new Error("item has no image url");
793
+ return makeResult({
794
+ provider: "pexels",
795
+ id: String(required(item, "id")),
796
+ url,
797
+ thumbnail_url: src.tiny || src.medium || null,
798
+ width: item.width ?? null,
799
+ height: item.height ?? null,
800
+ title: null,
801
+ description: item.alt || null,
802
+ tags: [],
803
+ // Pexels returns no tags
804
+ license: PEXELS_LICENSE,
805
+ license_url: PEXELS_LICENSE_URL,
806
+ attribution: photographer ? `Photo by ${photographer} on Pexels` : "Photo from Pexels",
807
+ source_page_url: item.url ?? null,
808
+ // the Pexels web page
809
+ author: photographer,
810
+ author_url: item.photographer_url ?? null,
811
+ cacheable: true,
812
+ // Pexels License permits caching for display
813
+ avg_color: item.avg_color ?? null,
814
+ query,
815
+ raw: { ...item }
816
+ });
817
+ }
818
+ });
819
+
820
+ // src/providers/pixabay.ts
821
+ var PIXABAY_LICENSE = "Pixabay License";
822
+ var PIXABAY_LICENSE_URL = "https://pixabay.com/service/license-summary/";
823
+ var ORIENTATION = {
824
+ landscape: "horizontal",
825
+ portrait: "vertical",
826
+ square: "all"
827
+ };
828
+ var SIZE_MIN_WIDTH = { large: 1920, medium: 1280, small: 640 };
829
+ var pixabay = defineSource("pixabay", {
830
+ paramMap: {
831
+ orientation: {
832
+ name: "orientation",
833
+ choices: ["landscape", "portrait", "square"],
834
+ coerce: (o) => ORIENTATION[String(o)]
835
+ },
836
+ size: {
837
+ name: "min_width",
838
+ choices: ["large", "medium", "small"],
839
+ coerce: (s) => SIZE_MIN_WIDTH[String(s)]
840
+ },
841
+ safe: { name: "safesearch", coerce: (safe) => safe ? "true" : "false" },
842
+ license_type: null,
843
+ // single Pixabay License
844
+ color: "colors",
845
+ content_type: { name: "image_type", choices: ["photo", "illustration", "vector"] }
846
+ },
847
+ items(response) {
848
+ return response.hits ?? [];
849
+ },
850
+ normalize(item, query) {
851
+ const user = item.user ?? null;
852
+ const tags = String(item.tags ?? "").split(",").map((t) => t.trim()).filter((t) => t.length > 0);
853
+ const url = item.largeImageURL || item.webformatURL || item.imageURL || null;
854
+ if (!url) throw new Error("item has no image url");
855
+ return makeResult({
856
+ provider: "pixabay",
857
+ id: String(required(item, "id")),
858
+ url,
859
+ thumbnail_url: item.previewURL ?? null,
860
+ width: item.imageWidth ?? null,
861
+ height: item.imageHeight ?? null,
862
+ title: null,
863
+ description: null,
864
+ tags,
865
+ license: PIXABAY_LICENSE,
866
+ license_url: PIXABAY_LICENSE_URL,
867
+ attribution: user ? `Image by ${user} on Pixabay` : "Image from Pixabay",
868
+ source_page_url: item.pageURL ?? null,
869
+ author: user,
870
+ author_url: null,
871
+ cacheable: true,
872
+ // licence permits caching / self-hosting
873
+ avg_color: null,
874
+ query,
875
+ raw: { ...item }
876
+ });
877
+ }
878
+ });
879
+
880
+ // src/providers/wikimedia.ts
881
+ var import_entities = require("entities");
882
+ var TAG_RE = /<[^>]+>/g;
883
+ var HREF_RE = /href=["']([^"']+)["']/i;
884
+ var CATEGORY_PREFIX = "category:";
885
+ var FILE_PREFIX = "file:";
886
+ function isFileTitle(query) {
887
+ return query.trim().toLowerCase().startsWith(FILE_PREFIX);
888
+ }
889
+ function isCategory(query) {
890
+ return query.trim().toLowerCase().startsWith(CATEGORY_PREFIX);
891
+ }
892
+ function categoryTitle(query) {
893
+ return `Category:${query.trim().slice(CATEGORY_PREFIX.length).trim()}`;
894
+ }
895
+ function fileTitle(query) {
896
+ return `File:${query.trim().slice(FILE_PREFIX.length).trim()}`;
897
+ }
898
+ function stripHtml(value) {
899
+ if (!value) return null;
900
+ const text = (0, import_entities.decodeHTML)(value.replace(TAG_RE, "")).trim();
901
+ return text || null;
902
+ }
903
+ function firstHref(value) {
904
+ if (!value) return null;
905
+ const m = HREF_RE.exec(value);
906
+ if (!m) return null;
907
+ const url = m[1];
908
+ return url.startsWith("//") ? `https:${url}` : url;
909
+ }
910
+ function buildAttribution(author, licenseShort) {
911
+ if (author && licenseShort) return `${author} / ${licenseShort}, via Wikimedia Commons`;
912
+ if (author) return `${author}, via Wikimedia Commons`;
913
+ if (licenseShort) return `${licenseShort}, via Wikimedia Commons`;
914
+ return null;
915
+ }
916
+ var wikimedia = defineSource("wikimedia", {
917
+ paramMap: {},
918
+ // Commons exposes no canonical search-time filters here
919
+ pageParams(page, perPage) {
920
+ return { gsrlimit: perPage, gsroffset: (page - 1) * perPage };
921
+ },
922
+ queryParams(query, page, perPage) {
923
+ if (isFileTitle(query)) {
924
+ return {
925
+ generator: null,
926
+ // removed by the request builder; this is a direct lookup
927
+ titles: query.split("|").filter((part) => part.trim()).map(fileTitle).join("|")
928
+ };
929
+ }
930
+ if (!isCategory(query)) {
931
+ return { gsrsearch: query, ...this.pageParams(page, perPage) };
932
+ }
933
+ return {
934
+ generator: "categorymembers",
935
+ gcmtitle: categoryTitle(query),
936
+ gcmtype: "file",
937
+ gcmlimit: perPage
938
+ };
939
+ },
940
+ items(response) {
941
+ const pages = response.query?.pages ?? [];
942
+ const items = Array.isArray(pages) ? pages : Object.values(pages);
943
+ return items.sort(
944
+ (a, b) => (a.index ?? 0) - (b.index ?? 0)
945
+ );
946
+ },
947
+ normalize(item, query) {
948
+ const info = (item.imageinfo ?? [{}])[0] ?? {};
949
+ if (!String(info.mime ?? "").startsWith("image/")) throw new Error("not an image file");
950
+ const em = info.extmetadata ?? {};
951
+ const emValue = (key) => {
952
+ const v = em[key];
953
+ return typeof v === "object" && v !== null && typeof v.value === "string" ? v.value : null;
954
+ };
955
+ const artistHtml = emValue("Artist");
956
+ let title = item.title ?? "";
957
+ if (title.startsWith("File:")) title = title.slice("File:".length);
958
+ const licenseShort = emValue("LicenseShortName");
959
+ return makeResult({
960
+ provider: "wikimedia",
961
+ id: String(required(item, "pageid")),
962
+ url: String(required(info, "url")),
963
+ thumbnail_url: info.thumburl ?? null,
964
+ width: info.width ?? null,
965
+ height: info.height ?? null,
966
+ title: title || null,
967
+ description: stripHtml(emValue("ImageDescription")),
968
+ tags: [],
969
+ // Commons categories are noisy/HTML; omitted at this layer
970
+ license: emValue("License") || licenseShort,
971
+ license_url: emValue("LicenseUrl"),
972
+ attribution: stripHtml(emValue("Attribution")) || buildAttribution(stripHtml(artistHtml), licenseShort),
973
+ source_page_url: info.descriptionurl ?? null,
974
+ author: stripHtml(artistHtml),
975
+ author_url: firstHref(artistHtml),
976
+ cacheable: true,
977
+ // free content; preserve attribution (gate per-file licence)
978
+ avg_color: null,
979
+ query,
980
+ raw: { ...item }
981
+ });
982
+ }
983
+ });
984
+
985
+ // src/registry.ts
986
+ var REGISTRY = /* @__PURE__ */ new Map();
987
+ function registerSource(source) {
988
+ REGISTRY.set(source.name, source);
989
+ return source;
990
+ }
991
+ function getSource(name) {
992
+ const source = REGISTRY.get(name);
993
+ if (!source) throw new UnknownSourceError(name, listSources());
994
+ return source;
995
+ }
996
+ function listSources() {
997
+ return Array.from(REGISTRY.keys());
998
+ }
999
+ function defaultSources() {
1000
+ return [...CONSTANTS.defaults.sources];
1001
+ }
1002
+ for (const source of [openverse, wikimedia, pexels, pixabay]) registerSource(source);
1003
+
1004
+ // src/search.ts
1005
+ async function search(query, opts = {}) {
1006
+ if (!query || typeof query !== "string") throw new Error("query must be a non-empty string");
1007
+ const n = opts.n ?? CONSTANTS.defaults.n;
1008
+ if (!(n > 0)) throw new Error(`n must be a positive integer, got ${n}`);
1009
+ const media = opts.media ?? "image";
1010
+ if (media !== "image") {
1011
+ throw new IllustrationError(
1012
+ `no ${media} sources are registered yet (illustration#33); only media: "image" is available`
1013
+ );
1014
+ }
1015
+ const names = resolveSourceNames(opts.source);
1016
+ const canonical = {
1017
+ orientation: opts.orientation ?? null,
1018
+ size: opts.size ?? null,
1019
+ safe: opts.safe ?? true,
1020
+ license_type: opts.licenseType ?? null,
1021
+ color: opts.color ?? null,
1022
+ content_type: opts.contentType ?? null
1023
+ };
1024
+ const perSource = await Promise.all(
1025
+ names.map(
1026
+ (name) => searchSource(getSource(name), query, {
1027
+ n,
1028
+ canonical,
1029
+ apiKey: opts.credentials?.[name] ?? null,
1030
+ auth: opts.auth,
1031
+ nativeParams: opts.providerParams?.[name] ?? null,
1032
+ fetch: opts.fetch,
1033
+ signal: opts.signal,
1034
+ userAgent: opts.userAgent
1035
+ })
1036
+ )
1037
+ );
1038
+ let results = perSource.flat();
1039
+ const allow = opts.licenseAllow === true ? null : opts.licenseAllow ? [...opts.licenseAllow] : void 0;
1040
+ if (opts.licenseAllow === true || allow && allow.length > 0) {
1041
+ results = licenseAllowlist(results, { allow });
1042
+ }
1043
+ return results;
1044
+ }
1045
+ function resolveSourceNames(source) {
1046
+ if (source === void 0 || source === null) return defaultSources();
1047
+ if (typeof source === "string") return [source];
1048
+ const names = [...source];
1049
+ if (names.length === 0) throw new Error("source list is empty; pass at least one source name");
1050
+ return names;
1051
+ }
1052
+
1053
+ // src/index.ts
1054
+ var RIGHTS_FIELDS = CONSTANTS.rights_fields;
1055
+ // Annotate the CommonJS export names for ESM import in node:
1056
+ 0 && (module.exports = {
1057
+ CONSTANTS,
1058
+ DEFAULT_LICENSE_ALLOWLIST,
1059
+ IllustrationError,
1060
+ MissingCredentialError,
1061
+ ProviderError,
1062
+ RIGHTS_FIELDS,
1063
+ RateLimitError,
1064
+ SOURCE_RECORDS,
1065
+ UnknownSourceError,
1066
+ defaultSources,
1067
+ defineSource,
1068
+ getSource,
1069
+ imageResultSchema,
1070
+ licenseAllowlist,
1071
+ listSources,
1072
+ makeParamTranslator,
1073
+ makeResult,
1074
+ normalizeLicense,
1075
+ openverse,
1076
+ perPageFor,
1077
+ pexels,
1078
+ pixabay,
1079
+ registerSource,
1080
+ search,
1081
+ searchSource,
1082
+ sourceRecord,
1083
+ wikimedia
1084
+ });