dredless 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dredless",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A fully-featured and from-scratch headless drednot.io client implementation written in pure js.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/certainhuman/dredless#readme",
@@ -152,6 +152,7 @@
152
152
  }
153
153
  },
154
154
  "dependencies": {
155
+ "sharp": "^0.35.4",
155
156
  "ws": "^8.18.0"
156
157
  },
157
158
  "files": [
package/src/index.d.ts CHANGED
@@ -26,12 +26,16 @@ export interface ShipList {
26
26
  export interface InviteMetadata {
27
27
  shipId: string | null;
28
28
  name: string | null;
29
- color: string | null;
29
+ /** CSS RGB color sampled from the preview image's top-left pixel, when available. */
30
+ color?: string | null;
30
31
  previewImageUrl: string | null;
31
32
  hostname: string;
32
33
  inviteCode: string;
33
34
  }
34
- export interface InviteFetchOptions { fetchImpl?: typeof fetch; }
35
+ export interface InviteFetchOptions {
36
+ fetchImpl?: typeof fetch;
37
+ includeColor?: boolean;
38
+ }
35
39
  export function fetchInviteMetadata(invite: string | URL, options?: InviteFetchOptions): Promise<InviteMetadata>;
36
40
  export function parseInviteMetadata(html: string): InviteMetadata;
37
41
 
@@ -1,11 +1,15 @@
1
1
  export interface InviteMetadata {
2
2
  shipId: string | null;
3
3
  name: string | null;
4
- color: string | null;
4
+ /** CSS RGB color sampled from the preview image's top-left pixel, when available. */
5
+ color?: string | null;
5
6
  previewImageUrl: string | null;
6
7
  hostname: string;
7
8
  inviteCode: string;
8
9
  }
9
- export interface InviteFetchOptions { fetchImpl?: typeof fetch; }
10
+ export interface InviteFetchOptions {
11
+ fetchImpl?: typeof fetch;
12
+ includeColor?: boolean;
13
+ }
10
14
  export function fetchInviteMetadata(invite: string | URL, options?: InviteFetchOptions): Promise<InviteMetadata>;
11
15
  export function parseInviteMetadata(html: string): InviteMetadata;
@@ -1,10 +1,48 @@
1
- import {getFetch} from "../runtime.js";
1
+ import {getFetch, isBrowser} from "../runtime.js";
2
2
 
3
- export async function fetchInviteMetadata(invite, {fetchImpl = null} = {}) {
3
+ export async function fetchInviteMetadata(invite, {fetchImpl = null, includeColor = false} = {}) {
4
4
  const url = normalizeInviteUrl(invite);
5
- const response = await (fetchImpl || getFetch())(url.href, {headers: {accept: "text/html"}});
5
+ const fetcher = fetchImpl || getFetch();
6
+ const response = await fetcher(url.href, {headers: {accept: "text/html"}});
6
7
  if (!response.ok) throw new Error(`Invite metadata request failed: ${response.status} ${response.statusText}`);
7
- return parseInviteMetadata(await response.text());
8
+ const metadata = parseInviteMetadata(await response.text());
9
+ if (!includeColor || !metadata.previewImageUrl) return metadata;
10
+ return Object.freeze({...metadata, color: await fetchTopLeftColor(metadata.previewImageUrl, fetcher)});
11
+ }
12
+
13
+ async function fetchTopLeftColor(imageUrl, fetcher) {
14
+ try {
15
+ let response = await fetcher(imageUrl, {headers: {range: "bytes=0-1023"}});
16
+ if (!response.ok) return null;
17
+ const buffer = await response.arrayBuffer();
18
+ try {
19
+ return await decodeImageColor(buffer);
20
+ } catch (error) {
21
+ if (response.status !== 206) throw error;
22
+ response = await fetcher(imageUrl);
23
+ if (!response.ok) return null;
24
+ return await decodeImageColor(await response.arrayBuffer());
25
+ }
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async function decodeImageColor(buffer) {
32
+ if (isBrowser()) {
33
+ const bitmap = await createImageBitmap(new Blob([buffer]));
34
+ const canvas = typeof OffscreenCanvas === "function" ? new OffscreenCanvas(1, 1) : document.createElement("canvas");
35
+ canvas.width = canvas.height = 1;
36
+ const context = canvas.getContext("2d");
37
+ context.drawImage(bitmap, 0, 0, 1, 1);
38
+ const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
39
+ bitmap.close();
40
+ return `rgb(${red},${green},${blue})`;
41
+ }
42
+ const sharpModule = await import("sharp");
43
+ const sharp = sharpModule.default || sharpModule;
44
+ const {data} = await sharp(buffer).extract({left: 0, top: 0, width: 1, height: 1}).raw().toBuffer({resolveWithObject: true});
45
+ return `rgb(${data[0]},${data[1]},${data[2]})`;
8
46
  }
9
47
 
10
48
  export function parseInviteMetadata(html) {
@@ -15,7 +53,6 @@ export function parseInviteMetadata(html) {
15
53
  return Object.freeze({
16
54
  shipId: parsedTitle.shipId,
17
55
  name: parsedTitle.name,
18
- color: metaContent(html, "theme-color") || metaContent(html, "color") || null,
19
56
  previewImageUrl: preview ? new URL(preview, url).href : null,
20
57
  hostname: url.hostname,
21
58
  inviteCode: url.pathname.split("/").filter(Boolean).at(-1)
@@ -29,7 +66,7 @@ function normalizeInviteUrl(invite) {
29
66
  }
30
67
 
31
68
  function parseInviteTitle(title) {
32
- const match = title.match(/^Invite:\s*(.*?)\s*\{([^{}]+)}(?:\s*-\s*drednot\.io)?\s*$/i);
69
+ const match = title.match(/^Invite:\s?(.*?)\s*\{([^{}]+)}(?:\s*-\s*drednot\.io)?\s*$/i);
33
70
  return match ? {name: decodeHtml(match[1]), shipId: decodeHtml(match[2])} : {name: null, shipId: null};
34
71
  }
35
72