@asterisk-labs/rumi 0.0.0-stage → 0.27.0-alpha.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.
@@ -0,0 +1,145 @@
1
+ :root {
2
+ --bg: #f4f6f2;
3
+ --panel: #ffffff;
4
+ --ink: #20251f;
5
+ --muted: #677067;
6
+ --line: #d7ddd3;
7
+ --line-strong: #aeb8aa;
8
+ --accent: #0f766e;
9
+ --accent-soft: #e3f3ef;
10
+ --shadow: 0 8px 22px rgba(31, 39, 27, .1);
11
+ --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
12
+ }
13
+
14
+ * { box-sizing: border-box; }
15
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
16
+ body { color: var(--ink); background: var(--bg); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
17
+ button, input, select { font: inherit; }
18
+ button:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible { outline: 3px solid rgba(15, 118, 110, .22); outline-offset: 2px; }
19
+
20
+ .topbar {
21
+ position: relative; z-index: 1000; height: 66px; display: grid;
22
+ grid-template-columns: 180px minmax(360px, 760px) 150px; align-items: center;
23
+ justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--line);
24
+ padding: 0 18px; background: rgba(255, 255, 255, .97);
25
+ }
26
+ .brand { display: inline-flex; align-items: center; gap: 10px; width: fit-content; color: inherit; text-decoration: none; }
27
+ .brand img { width: 36px; height: 36px; }
28
+ .brand span { min-width: 70px; }
29
+ .brand strong { display: block; font-size: 16px; letter-spacing: .12em; line-height: 1; white-space: nowrap; }
30
+ .brand small { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; white-space: nowrap; }
31
+
32
+ .source-picker { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 7px; min-width: 0; }
33
+ .source-picker input[type="text"] { min-width: 0; width: 100%; height: 38px; border: 1px solid var(--line-strong); border-radius: 2px; padding: 0 10px; color: var(--ink); background: #fff; font: 12px/1 var(--mono); }
34
+ .source-picker input:focus { border-color: var(--accent); outline: 3px solid rgba(15, 118, 110, .12); }
35
+ .source-picker button, .file-button { height: 38px; display: inline-grid; place-items: center; border: 1px solid var(--line-strong); border-radius: 2px; padding: 0 12px; color: var(--ink); background: #fff; cursor: pointer; font: 750 10px/1 var(--mono); letter-spacing: .05em; text-transform: uppercase; white-space: nowrap; }
36
+ .source-picker button:hover, .file-button:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
37
+ .source-picker .primary { border-color: var(--accent); color: #fff; background: var(--accent); }
38
+ .source-picker .primary:hover { color: #fff; background: #0b5f59; }
39
+ .file-button input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
40
+ .status { justify-self: end; display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font: 11px/1 var(--mono); }
41
+ .status i { width: 7px; height: 7px; border-radius: 50%; background: var(--line-strong); }
42
+ .status.ready i { background: var(--accent); }
43
+ .status.loading i { background: #d97706; animation: blink 1s infinite; }
44
+ .status.error i { background: #b42318; }
45
+ @keyframes blink { 50% { opacity: .28; } }
46
+
47
+ main { position: relative; height: calc(100% - 66px); }
48
+ #map { width: 100%; height: 100%; background: #e7ebe3; }
49
+ .maplibregl-map { font-family: inherit; }
50
+ .maplibregl-ctrl-bottom-right { right: 8px; bottom: 8px; }
51
+ .maplibregl-ctrl-group { border: 1px solid var(--line-strong); border-radius: 2px; box-shadow: none; overflow: hidden; }
52
+ .maplibregl-ctrl-group button { width: 30px; height: 30px; background-color: rgba(255, 255, 255, .96); }
53
+ .maplibregl-ctrl-group button:hover { background-color: var(--accent-soft); }
54
+
55
+ .message { position: absolute; z-index: 800; top: 16px; left: 50%; transform: translateX(-50%); max-width: min(520px, calc(100% - 40px)); border: 1px solid #e3c46b; border-radius: 2px; padding: 8px 11px; color: #704a00; background: #fff9df; box-shadow: var(--shadow); font-size: 11px; text-align: center; }
56
+ .message.error { border-color: #e0aaa5; color: #8f231b; background: #fff2f0; }
57
+
58
+ .render-panel { position: absolute; z-index: 650; left: 12px; bottom: 12px; width: min(350px, calc(100% - 92px)); overflow: hidden; border: 1px solid var(--line-strong); border-radius: 2px; color: var(--ink); background: rgba(255, 255, 255, .97); box-shadow: var(--shadow); }
59
+ .render-panel[hidden] { display: none; }
60
+ .render-panel > header { display: flex; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid var(--line); padding: 12px 13px 10px; }
61
+ .render-panel header p, .metadata-panel header p { margin: 0 0 3px; color: var(--accent); font: 750 8px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
62
+ .render-panel h2, .metadata-panel h2 { margin: 0; font-size: 15px; line-height: 1.25; }
63
+ .panel-link { border: 1px solid var(--line-strong); border-radius: 2px; padding: 6px 8px; color: var(--muted); background: #fff; cursor: pointer; font: 750 8px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
64
+ .panel-link:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
65
+ .source-summary { display: grid; gap: 3px; border-bottom: 1px solid var(--line); padding: 9px 13px; background: #f8faf7; }
66
+ .source-summary strong { overflow: hidden; font: 700 10px/1.3 var(--mono); text-overflow: ellipsis; white-space: nowrap; }
67
+ .source-summary span { color: var(--muted); font: 9px/1.35 var(--mono); }
68
+ .field { display: grid; gap: 5px; }
69
+ .field > label, .opacity-field > label { color: var(--muted); font-size: 8px; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }
70
+ .render-panel > .field { padding: 10px 13px 0; }
71
+ .band-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; padding: 10px 13px 0; }
72
+ .field select { width: 100%; min-width: 0; height: 34px; border: 1px solid var(--line-strong); border-radius: 2px; padding: 0 7px; color: var(--ink); background: #fff; font: 700 9px/1 var(--mono); }
73
+ .field select:focus { border-color: var(--accent); outline: 3px solid rgba(15, 118, 110, .12); }
74
+ .opacity-field { display: grid; grid-template-columns: 1fr auto; gap: 6px; padding: 11px 13px 0; }
75
+ .opacity-field output { color: var(--muted); font: 9px/1 var(--mono); }
76
+ .opacity-field input { grid-column: 1 / -1; width: 100%; accent-color: var(--accent); }
77
+ .render-button { width: calc(100% - 26px); height: 34px; margin: 10px 13px 13px; border: 1px solid var(--accent); border-radius: 2px; color: #fff; background: var(--accent); cursor: pointer; font: 800 10px/1 var(--mono); letter-spacing: .04em; text-transform: uppercase; }
78
+ .render-button:hover { background: #0b5f59; }
79
+
80
+ .metadata-panel { position: absolute; z-index: 700; top: 12px; right: 12px; width: min(380px, calc(100% - 24px)); height: calc(100% - 24px); display: flex; flex-direction: column; border: 1px solid var(--line-strong); border-radius: 2px; color: var(--ink); background: rgba(255, 255, 255, .98); box-shadow: var(--shadow); transform: translateX(12px) scale(.99); transform-origin: right center; opacity: 0; transition: transform .2s ease, opacity .2s ease; }
81
+ .metadata-panel[hidden] { display: none; }
82
+ .metadata-panel.open { transform: translateX(0) scale(1); opacity: 1; }
83
+ .metadata-panel > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex: 0 0 auto; border-bottom: 1px solid var(--line); padding: 13px 14px 12px; }
84
+ .metadata-panel h2 { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
85
+ .icon-button { flex: 0 0 auto; width: 31px; height: 31px; border: 1px solid var(--line-strong); border-radius: 2px; padding: 0; color: var(--muted); background: #fff; cursor: pointer; font-size: 18px; }
86
+ .icon-button:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
87
+ .metadata-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 14px 16px; }
88
+ .metadata-group { margin-top: 14px; border: 1px solid var(--line); border-radius: 2px; overflow: hidden; }
89
+ .metadata-group > h3 { margin: 0; border-bottom: 1px solid var(--line); padding: 8px 10px; color: var(--muted); background: #f8faf7; font-size: 8px; letter-spacing: .12em; text-transform: uppercase; }
90
+ .metadata-group dl { margin: 0; }
91
+ .metadata-row { display: grid; grid-template-columns: minmax(92px, .65fr) minmax(0, 1.35fr); gap: 10px; border-bottom: 1px solid #e7eae4; padding: 8px 10px; }
92
+ .metadata-row:last-child { border-bottom: 0; }
93
+ .metadata-row dt { color: var(--muted); font: 9px/1.4 var(--mono); overflow-wrap: anywhere; }
94
+ .metadata-row dd { min-width: 0; margin: 0; color: var(--ink); font: 10px/1.4 var(--mono); overflow-wrap: anywhere; white-space: pre-wrap; text-align: right; }
95
+ .band-list { margin: 0; padding: 0; list-style: none; }
96
+ .band-list li { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 8px; border-bottom: 1px solid #e7eae4; padding: 8px 10px; }
97
+ .band-list li:last-child { border-bottom: 0; }
98
+ .band-list b { color: var(--accent); font: 800 9px/1.4 var(--mono); }
99
+ .band-list span { font: 9px/1.4 var(--mono); overflow-wrap: anywhere; }
100
+ .code-value { display: block; border-radius: 1px; padding: 8px; color: #f2f5ef; background: #20251f; font: 9px/1.5 var(--mono); text-align: left; word-break: break-word; }
101
+
102
+ .drop-overlay { position: absolute; z-index: 950; inset: 12px; display: grid; place-content: center; justify-items: center; gap: 8px; border: 2px dashed var(--accent); color: var(--ink); background: rgba(244, 246, 242, .9); backdrop-filter: blur(5px); }
103
+ .drop-overlay[hidden] { display: none; }
104
+ .drop-overlay img { width: 64px; height: 64px; }
105
+ .drop-overlay strong { font-size: 16px; }
106
+ .drop-overlay span { color: var(--muted); font: 10px/1.3 var(--mono); }
107
+
108
+ .loading { position: absolute; z-index: 900; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--ink); background: rgba(244, 246, 242, .58); backdrop-filter: blur(2px); font: 700 11px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
109
+ .loading[hidden] { display: none; }
110
+ .rumi-loader { position: relative; width: 76px; height: 76px; }
111
+ .rumi-loader > img { position: absolute; inset: 19px; width: 38px; height: 38px; }
112
+ .rumi-loader > div { position: absolute; inset: 4px; border: 1px solid rgba(15, 118, 110, .36); border-radius: 50%; animation: spin 4s linear infinite; }
113
+ .rumi-loader span { position: absolute; top: -8px; left: calc(50% - 8px); font-size: 15px; animation: counter-spin 4s linear infinite; }
114
+ @keyframes spin { to { transform: rotate(360deg); } }
115
+ @keyframes counter-spin { to { transform: rotate(-360deg); } }
116
+
117
+ .colophon { position: absolute; z-index: 600; left: 50%; bottom: 10px; transform: translateX(-50%); display: flex; align-items: center; justify-content: center; gap: 4px; border-radius: 2px; padding: 4px 9px; color: #70685e; background: rgba(255, 255, 255, .88); font-size: 10px; line-height: 1.2; white-space: nowrap; }
118
+ .heart { position: relative; top: -1px; color: var(--accent); font-size: 13px; line-height: 1; transition: color .16s ease; }
119
+ .colophon:hover .heart { color: #f05b7f; }
120
+ .by { display: inline-flex; align-items: center; text-decoration: none; transition: opacity .16s ease; }
121
+ .by img { width: 76px; height: auto; }
122
+ .by:hover, .by:focus-visible { opacity: .76; }
123
+
124
+ @media (max-width: 820px) {
125
+ .topbar { height: 106px; grid-template-columns: 1fr auto; grid-template-rows: 46px 48px; gap: 0 10px; padding: 6px 10px; }
126
+ .brand { grid-column: 1; grid-row: 1; }
127
+ .status { grid-column: 2; grid-row: 1; }
128
+ .source-picker { grid-column: 1 / -1; grid-row: 2; }
129
+ main { height: calc(100% - 106px); }
130
+ .metadata-panel { top: 8px; right: 8px; bottom: 8px; width: min(360px, calc(100% - 16px)); height: auto; }
131
+ .render-panel { left: 8px; bottom: 36px; width: min(330px, calc(100% - 16px)); }
132
+ }
133
+
134
+ @media (max-width: 560px) {
135
+ .file-button { padding: 0 8px; }
136
+ .status { display: none; }
137
+ .metadata-panel { left: 8px; width: auto; height: min(62vh, 560px); }
138
+ .render-panel { right: 8px; width: auto; }
139
+ .colophon { position: fixed; z-index: 1100; top: 17px; right: 10px; bottom: auto; left: auto; transform: none; padding: 0; background: transparent; }
140
+ .by img { width: 64px; }
141
+ }
142
+
143
+ @media (prefers-reduced-motion: reduce) {
144
+ *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
145
+ }
package/src/dtypes.js ADDED
@@ -0,0 +1,48 @@
1
+ const rows = [
2
+ [1, "uint8", 1, 8, 1, 1, Uint8Array],
3
+ [2, "int8", 2, 8, 1, 1, Int8Array],
4
+ [3, "uint16", 1, 16, 2, 2, Uint16Array],
5
+ [4, "int16", 2, 16, 2, 2, Int16Array],
6
+ [5, "uint32", 1, 32, 4, 4, Uint32Array],
7
+ [6, "int32", 2, 32, 4, 4, Int32Array],
8
+ [7, "uint64", 1, 64, 8, 8, BigUint64Array],
9
+ [8, "int64", 2, 64, 8, 8, BigInt64Array],
10
+ [9, "float16", 3, 16, 2, 2, Uint16Array],
11
+ [10, "float32", 3, 32, 4, 4, Float32Array],
12
+ [11, "float64", 3, 64, 8, 8, Float64Array],
13
+ [14, "complex32", 6, 32, 4, 2, Uint16Array],
14
+ [15, "complex64", 6, 64, 8, 4, Float32Array],
15
+ [16, "complex128", 6, 128, 16, 8, Float64Array],
16
+ [17, "float8_e4m3fn", 100, 8, 1, 1, Uint8Array],
17
+ [18, "float8_e5m2", 101, 8, 1, 1, Uint8Array],
18
+ [19, "bfloat16", 102, 16, 2, 2, Uint16Array],
19
+ [24, "bool", 1, 1, 1, 1, Uint8Array],
20
+ [25, "float8_e8m0fnu", 103, 8, 1, 1, Uint8Array],
21
+ [29, "float8_e4m3fnuz", 107, 8, 1, 1, Uint8Array],
22
+ [30, "float8_e5m2fnuz", 108, 8, 1, 1, Uint8Array],
23
+ ];
24
+
25
+ const table = new Map();
26
+ const littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
27
+
28
+ for (const [code, name, sampleFormat, bits, bytes, componentBytes, ArrayType] of rows) {
29
+ const dtype = Object.freeze({
30
+ code,
31
+ name,
32
+ sampleFormat,
33
+ bits,
34
+ bytes,
35
+ componentBytes,
36
+ ArrayType,
37
+ });
38
+ table.set(`${sampleFormat}:${bits}`, dtype);
39
+ }
40
+
41
+ export function dtypeFromSample(sampleFormat, bits) {
42
+ return table.get(`${sampleFormat}:${bits}`);
43
+ }
44
+
45
+ export function storageView(bytes, dtype) {
46
+ if (!littleEndian) throw new Error("Rumi requires a little-endian host");
47
+ return new dtype.ArrayType(bytes.buffer, bytes.byteOffset, bytes.byteLength / dtype.componentBytes);
48
+ }
package/src/epsg.js ADDED
@@ -0,0 +1,138 @@
1
+ // Generated from core/include/rumi/epsg_kinds.def. Keep both tables in sync.
2
+ const GEOGRAPHIC = new Uint16Array([
3
+ 3819, 3819, 3821, 3821, 3823, 3824, 3888, 3889, 3906, 3906, 4017, 4017, 4023, 4023, 4040, 4040,
4
+ 4046, 4046, 4074, 4075, 4080, 4081, 4120, 4124, 4127, 4139, 4141, 4171, 4173, 4176, 4178, 4184,
5
+ 4188, 4216, 4218, 4225, 4227, 4227, 4229, 4232, 4236, 4259, 4261, 4263, 4265, 4267, 4269, 4279,
6
+ 4281, 4286, 4288, 4289, 4292, 4293, 4295, 4295, 4297, 4304, 4306, 4316, 4318, 4319, 4322, 4322,
7
+ 4324, 4324, 4326, 4326, 4463, 4463, 4466, 4466, 4469, 4470, 4472, 4472, 4475, 4475, 4480, 4480,
8
+ 4482, 4483, 4490, 4490, 4555, 4555, 4557, 4558, 4600, 4630, 4632, 4633, 4636, 4639, 4641, 4644,
9
+ 4646, 4646, 4657, 4680, 4682, 4684, 4686, 4730, 4732, 4765, 4801, 4807, 4809, 4811, 4813, 4818,
10
+ 4820, 4821, 4823, 4824, 4883, 4883, 4885, 4885, 4887, 4887, 4889, 4889, 4891, 4891, 4893, 4893,
11
+ 4895, 4895, 4898, 4898, 4900, 4901, 4903, 4904, 4907, 4907, 4909, 4909, 4921, 4921, 4923, 4923,
12
+ 4925, 4925, 4927, 4927, 4929, 4929, 4931, 4931, 4933, 4933, 4935, 4935, 4937, 4937, 4939, 4939,
13
+ 4941, 4941, 4943, 4943, 4945, 4945, 4947, 4947, 4949, 4949, 4951, 4951, 4953, 4953, 4955, 4955,
14
+ 4957, 4957, 4959, 4959, 4961, 4961, 4963, 4963, 4965, 4965, 4967, 4967, 4971, 4971, 4975, 4975,
15
+ 4977, 4977, 4979, 4979, 4981, 4981, 4983, 4983, 4985, 4985, 4987, 4987, 4989, 4989, 4991, 4991,
16
+ 4993, 4993, 4995, 4995, 4997, 4997, 4999, 4999, 5012, 5013, 5132, 5132, 5228, 5229, 5233, 5233,
17
+ 5245, 5246, 5251, 5252, 5263, 5264, 5323, 5324, 5340, 5340, 5342, 5342, 5353, 5354, 5359, 5360,
18
+ 5364, 5365, 5370, 5373, 5380, 5381, 5392, 5393, 5451, 5451, 5464, 5464, 5467, 5467, 5488, 5489,
19
+ 5524, 5524, 5527, 5527, 5545, 5546, 5560, 5561, 5592, 5593, 5681, 5681, 5830, 5830, 5885, 5886,
20
+ 6134, 6135, 6207, 6207, 6310, 6311, 6318, 6319, 6321, 6322, 6324, 6325, 6364, 6365, 6667, 6668,
21
+ 6705, 6706, 6782, 6783, 6881, 6883, 6892, 6892, 6894, 6894, 6982, 6983, 6989, 6990, 7034, 7042,
22
+ 7072, 7073, 7084, 7087, 7133, 7133, 7135, 7136, 7138, 7139, 7372, 7373, 7657, 7657, 7659, 7659,
23
+ 7661, 7661, 7663, 7663, 7665, 7665, 7678, 7678, 7680, 7680, 7682, 7683, 7685, 7686, 7797, 7798,
24
+ 7816, 7816, 7843, 7844, 7880, 7881, 7885, 7886, 7900, 7912, 7915, 7915, 7917, 7917, 7919, 7919,
25
+ 7921, 7921, 7923, 7923, 7925, 7925, 7927, 7927, 7929, 7929, 7931, 7931, 8042, 8043, 8085, 8086,
26
+ 8231, 8232, 8235, 8235, 8237, 8237, 8239, 8240, 8244, 8244, 8246, 8246, 8248, 8249, 8251, 8252,
27
+ 8254, 8255, 8351, 8351, 8399, 8399, 8403, 8403, 8426, 8428, 8430, 8431, 8542, 8542, 8544, 8545,
28
+ 8684, 8685, 8694, 8694, 8698, 8699, 8817, 8818, 8860, 8860, 8888, 8888, 8899, 8902, 8906, 8907,
29
+ 8916, 8916, 8918, 8918, 8920, 8920, 8922, 8922, 8924, 8924, 8926, 8926, 8928, 8928, 8930, 8930,
30
+ 8932, 8932, 8934, 8934, 8936, 8936, 8938, 8938, 8940, 8940, 8942, 8942, 8944, 8944, 8946, 8946,
31
+ 8948, 8949, 8972, 9000, 9002, 9003, 9005, 9006, 9008, 9009, 9011, 9014, 9016, 9019, 9053, 9057,
32
+ 9059, 9069, 9071, 9072, 9074, 9075, 9139, 9140, 9147, 9148, 9152, 9153, 9248, 9248, 9251, 9251,
33
+ 9253, 9253, 9267, 9267, 9293, 9294, 9299, 9299, 9308, 9309, 9332, 9333, 9364, 9364, 9372, 9372,
34
+ 9379, 9380, 9384, 9384, 9403, 9403, 9453, 9453, 9469, 9470, 9474, 9475, 9546, 9547, 9695, 9696,
35
+ 9701, 9702, 9739, 9739, 9754, 9755, 9758, 9758, 9763, 9763, 9776, 9779, 9781, 9784, 9866, 9866,
36
+ 9871, 9871, 9893, 9893, 9939, 9939, 9964, 9964, 9969, 9969, 9974, 9974, 9989, 9990, 10158, 10158,
37
+ 10175, 10175, 10177, 10178, 10185, 10185, 10191, 10191, 10196, 10196, 10204, 10204, 10209, 10209, 10214, 10214,
38
+ 10219, 10219, 10224, 10224, 10229, 10229, 10237, 10237, 10249, 10249, 10252, 10252, 10256, 10256, 10260, 10260,
39
+ 10265, 10265, 10268, 10268, 10272, 10272, 10277, 10277, 10283, 10284, 10298, 10300, 10304, 10305, 10307, 10307,
40
+ 10309, 10312, 10327, 10328, 10345, 10346, 10413, 10414, 10468, 10468, 10474, 10475, 10570, 10571, 10605, 10606,
41
+ 10623, 10623, 10628, 10628, 10670, 10673, 20033, 20033, 20040, 20041, 20045, 20046,
42
+ ]);
43
+
44
+ const PROJECTED = new Uint16Array([
45
+ 2000, 2007, 2009, 2035, 2039, 2062, 2065, 2084, 2087, 2090, 2093, 2138, 2154, 2154, 2157, 2162,
46
+ 2164, 2165, 2169, 2169, 2172, 2180, 2188, 2190, 2193, 2193, 2195, 2198, 2200, 2213, 2215, 2243,
47
+ 2246, 2290, 2294, 2296, 2299, 2299, 2301, 2301, 2303, 2393, 2393, 2399, 2401, 2491, 2494, 2549,
48
+ 2551, 2576, 2578, 2599, 2601, 2693, 2695, 2888, 2891, 2933, 2935, 2943, 2945, 2973, 2975, 2978,
49
+ 2980, 2981, 2985, 2988, 2991, 3021, 3021, 3027, 3027, 3037, 3040, 3049, 3052, 3072, 3074, 3075,
50
+ 3077, 3102, 3106, 3106, 3106, 3142, 3144, 3145, 3148, 3149, 3152, 3313, 3316, 3348, 3350, 3355,
51
+ 3358, 3358, 3360, 3365, 3367, 3453, 3455, 3751, 3753, 3773, 3775, 3777, 3779, 3781, 3783, 3784,
52
+ 3788, 3791, 3793, 3802, 3812, 3812, 3814, 3816, 3825, 3829, 3832, 3841, 3844, 3852, 3854, 3854,
53
+ 3857, 3857, 3873, 3885, 3890, 3893, 3912, 3912, 3920, 3920, 3942, 3950, 3968, 3970, 3976, 3976,
54
+ 3978, 3979, 3986, 3989, 3991, 3997, 4026, 4026, 4037, 4038, 4048, 4051, 4056, 4063, 4071, 4071,
55
+ 4082, 4083, 4087, 4087, 4093, 4096, 4217, 4217, 4390, 4415, 4417, 4434, 4437, 4439, 4455, 4457,
56
+ 4462, 4462, 4467, 4467, 4471, 4471, 4484, 4489, 4491, 4554, 4559, 4559, 4568, 4589, 4647, 4647,
57
+ 4652, 4656, 4766, 4800, 4812, 4812, 4822, 4822, 4826, 4826, 4839, 4839, 5014, 5018, 5041, 5042,
58
+ 5048, 5048, 5069, 5072, 5105, 5130, 5167, 5188, 5221, 5221, 5223, 5225, 5234, 5235, 5243, 5243,
59
+ 5247, 5247, 5253, 5259, 5266, 5266, 5269, 5275, 5292, 5311, 5316, 5316, 5320, 5321, 5325, 5325,
60
+ 5329, 5331, 5337, 5337, 5343, 5349, 5355, 5357, 5361, 5362, 5367, 5367, 5382, 5383, 5387, 5387,
61
+ 5389, 5389, 5396, 5396, 5456, 5457, 5459, 5463, 5469, 5469, 5472, 5472, 5479, 5482, 5490, 5490,
62
+ 5513, 5516, 5518, 5520, 5523, 5523, 5530, 5531, 5533, 5539, 5550, 5552, 5559, 5559, 5562, 5569,
63
+ 5588, 5589, 5596, 5596, 5623, 5625, 5627, 5627, 5629, 5629, 5631, 5639, 5641, 5641, 5643, 5644,
64
+ 5646, 5646, 5649, 5655, 5659, 5659, 5663, 5680, 5682, 5685, 5700, 5700, 5825, 5825, 5836, 5837,
65
+ 5839, 5839, 5842, 5842, 5844, 5844, 5858, 5858, 5875, 5877, 5879, 5880, 5887, 5887, 5896, 5897,
66
+ 5897, 5898, 5898, 5899, 5921, 5940, 6050, 6125, 6128, 6129, 6201, 6202, 6204, 6204, 6210, 6211,
67
+ 6244, 6275, 6307, 6307, 6312, 6312, 6316, 6316, 6316, 6316, 6328, 6348, 6350, 6356, 6362, 6362,
68
+ 6366, 6372, 6381, 6387, 6391, 6391, 6393, 6516, 6518, 6603, 6605, 6637, 6646, 6646, 6669, 6692,
69
+ 6703, 6703, 6707, 6709, 6720, 6723, 6736, 6738, 6784, 6863, 6867, 6868, 6870, 6870, 6875, 6876,
70
+ 6879, 6880, 6884, 6887, 6915, 6915, 6922, 6925, 6931, 6933, 6962, 6962, 6966, 6966, 6984, 6984,
71
+ 6991, 6991, 7005, 7007, 7057, 7070, 7074, 7081, 7109, 7128, 7131, 7132, 7142, 7142, 7257, 7370,
72
+ 7374, 7376, 7528, 7645, 7692, 7696, 7755, 7787, 7791, 7795, 7799, 7801, 7803, 7803, 7805, 7805,
73
+ 7825, 7831, 7845, 7859, 7877, 7878, 7882, 7883, 7887, 7887, 7899, 7899, 7991, 7992, 8013, 8032,
74
+ 8035, 8036, 8044, 8045, 8058, 8059, 8065, 8068, 8082, 8083, 8088, 8088, 8090, 8093, 8095, 8173,
75
+ 8177, 8177, 8179, 8182, 8184, 8185, 8187, 8187, 8189, 8189, 8191, 8191, 8193, 8193, 8196, 8198,
76
+ 8200, 8210, 8212, 8214, 8216, 8216, 8218, 8218, 8220, 8220, 8222, 8222, 8224, 8226, 8311, 8348,
77
+ 8352, 8353, 8379, 8385, 8387, 8387, 8391, 8391, 8395, 8395, 8433, 8433, 8441, 8441, 8455, 8456,
78
+ 8518, 8529, 8531, 8531, 8533, 8536, 8538, 8540, 8677, 8679, 8682, 8682, 8686, 8687, 8692, 8693,
79
+ 8826, 8826, 8836, 8840, 8857, 8859, 8903, 8903, 8908, 8910, 8950, 8951, 9039, 9040, 9141, 9141,
80
+ 9149, 9150, 9154, 9159, 9191, 9191, 9205, 9218, 9221, 9222, 9249, 9250, 9252, 9252, 9254, 9254,
81
+ 9265, 9265, 9271, 9273, 9284, 9285, 9295, 9297, 9300, 9300, 9311, 9311, 9354, 9354, 9356, 9360,
82
+ 9367, 9367, 9373, 9373, 9377, 9377, 9387, 9387, 9391, 9391, 9404, 9407, 9456, 9456, 9473, 9473,
83
+ 9476, 9482, 9487, 9494, 9498, 9498, 9549, 9549, 9674, 9674, 9678, 9678, 9680, 9680, 9697, 9699,
84
+ 9709, 9709, 9712, 9713, 9716, 9716, 9741, 9741, 9748, 9749, 9761, 9761, 9766, 9766, 9793, 9794,
85
+ 9821, 9865, 9869, 9869, 9874, 9875, 9880, 9880, 9895, 9895, 9943, 9943, 9945, 9945, 9947, 9947,
86
+ 9967, 9967, 9972, 9972, 9977, 9977, 10160, 10160, 10183, 10183, 10188, 10188, 10194, 10194, 10199, 10199,
87
+ 10207, 10207, 10212, 10212, 10217, 10217, 10222, 10222, 10227, 10227, 10235, 10235, 10240, 10240, 10250, 10250,
88
+ 10254, 10254, 10258, 10258, 10262, 10262, 10266, 10266, 10270, 10270, 10275, 10275, 10280, 10280, 10285, 10291,
89
+ 10306, 10306, 10314, 10317, 10329, 10329, 10448, 10465, 10471, 10471, 10477, 10477, 10481, 10481, 10516, 10516,
90
+ 10592, 10592, 10594, 10594, 10596, 10596, 10598, 10598, 10601, 10601, 10603, 10603, 10622, 10622, 10626, 10626,
91
+ 10632, 10632, 10665, 10665, 10674, 10674, 10731, 10733, 11114, 11118, 20002, 20002, 20004, 20032, 20042, 20042,
92
+ 20047, 20050, 20135, 20138, 20249, 20258, 20349, 20356, 20436, 20440, 20499, 20499, 20538, 20539, 20790, 20791,
93
+ 20822, 20824, 20904, 20932, 20934, 20936, 21004, 21032, 21035, 21037, 21095, 21097, 21148, 21150, 21207, 21264,
94
+ 21291, 21292, 21307, 21364, 21413, 21423, 21453, 21463, 21500, 21500, 21780, 21782, 21818, 21818, 21896, 21899,
95
+ 22032, 22033, 22091, 22092, 22171, 22177, 22181, 22187, 22191, 22197, 22207, 22213, 22213, 22222, 22229, 22232,
96
+ 22234, 22235, 22239, 22240, 22243, 22250, 22262, 22265, 22275, 22275, 22277, 22277, 22279, 22279, 22281, 22281,
97
+ 22283, 22283, 22285, 22285, 22287, 22287, 22289, 22289, 22291, 22291, 22293, 22293, 22300, 22300, 22307, 22322,
98
+ 22332, 22332, 22337, 22338, 22348, 22357, 22391, 22392, 22407, 22422, 22462, 22465, 22521, 22525, 22607, 22622,
99
+ 22639, 22639, 22641, 22646, 22648, 22657, 22700, 22700, 22707, 22722, 22739, 22739, 22762, 22765, 22770, 22770,
100
+ 22780, 22780, 22807, 22822, 22991, 22994, 23028, 23038, 23090, 23090, 23095, 23095, 23239, 23240, 23301, 23333,
101
+ 23700, 23700, 23830, 23852, 23866, 23872, 23877, 23884, 23887, 23894, 23946, 23948, 24047, 24048, 24100, 24100,
102
+ 24200, 24200, 24305, 24306, 24311, 24313, 24342, 24347, 24370, 24383, 24500, 24500, 24547, 24548, 24600, 24600,
103
+ 24718, 24720, 24817, 24821, 24877, 24882, 24891, 24893, 25000, 25000, 25231, 25231, 25391, 25395, 25828, 25829,
104
+ 25829, 25830, 25830, 25831, 25831, 25832, 25832, 25833, 25833, 25834, 25834, 25835, 25835, 25836, 25836, 25837,
105
+ 25884, 25884, 25932, 25932, 26191, 26192, 26194, 26195, 26237, 26237, 26331, 26332, 26391, 26393, 26632, 26632,
106
+ 26692, 26692, 26701, 26722, 26729, 26746, 26748, 26760, 26766, 26787, 26791, 26799, 26847, 26870, 26891, 26899,
107
+ 26901, 26923, 26929, 26946, 26948, 26978, 26980, 26998, 27039, 27040, 27120, 27120, 27200, 27200, 27205, 27232,
108
+ 27258, 27260, 27291, 27292, 27391, 27398, 27429, 27429, 27493, 27493, 27500, 27500, 27561, 27564, 27571, 27574,
109
+ 27700, 27707, 28191, 28193, 28232, 28232, 28348, 28358, 28404, 28432, 28600, 28600, 28991, 28992, 29101, 29101,
110
+ 29168, 29172, 29187, 29195, 29220, 29221, 29333, 29333, 29371, 29371, 29373, 29373, 29375, 29375, 29377, 29377,
111
+ 29379, 29379, 29381, 29381, 29383, 29383, 29385, 29385, 29701, 29702, 29738, 29739, 29849, 29850, 29871, 29874,
112
+ 29901, 29903, 30161, 30179, 30200, 30200, 30339, 30340, 30491, 30494, 30729, 30732, 30791, 30792, 31028, 31028,
113
+ 31121, 31121, 31154, 31154, 31170, 31171, 31251, 31259, 31281, 31290, 31300, 31300, 31370, 31370, 31466, 31469,
114
+ 31528, 31529, 31600, 31600, 31838, 31839, 31901, 31901, 31965, 32003, 32005, 32017, 32019, 32028, 32030, 32031,
115
+ 32033, 32035, 32037, 32058, 32064, 32067, 32081, 32086, 32098, 32100, 32104, 32104, 32107, 32130, 32133, 32159,
116
+ 32161, 32161, 32164, 32167, 32181, 32199, 32201, 32260, 32301, 32360, 32401, 32460, 32501, 32560, 32600, 32661,
117
+ 32664, 32667, 32700, 32761, 32766, 32766,
118
+ ]);
119
+
120
+ function contains(ranges, code) {
121
+ let low = 0;
122
+ let high = ranges.length / 2;
123
+ while (low < high) {
124
+ const middle = low + Math.floor((high - low) / 2);
125
+ const start = ranges[middle * 2];
126
+ const end = ranges[middle * 2 + 1];
127
+ if (code < start) high = middle;
128
+ else if (code > end) low = middle + 1;
129
+ else return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ export function epsgKind(code) {
135
+ if (contains(GEOGRAPHIC, code)) return "geographic";
136
+ if (contains(PROJECTED, code)) return "projected";
137
+ return undefined;
138
+ }
@@ -0,0 +1,284 @@
1
+ import { derivedBaseOffset, makeExternalHeader } from "./header.js";
2
+ import { epsgKind } from "./epsg.js";
3
+ import { readExact } from "./source.js";
4
+ import { readTrailer } from "./trailer.js";
5
+
6
+ const FILE_HEADER_SIZE = 16;
7
+ const IFD_OFFSET = 16;
8
+ const IFD_ENTRIES = 13;
9
+ const IFD_ENTRY_SIZE = 20;
10
+ const IFD_SIZE = 8 + IFD_ENTRIES * IFD_ENTRY_SIZE + 8;
11
+ const MAX_U32 = 0xffffffffn;
12
+ const MAX_U64 = (1n << 64n) - 1n;
13
+
14
+ const SHORT = 3;
15
+ const LONG = 4;
16
+ const DOUBLE = 12;
17
+ const LONG8 = 16;
18
+
19
+ const required = [
20
+ [256, LONG, true],
21
+ [257, LONG, true],
22
+ [258, SHORT, false],
23
+ [277, SHORT, true],
24
+ [322, SHORT, true],
25
+ [323, SHORT, true],
26
+ [324, LONG8, false],
27
+ [325, LONG, false],
28
+ [339, SHORT, false],
29
+ [34264, DOUBLE, false],
30
+ [34735, SHORT, false],
31
+ [65000, SHORT, true],
32
+ [65001, LONG, true],
33
+ ];
34
+
35
+ const typeBytes = new Map([[SHORT, 2], [LONG, 4], [DOUBLE, 8], [LONG8, 8]]);
36
+
37
+ function numberOf(value, label) {
38
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError(`${label} is too large`);
39
+ return Number(value);
40
+ }
41
+
42
+ function entryValue(entry, width, index = 0) {
43
+ const offset = index * width;
44
+ if (width === 2) return BigInt(entry.data.getUint16(offset, true));
45
+ if (width === 4) return BigInt(entry.data.getUint32(offset, true));
46
+ return entry.data.getBigUint64(offset, true);
47
+ }
48
+
49
+ function entryBytes(metadata, entry, maxMetadataBytes) {
50
+ const width = typeBytes.get(entry.type);
51
+ const total = entry.count * BigInt(width);
52
+ if (total > BigInt(maxMetadataBytes)) throw new RangeError(`tag ${entry.tag} exceeds maxMetadataBytes`);
53
+ const size = numberOf(total, `tag ${entry.tag}`);
54
+ if (size <= 8) return entry.raw.slice(0, size);
55
+ const offset = new DataView(entry.raw.buffer, entry.raw.byteOffset, 8).getBigUint64(0, true);
56
+ const start = numberOf(offset, `tag ${entry.tag} offset`);
57
+ if (start > metadata.byteLength || size > metadata.byteLength - start) {
58
+ throw new RangeError(`tag ${entry.tag} leaves the metadata region`);
59
+ }
60
+ return metadata.subarray(start, start + size);
61
+ }
62
+
63
+ function integerValues(metadata, entry, expected, maxMetadataBytes) {
64
+ if (entry.count !== BigInt(expected)) {
65
+ throw new Error(`tag ${entry.tag} has ${entry.count} values, expected ${expected}`);
66
+ }
67
+ const width = typeBytes.get(entry.type);
68
+ const bytes = entryBytes(metadata, entry, maxMetadataBytes);
69
+ const data = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
70
+ const readable = { ...entry, data };
71
+ const out = new Array(expected);
72
+ for (let index = 0; index < expected; index += 1) {
73
+ out[index] = entryValue(readable, width, index);
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function doubleValues(metadata, entry, expected, maxMetadataBytes) {
79
+ if (entry.count !== BigInt(expected) || entry.type !== DOUBLE) {
80
+ throw new Error(`tag ${entry.tag} must contain ${expected} doubles`);
81
+ }
82
+ const bytes = entryBytes(metadata, entry, maxMetadataBytes);
83
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
84
+ return Array.from({ length: expected }, (_, index) => view.getFloat64(index * 8, true));
85
+ }
86
+
87
+ function scalar(metadata, entries, tag, maxMetadataBytes) {
88
+ const entry = entries.get(tag);
89
+ return integerValues(metadata, entry, 1, maxMetadataBytes)[0];
90
+ }
91
+
92
+ export async function externalHeaderFromFile(source, {
93
+ maxMetadataBytes,
94
+ maxTimeCoordinates,
95
+ }) {
96
+ const fixed = await readExact(source, 0n, IFD_OFFSET + IFD_SIZE);
97
+ const view = new DataView(fixed.buffer, fixed.byteOffset, fixed.byteLength);
98
+ if (fixed[0] !== 0x52 || fixed[1] !== 0x55 || fixed[2] !== 0x4d || fixed[3] !== 0x49) {
99
+ throw new Error("source is not a Rumi file");
100
+ }
101
+ if (view.getUint16(4, true) !== 1) throw new Error("unsupported Rumi file version");
102
+ if (view.getUint16(6, true) !== 0) throw new Error("Rumi file reserved field is not zero");
103
+ if (view.getBigUint64(8, true) !== BigInt(IFD_OFFSET)) throw new Error("Rumi IFD is not at byte 16");
104
+ if (view.getBigUint64(IFD_OFFSET, true) !== BigInt(IFD_ENTRIES)) {
105
+ throw new Error("Rumi file must contain exactly 13 IFD entries");
106
+ }
107
+ if (view.getBigUint64(IFD_OFFSET + 8 + IFD_ENTRIES * IFD_ENTRY_SIZE, true) !== 0n) {
108
+ throw new Error("Rumi file contains more than one IFD");
109
+ }
110
+
111
+ const entries = new Map();
112
+ let externalCursor = BigInt(IFD_OFFSET + IFD_SIZE);
113
+ for (let index = 0; index < required.length; index += 1) {
114
+ const at = IFD_OFFSET + 8 + index * IFD_ENTRY_SIZE;
115
+ const [expectedTag, expectedType, single] = required[index];
116
+ const tag = view.getUint16(at, true);
117
+ const type = view.getUint16(at + 2, true);
118
+ const count = view.getBigUint64(at + 4, true);
119
+ if (tag !== expectedTag || type !== expectedType) {
120
+ throw new Error(`IFD entry ${index} does not match the Rumi profile`);
121
+ }
122
+ if (single && count !== 1n) throw new Error(`tag ${tag} must contain one value`);
123
+ const width = typeBytes.get(type);
124
+ if (count > MAX_U64 / BigInt(width)) throw new RangeError(`tag ${tag} size overflows uint64`);
125
+ const total = count * BigInt(width);
126
+ const raw = fixed.slice(at + 12, at + 20);
127
+ if (total <= 8n) {
128
+ for (let byte = Number(total); byte < 8; byte += 1) {
129
+ if (raw[byte] !== 0) throw new Error(`tag ${tag} has non-zero inline padding`);
130
+ }
131
+ } else {
132
+ const offset = new DataView(raw.buffer, raw.byteOffset, 8).getBigUint64(0, true);
133
+ if (offset !== externalCursor) throw new Error(`tag ${tag} external value is not contiguous`);
134
+ externalCursor += total + (total & 1n);
135
+ if (externalCursor > BigInt(maxMetadataBytes)) {
136
+ throw new RangeError("Rumi file metadata exceeds maxMetadataBytes");
137
+ }
138
+ }
139
+ entries.set(tag, { tag, type, count, raw });
140
+ }
141
+
142
+ const externalSize = numberOf(externalCursor - BigInt(fixed.byteLength), "Rumi metadata");
143
+ const metadata = new Uint8Array(fixed.byteLength + externalSize);
144
+ metadata.set(fixed);
145
+ if (externalSize !== 0) {
146
+ metadata.set(await readExact(source, BigInt(fixed.byteLength), externalSize), fixed.byteLength);
147
+ }
148
+
149
+ if (entries.get(34264).count !== 16n || entries.get(34735).count !== 16n) {
150
+ throw new Error("Rumi transform and GeoKey arrays must contain 16 values");
151
+ }
152
+
153
+ const fields = {
154
+ width: numberOf(scalar(metadata, entries, 256, maxMetadataBytes), "width"),
155
+ height: numberOf(scalar(metadata, entries, 257, maxMetadataBytes), "height"),
156
+ bands: numberOf(scalar(metadata, entries, 277, maxMetadataBytes), "bands"),
157
+ tileWidth: numberOf(scalar(metadata, entries, 322, maxMetadataBytes), "tileWidth"),
158
+ tileHeight: numberOf(scalar(metadata, entries, 323, maxMetadataBytes), "tileHeight"),
159
+ frameUnit: numberOf(scalar(metadata, entries, 65000, maxMetadataBytes), "frameUnit"),
160
+ timeCount: numberOf(scalar(metadata, entries, 65001, maxMetadataBytes), "timeCount"),
161
+ };
162
+ if (fields.width === 0 || fields.height === 0 || fields.bands === 0
163
+ || fields.tileWidth === 0 || fields.tileHeight === 0 || fields.timeCount === 0) {
164
+ throw new RangeError("invalid Rumi dimensions");
165
+ }
166
+
167
+ const bits = integerValues(metadata, entries.get(258), fields.bands, maxMetadataBytes);
168
+ const formats = integerValues(metadata, entries.get(339), fields.bands, maxMetadataBytes);
169
+ if (bits.some((value) => value !== bits[0])) throw new Error("mixed BitsPerSample is not supported");
170
+ if (formats.some((value) => value !== formats[0])) throw new Error("mixed SampleFormat is not supported");
171
+ if (bits[0] > 255n || formats[0] > 255n) {
172
+ throw new Error(`unsupported sample format ${formats[0]}/${bits[0]}`);
173
+ }
174
+ fields.bits = numberOf(bits[0], "BitsPerSample");
175
+ fields.sampleFormat = numberOf(formats[0], "SampleFormat");
176
+
177
+ const matrix = doubleValues(metadata, entries.get(34264), 16, maxMetadataBytes);
178
+ if (matrix.some((value) => !Number.isFinite(value))) {
179
+ throw new Error("Rumi transformation contains a non-finite value");
180
+ }
181
+ for (const index of [2, 6, 8, 9, 10, 11, 12, 13, 14]) {
182
+ if (matrix[index] !== 0) throw new Error(`Rumi transformation element ${index} must be zero`);
183
+ }
184
+ if (matrix[15] !== 1) throw new Error("Rumi transformation element 15 must be one");
185
+
186
+ const geokeys = integerValues(metadata, entries.get(34735), 16, maxMetadataBytes);
187
+ const expectedHeader = [1n, 1n, 0n, 3n];
188
+ if (expectedHeader.some((value, index) => geokeys[index] !== value)) {
189
+ throw new Error("invalid Rumi GeoKey directory header");
190
+ }
191
+ for (let key = 0; key < 3; key += 1) {
192
+ const at = 4 + key * 4;
193
+ if (geokeys[at + 1] !== 0n || geokeys[at + 2] !== 1n) {
194
+ throw new Error("Rumi GeoKeys must store one inline value");
195
+ }
196
+ }
197
+ const model = numberOf(geokeys[7], "GeoKey model");
198
+ const raster = numberOf(geokeys[11], "GeoKey raster type");
199
+ const crsKey = numberOf(geokeys[12], "GeoKey CRS key");
200
+ const epsgValue = numberOf(geokeys[15], "GeoKey EPSG code");
201
+ if (geokeys[4] !== 1024n || geokeys[8] !== 1025n || ![1, 2].includes(raster)) {
202
+ throw new Error("invalid Rumi GeoKey directory");
203
+ }
204
+ let epsg = null;
205
+ if (model === 0) {
206
+ if (crsKey !== 2048 || epsgValue !== 0) throw new Error("invalid undefined Rumi CRS");
207
+ const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
208
+ if (matrix.some((value, index) => value !== identity[index])) {
209
+ throw new Error("undefined Rumi georeferencing must use the identity transform");
210
+ }
211
+ } else if ((model === 1 && crsKey === 3072) || (model === 2 && crsKey === 2048)) {
212
+ if (epsgValue < 1024 || epsgValue > 32766) throw new Error("Rumi EPSG code is out of range");
213
+ const kind = epsgKind(epsgValue);
214
+ if (kind === undefined) throw new Error(`EPSG:${epsgValue} is not a supported geographic or projected CRS`);
215
+ if ((model === 1) !== (kind === "projected")) {
216
+ throw new Error(`Rumi GeoKey model disagrees with EPSG:${epsgValue}`);
217
+ }
218
+ epsg = epsgValue;
219
+ } else {
220
+ throw new Error("Rumi GeoKey model and CRS key disagree");
221
+ }
222
+
223
+ const offsetEntry = entries.get(324);
224
+ const countEntry = entries.get(325);
225
+ if (offsetEntry.count === 0n || offsetEntry.count > MAX_U32 || countEntry.count !== offsetEntry.count) {
226
+ throw new Error("invalid Rumi frame table lengths");
227
+ }
228
+ const frameCount = Number(offsetEntry.count);
229
+ const present = Number(fields.bands > 1) + Number(fields.timeCount > 1);
230
+ const validUnit = fields.frameUnit === 0
231
+ || ((fields.frameUnit === 1 || fields.frameUnit === 2) && present === 1)
232
+ || (fields.frameUnit >= 3 && fields.frameUnit <= 9 && present === 2);
233
+ if (!validUnit) throw new Error("frameUnit does not fit the band and time dimensions");
234
+ let expectedFrames = BigInt(Math.ceil(fields.width / fields.tileWidth))
235
+ * BigInt(Math.ceil(fields.height / fields.tileHeight));
236
+ if (fields.frameUnit === 0 || fields.frameUnit === 9) {
237
+ expectedFrames *= BigInt(fields.bands) * BigInt(fields.timeCount);
238
+ }
239
+ if (offsetEntry.count !== expectedFrames) {
240
+ throw new Error(`frame table has ${offsetEntry.count} entries, expected ${expectedFrames}`);
241
+ }
242
+ if (frameCount * 12 > maxMetadataBytes) throw new RangeError("frame index exceeds maxMetadataBytes");
243
+ const offsetBytes = entryBytes(metadata, offsetEntry, maxMetadataBytes);
244
+ const countBytes = entryBytes(metadata, countEntry, maxMetadataBytes);
245
+ const offsetView = new DataView(offsetBytes.buffer, offsetBytes.byteOffset, offsetBytes.byteLength);
246
+ const countView = new DataView(countBytes.buffer, countBytes.byteOffset, countBytes.byteLength);
247
+ const counts = new Uint32Array(frameCount);
248
+ const firstOffset = offsetView.getBigUint64(0, true);
249
+ let running = firstOffset;
250
+ for (let index = 0; index < frameCount; index += 1) {
251
+ const offset = offsetView.getBigUint64(index * 8, true);
252
+ const count = BigInt(countView.getUint32(index * 4, true));
253
+ if (count === 0n || count > MAX_U32) throw new RangeError(`frame ${index} has an invalid byte count`);
254
+ if (offset !== running) throw new Error(`frame ${index} is not contiguous`);
255
+ if (running > MAX_U64 - count) throw new RangeError("frame offsets overflow uint64");
256
+ counts[index] = Number(count);
257
+ running += count;
258
+ }
259
+ if (firstOffset !== derivedBaseOffset(fields.bands, BigInt(frameCount))) {
260
+ throw new Error("frame data does not start at the canonical offset");
261
+ }
262
+ const trailer = await readTrailer(
263
+ source,
264
+ running,
265
+ fields.bands,
266
+ fields.timeCount,
267
+ maxMetadataBytes,
268
+ maxTimeCoordinates,
269
+ );
270
+ return {
271
+ header: makeExternalHeader(fields, counts),
272
+ metadata: Object.freeze({
273
+ transform: epsg === null
274
+ ? null
275
+ : Object.freeze([matrix[0], matrix[1], matrix[3], matrix[4], matrix[5], matrix[7]]),
276
+ epsg,
277
+ crs: epsg === null ? null : `EPSG:${epsg}`,
278
+ pixelIsPoint: raster === 2,
279
+ bands: trailer.bands,
280
+ time: trailer.time,
281
+ timeKind: trailer.timeKind,
282
+ }),
283
+ };
284
+ }