@peristyle/emdash-plugin-instagram-to-recipe 0.1.2 → 0.1.3
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.js +1 -1
- package/dist/sandbox-entry.d.ts +2 -0
- package/dist/sandbox-entry.js +68 -0
- package/package.json +11 -10
- package/src/image-dimensions.ts +113 -0
- package/src/instagram-recipes.ts +2 -0
- package/src/recipe-content.ts +2 -0
- package/src/sandbox-entry.ts +7 -0
package/dist/index.js
CHANGED
package/dist/sandbox-entry.d.ts
CHANGED
package/dist/sandbox-entry.js
CHANGED
|
@@ -466,6 +466,72 @@ function buildParsedRecipeFromPostLenient(post) {
|
|
|
466
466
|
return recipe;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
// src/image-dimensions.ts
|
|
470
|
+
function decodeImageDimensions(buffer) {
|
|
471
|
+
const b = new Uint8Array(buffer);
|
|
472
|
+
const view = new DataView(buffer);
|
|
473
|
+
if (b.length >= 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
|
|
474
|
+
return valid(view.getUint32(16), view.getUint32(20));
|
|
475
|
+
}
|
|
476
|
+
if (b.length >= 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) {
|
|
477
|
+
return valid(view.getUint16(6, true), view.getUint16(8, true));
|
|
478
|
+
}
|
|
479
|
+
if (b.length >= 30 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) {
|
|
480
|
+
const fourcc = String.fromCharCode(b[12], b[13], b[14], b[15]);
|
|
481
|
+
if (fourcc === "VP8X") {
|
|
482
|
+
const w = 1 + (b[24] | b[25] << 8 | b[26] << 16);
|
|
483
|
+
const h = 1 + (b[27] | b[28] << 8 | b[29] << 16);
|
|
484
|
+
return valid(w, h);
|
|
485
|
+
}
|
|
486
|
+
if (fourcc === "VP8 ") {
|
|
487
|
+
if (b[23] === 157 && b[24] === 1 && b[25] === 42) {
|
|
488
|
+
return valid(
|
|
489
|
+
view.getUint16(26, true) & 16383,
|
|
490
|
+
view.getUint16(28, true) & 16383
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
if (fourcc === "VP8L") {
|
|
496
|
+
if (b[20] !== 47) return null;
|
|
497
|
+
const bits = view.getUint32(21, true);
|
|
498
|
+
return valid((bits & 16383) + 1, (bits >> 14 & 16383) + 1);
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (b.length >= 4 && b[0] === 255 && b[1] === 216) {
|
|
503
|
+
let off = 2;
|
|
504
|
+
while (off + 9 < b.length) {
|
|
505
|
+
if (b[off] !== 255) {
|
|
506
|
+
off++;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const marker = b[off + 1];
|
|
510
|
+
if (marker === 255) {
|
|
511
|
+
off++;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (marker === 1 || marker >= 208 && marker <= 217) {
|
|
515
|
+
off += 2;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const len = view.getUint16(off + 2);
|
|
519
|
+
if (len < 2) return null;
|
|
520
|
+
const isSOF = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
|
|
521
|
+
if (isSOF) {
|
|
522
|
+
if (off + 9 > b.length) return null;
|
|
523
|
+
return valid(view.getUint16(off + 7), view.getUint16(off + 5));
|
|
524
|
+
}
|
|
525
|
+
off += 2 + len;
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
function valid(width, height) {
|
|
532
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
469
535
|
// src/instagram-profile-image.ts
|
|
470
536
|
var IG_APP_ID = "936619743392459";
|
|
471
537
|
var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
@@ -1028,12 +1094,14 @@ async function uploadFeaturedImageBytes(ctx, recipe, bytes, contentType) {
|
|
|
1028
1094
|
return void 0;
|
|
1029
1095
|
}
|
|
1030
1096
|
const filename = recipe.featured_image.filename || `${recipe.suggested_slug}.jpg`;
|
|
1097
|
+
const dims = decodeImageDimensions(bytes);
|
|
1031
1098
|
const uploaded = await ctx.media.upload(filename, contentType, bytes);
|
|
1032
1099
|
return {
|
|
1033
1100
|
provider: "local",
|
|
1034
1101
|
id: uploaded.mediaId,
|
|
1035
1102
|
src: uploaded.url,
|
|
1036
1103
|
mimeType: contentType,
|
|
1104
|
+
...dims ?? {},
|
|
1037
1105
|
meta: { storageKey: uploaded.storageKey }
|
|
1038
1106
|
};
|
|
1039
1107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peristyle/emdash-plugin-instagram-to-recipe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Import Instagram post URLs as recipe drafts in the EmDash admin",
|
|
6
6
|
"publishConfig": {
|
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
"dist",
|
|
22
22
|
"src"
|
|
23
23
|
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"preversion": "pnpm typecheck",
|
|
29
|
+
"version": "pnpm build",
|
|
30
|
+
"postversion": "git push --follow-tags",
|
|
31
|
+
"prepublishOnly": "pnpm build"
|
|
32
|
+
},
|
|
24
33
|
"peerDependencies": {
|
|
25
34
|
"astro": ">=6.0.0-beta.0",
|
|
26
35
|
"emdash": "^0.14.0"
|
|
@@ -29,13 +38,5 @@
|
|
|
29
38
|
"emdash": "^0.14.0",
|
|
30
39
|
"tsup": "^8.0.0",
|
|
31
40
|
"typescript": "^5.0.0"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "tsup",
|
|
35
|
-
"dev": "tsup --watch",
|
|
36
|
-
"typecheck": "tsc --noEmit",
|
|
37
|
-
"preversion": "pnpm typecheck",
|
|
38
|
-
"version": "pnpm build",
|
|
39
|
-
"postversion": "git push --follow-tags"
|
|
40
41
|
}
|
|
41
|
-
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode intrinsic pixel dimensions from raw image bytes.
|
|
3
|
+
*
|
|
4
|
+
* Sandboxed plugins can't use sharp or canvas, so this reads the container
|
|
5
|
+
* headers directly. Supports the formats Instagram serves (JPEG) plus PNG,
|
|
6
|
+
* GIF, and WebP for robustness. Returns null for anything it can't parse —
|
|
7
|
+
* callers should treat dimensions as best-effort metadata.
|
|
8
|
+
*/
|
|
9
|
+
export function decodeImageDimensions(
|
|
10
|
+
buffer: ArrayBuffer,
|
|
11
|
+
): { width: number; height: number } | null {
|
|
12
|
+
const b = new Uint8Array(buffer);
|
|
13
|
+
const view = new DataView(buffer);
|
|
14
|
+
|
|
15
|
+
// PNG: 8-byte signature, then IHDR chunk — width/height at offsets 16/20
|
|
16
|
+
if (
|
|
17
|
+
b.length >= 24 &&
|
|
18
|
+
b[0] === 0x89 &&
|
|
19
|
+
b[1] === 0x50 &&
|
|
20
|
+
b[2] === 0x4e &&
|
|
21
|
+
b[3] === 0x47
|
|
22
|
+
) {
|
|
23
|
+
return valid(view.getUint32(16), view.getUint32(20));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// GIF87a / GIF89a: little-endian u16 logical screen size at offsets 6/8
|
|
27
|
+
if (b.length >= 10 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) {
|
|
28
|
+
return valid(view.getUint16(6, true), view.getUint16(8, true));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// WebP: RIFF container with WEBP form type
|
|
32
|
+
if (
|
|
33
|
+
b.length >= 30 &&
|
|
34
|
+
b[0] === 0x52 &&
|
|
35
|
+
b[1] === 0x49 &&
|
|
36
|
+
b[2] === 0x46 &&
|
|
37
|
+
b[3] === 0x46 &&
|
|
38
|
+
b[8] === 0x57 &&
|
|
39
|
+
b[9] === 0x45 &&
|
|
40
|
+
b[10] === 0x42 &&
|
|
41
|
+
b[11] === 0x50
|
|
42
|
+
) {
|
|
43
|
+
const fourcc = String.fromCharCode(b[12], b[13], b[14], b[15]);
|
|
44
|
+
if (fourcc === "VP8X") {
|
|
45
|
+
// Extended header: 24-bit little-endian canvas size minus one
|
|
46
|
+
const w = 1 + (b[24] | (b[25] << 8) | (b[26] << 16));
|
|
47
|
+
const h = 1 + (b[27] | (b[28] << 8) | (b[29] << 16));
|
|
48
|
+
return valid(w, h);
|
|
49
|
+
}
|
|
50
|
+
if (fourcc === "VP8 ") {
|
|
51
|
+
// Lossy: frame start code 9d 01 2a, then 14-bit dimensions
|
|
52
|
+
if (b[23] === 0x9d && b[24] === 0x01 && b[25] === 0x2a) {
|
|
53
|
+
return valid(
|
|
54
|
+
view.getUint16(26, true) & 0x3fff,
|
|
55
|
+
view.getUint16(28, true) & 0x3fff,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (fourcc === "VP8L") {
|
|
61
|
+
// Lossless: signature byte 0x2f, then 14+14 bits, each minus one
|
|
62
|
+
if (b[20] !== 0x2f) return null;
|
|
63
|
+
const bits = view.getUint32(21, true);
|
|
64
|
+
return valid((bits & 0x3fff) + 1, ((bits >> 14) & 0x3fff) + 1);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// JPEG: scan markers for a start-of-frame (SOF0–SOF15, excluding
|
|
70
|
+
// DHT/JPG/DAC which share the 0xC0 range but aren't frames)
|
|
71
|
+
if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8) {
|
|
72
|
+
let off = 2;
|
|
73
|
+
while (off + 9 < b.length) {
|
|
74
|
+
if (b[off] !== 0xff) {
|
|
75
|
+
off++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const marker = b[off + 1];
|
|
79
|
+
if (marker === 0xff) {
|
|
80
|
+
off++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Standalone markers (no length field)
|
|
84
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
|
|
85
|
+
off += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const len = view.getUint16(off + 2);
|
|
89
|
+
if (len < 2) return null;
|
|
90
|
+
const isSOF =
|
|
91
|
+
marker >= 0xc0 &&
|
|
92
|
+
marker <= 0xcf &&
|
|
93
|
+
marker !== 0xc4 &&
|
|
94
|
+
marker !== 0xc8 &&
|
|
95
|
+
marker !== 0xcc;
|
|
96
|
+
if (isSOF) {
|
|
97
|
+
if (off + 9 > b.length) return null;
|
|
98
|
+
return valid(view.getUint16(off + 7), view.getUint16(off + 5));
|
|
99
|
+
}
|
|
100
|
+
off += 2 + len;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function valid(
|
|
109
|
+
width: number,
|
|
110
|
+
height: number,
|
|
111
|
+
): { width: number; height: number } | null {
|
|
112
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
113
|
+
}
|
package/src/instagram-recipes.ts
CHANGED
package/src/recipe-content.ts
CHANGED
package/src/sandbox-entry.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildParsedRecipeFromPostLenient,
|
|
17
17
|
type InstagramPostForRecipes,
|
|
18
18
|
} from "./instagram-recipes.js";
|
|
19
|
+
import { decodeImageDimensions } from "./image-dimensions.js";
|
|
19
20
|
import {
|
|
20
21
|
fetchInstagramPost,
|
|
21
22
|
instagramBrowserUserAgent,
|
|
@@ -163,6 +164,11 @@ async function uploadFeaturedImageBytes(
|
|
|
163
164
|
const filename =
|
|
164
165
|
recipe.featured_image.filename || `${recipe.suggested_slug}.jpg`;
|
|
165
166
|
|
|
167
|
+
// Decode intrinsic dimensions before upload so the MediaValue stored in
|
|
168
|
+
// content carries width/height — the site needs them for aspect-ratio
|
|
169
|
+
// (CLS protection) and responsive srcset generation.
|
|
170
|
+
const dims = decodeImageDimensions(bytes);
|
|
171
|
+
|
|
166
172
|
const uploaded = await ctx.media.upload(filename, contentType, bytes);
|
|
167
173
|
|
|
168
174
|
return {
|
|
@@ -170,6 +176,7 @@ async function uploadFeaturedImageBytes(
|
|
|
170
176
|
id: uploaded.mediaId,
|
|
171
177
|
src: uploaded.url,
|
|
172
178
|
mimeType: contentType,
|
|
179
|
+
...(dims ?? {}),
|
|
173
180
|
meta: { storageKey: uploaded.storageKey },
|
|
174
181
|
};
|
|
175
182
|
}
|