omni-viewer-core 0.23.0 → 0.24.0

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.
Files changed (81) hide show
  1. package/README.md +45 -9
  2. package/dist/assets/audio-engine/spectrogram_kernel.mjs +256 -0
  3. package/dist/assets/audio-engine/spectrogram_worker.mjs +24 -0
  4. package/dist/i18n/catalog.en.js +65 -0
  5. package/dist/i18n/catalog.ja.js +65 -0
  6. package/dist/i18n/catalog.ko.js +65 -0
  7. package/dist/i18n/catalog.zh-cn.js +65 -0
  8. package/dist/parsers/cfb.d.ts +21 -0
  9. package/dist/parsers/cfb.js +122 -0
  10. package/dist/parsers/doc-binary/index.js +2 -76
  11. package/dist/parsers/drawio/index.d.ts +46 -0
  12. package/dist/parsers/drawio/index.js +193 -0
  13. package/dist/parsers/epub/index.d.ts +2 -0
  14. package/dist/parsers/epub/index.js +16 -12
  15. package/dist/parsers/epub/zip.d.ts +2 -1
  16. package/dist/parsers/epub/zip.js +88 -44
  17. package/dist/parsers/file-preview.d.ts +45 -0
  18. package/dist/parsers/file-preview.js +179 -0
  19. package/dist/parsers/gps/index.d.ts +18 -0
  20. package/dist/parsers/gps/index.js +264 -0
  21. package/dist/parsers/har/index.d.ts +1 -7
  22. package/dist/parsers/har/index.js +2 -42
  23. package/dist/parsers/iso8601.d.ts +7 -0
  24. package/dist/parsers/iso8601.js +42 -0
  25. package/dist/parsers/junit/sniff.js +3 -61
  26. package/dist/parsers/msg/index.d.ts +17 -0
  27. package/dist/parsers/msg/index.js +222 -0
  28. package/dist/parsers/opendocument/index.d.ts +44 -0
  29. package/dist/parsers/opendocument/index.js +355 -0
  30. package/dist/parsers/sarif/index.d.ts +19 -0
  31. package/dist/parsers/sarif/index.js +238 -0
  32. package/dist/parsers/sbom/index.d.ts +19 -0
  33. package/dist/parsers/sbom/index.js +78 -0
  34. package/dist/parsers/trace/index.d.ts +23 -0
  35. package/dist/parsers/trace/index.js +129 -0
  36. package/dist/parsers/xml/prologue.d.ts +3 -0
  37. package/dist/parsers/xml/prologue.js +59 -0
  38. package/dist/registry/container.js +4 -0
  39. package/dist/registry/file-preview-sniff.d.ts +5 -0
  40. package/dist/registry/file-preview-sniff.js +112 -0
  41. package/dist/registry/file-previews.d.ts +57 -0
  42. package/dist/registry/file-previews.js +30 -0
  43. package/dist/registry/index.d.ts +1 -0
  44. package/dist/registry/index.js +22 -2
  45. package/dist/registry/probe.js +74 -145
  46. package/dist/styles/audio.css +4 -4
  47. package/dist/styles/file-preview.css +34 -0
  48. package/dist/viewers/audio/audio-window.d.ts +16 -0
  49. package/dist/viewers/audio/audio-window.js +141 -49
  50. package/dist/viewers/audio/index.d.ts +9 -0
  51. package/dist/viewers/audio/index.js +60 -16
  52. package/dist/viewers/audio/spectrogram-view.d.ts +40 -3
  53. package/dist/viewers/audio/spectrogram-view.js +247 -38
  54. package/dist/viewers/audio/spectrogram-worker.d.ts +27 -0
  55. package/dist/viewers/audio/spectrogram-worker.js +117 -0
  56. package/dist/viewers/audio/spectrogram.d.ts +0 -6
  57. package/dist/viewers/audio/spectrogram.js +12 -12
  58. package/dist/viewers/audio/styles.js +4 -4
  59. package/dist/viewers/drawio/index.d.ts +15 -0
  60. package/dist/viewers/drawio/index.js +447 -0
  61. package/dist/viewers/file-preview-styles.d.ts +1 -0
  62. package/dist/viewers/file-preview-styles.js +35 -0
  63. package/dist/viewers/file-preview.d.ts +36 -0
  64. package/dist/viewers/file-preview.js +248 -0
  65. package/dist/viewers/geojson/index.d.ts +4 -1
  66. package/dist/viewers/geojson/index.js +9 -4
  67. package/dist/viewers/gps/index.d.ts +15 -0
  68. package/dist/viewers/gps/index.js +170 -0
  69. package/dist/viewers/msg/index.d.ts +18 -0
  70. package/dist/viewers/msg/index.js +83 -0
  71. package/dist/viewers/opendocument/index.d.ts +15 -0
  72. package/dist/viewers/opendocument/index.js +122 -0
  73. package/dist/viewers/sarif/index.d.ts +14 -0
  74. package/dist/viewers/sarif/index.js +35 -0
  75. package/dist/viewers/sbom/index.d.ts +14 -0
  76. package/dist/viewers/sbom/index.js +44 -0
  77. package/dist/viewers/trace/args-preview.d.ts +4 -0
  78. package/dist/viewers/trace/args-preview.js +60 -0
  79. package/dist/viewers/trace/index.d.ts +14 -0
  80. package/dist/viewers/trace/index.js +167 -0
  81. package/package.json +57 -1
package/README.md CHANGED
@@ -14,23 +14,23 @@ models, viewers mount into a DOM element, and everything host-specific
14
14
 
15
15
  - **Documents** — Plain text (.txt), PDF, Word (DOCX and legacy DOC), HWP, PowerPoint (PPTX and
16
16
  legacy PPT), HTML, Markdown, Jupyter Notebook (.ipynb), EPUB e-books (2 and 3),
17
- LaTeX (structure and math preview, not typesetting)
17
+ LaTeX (structure and math preview, not typesetting), OpenDocument (ODT/ODP)
18
18
  - **Data & spreadsheets** — Excel, CSV/TSV, JSON, JSONL/NDJSON, YAML, TOML, XML,
19
19
  Parquet, Arrow IPC / Feather V2 (.arrow/.feather), Avro, HDF5, MATLAB MAT, NumPy (NPY/NPZ), Safetensors, GGUF, ONNX, TFLite/LiteRT,
20
20
  Keras (.keras and legacy .h5), Core ML (.mlmodel and .mlpackage),
21
21
  OpenVINO IR (.xml + .bin), PyTorch Export (.pt2), ExecuTorch (.pte), Protocol
22
22
  Buffers, ReqIF, SQLite
23
- - **Email, web & network** — EML email messages, HAR (HTTP Archive) request logs,
23
+ - **Email, web & network** — EML and Outlook MSG email messages, HAR (HTTP Archive) request logs,
24
24
  OpenAPI / Swagger API documents (JSON/YAML)
25
- - **Developer tooling** — JUnit XML test reports (Gradle, Maven Surefire,
26
- pytest, vitest/Jest, Jenkins and other JUnit-format writers)
25
+ - **Developer tooling** — JUnit XML test reports, SARIF 2.1.0, CycloneDX JSON
26
+ SBOMs, Chrome Trace Event JSON
27
27
  - **Medical imaging** — DICOM (.dcm/.dicom), native grayscale/RGB image preview,
28
28
  multi-frame navigation, windowing and metadata inspection
29
29
  - **Media & graphics** — audio (waveform/spectrogram), video, images,
30
30
  Photoshop PSD, fonts (TTF/OTF/WOFF), 3D meshes (GLB/glTF/STL/OBJ)
31
31
  - **Engineering & automotive** — CAN DBC, AUTOSAR ARXML, ASAM A2L, Vector
32
32
  ASC/BLF logs, ASAM MDF 4 (MF4), PCAP/PCAPNG, ROS bag, MCAP (.mcap), STEP (STP)
33
- - **Diagrams & GIS** — Mermaid, PlantUML, ESRI Shapefile, GeoJSON
33
+ - **Diagrams & GIS** — Mermaid, PlantUML, draw.io, ESRI Shapefile, GeoJSON, GPX, KML and KMZ
34
34
  - **Archives** — ZIP-family listing and safe entry preview
35
35
 
36
36
  The HTML viewer handles `.html` and `.htm` files with preview and source tabs.
@@ -123,6 +123,40 @@ Viewers follow the same pattern — a `mount*Viewer(input, container, ctx,
123
123
  deps)` entry per format, plus `self-loading` variants that dynamically import
124
124
  their own dependencies for hosts that don't want to wire them manually.
125
125
 
126
+ ### Read-only file previews
127
+
128
+ | Viewer ID | Parser / mount | Preview |
129
+ | --- | --- | --- |
130
+ | `sarif` | `parseSarif` / `mountSarifViewer` | Result severity, rule, tool and source locations |
131
+ | `gps` | `parseGps` / `mountGpsViewer` | Offline GPX/KML/KMZ map and GPX track statistics |
132
+ | `trace` | `parseTrace` / `mountTraceViewer` | Chrome Trace Event JSON timeline with search and zoom |
133
+ | `sbom` | `parseSbom` / `mountSbomViewer` | CycloneDX components, licenses and direct dependencies |
134
+ | `msg` | `parseMsg` / `mountMsgViewer` | Outlook message headers, text/HTML and file attachments |
135
+ | `drawio` | `parseDrawio` / `mountDrawioViewer` | Compressed or plain diagrams, basic shapes, connectors and layers |
136
+ | `opendocument` | `parseOpenDocument` / `mountOpenDocumentViewer` | ODT structure and basic ODP slide layout |
137
+
138
+ Import each from `omni-viewer-core/parsers/<id>` or
139
+ `omni-viewer-core/viewers/<id>`. Register the extensions and map the viewer IDs
140
+ to their mount functions in the host. Scoped mounts use
141
+ `omni-viewer-core/styles/file-preview.css`; the GPS map also uses
142
+ `omni-viewer-core/styles/geojson.css`. Deflated KMZ, OpenDocument and draw.io
143
+ content uses the runtime’s raw deflate `DecompressionStream` or a bounded
144
+ `inflate` function supplied by the host.
145
+
146
+ When `detectViewer` receives a text sample, it also routes `.json` and
147
+ extensionless SARIF 2.1.0, CycloneDX and Trace Event JSON to `sarif`, `sbom`
148
+ and `trace`, and `.xml` and extensionless namespaced GPX/KML and draw.io XML to
149
+ `gps` and `drawio`. Extensionless KMZ, ODT/ODP and Outlook MSG are recognized by
150
+ `probeContainer`. A `.msg` file must be an OLE compound file, so plain-text
151
+ `.msg` files (ROS messages, Tcl catalogs) keep their previous routing.
152
+
153
+ The GPS map draws geometry without basemap tiles and skips KML network links
154
+ and overlays. The trace preview supports complete, begin/end and instant events.
155
+ The MSG preview reads ordinary attachments and displays a diagnostic for
156
+ embedded messages or RTF-only bodies. draw.io approximates advanced styles
157
+ with basic shapes. OpenDocument previews text structure and basic slide layout.
158
+ Each preview reports records omitted because of resource limits.
159
+
126
160
  ### OpenAPI / Swagger (JSON / YAML)
127
161
 
128
162
  Open Swagger 2.0 and OpenAPI 3.0–3.2 specifications with searchable API
@@ -827,7 +861,7 @@ stored `mimetype` member.
827
861
  ### XML (.xml and XML dialects)
828
862
 
829
863
  Configuration and integration documents (`pom.xml`, Android manifests,
830
- MSBuild projects, SOAP/WSDL, RSS/Atom, XLIFF, KML/GPX…) open as an
864
+ MSBuild projects, SOAP/WSDL, RSS/Atom, XLIFF…) open as an
831
865
  element/attribute tree with a node inspector and the original source side by
832
866
  side:
833
867
 
@@ -851,9 +885,11 @@ comments and PIs, with next/previous and a matches-only filter. Copy gives the
851
885
  XPath (positions appear only where names repeat), the value, or the node's XML.
852
886
  Both panes are virtualized, and long single-line sources are split into
853
887
  display segments. Defaults are 64 MiB input, 500,000 nodes, and depth 1,000.
854
- ARXML and ReqIF keep their dedicated viewers by extension. An `.xml` whose root
855
- is an OpenVINO IR or a JUnit test report goes to the OpenVINO or JUnit viewer
856
- when a text sample is provided.
888
+ ARXML and ReqIF keep their dedicated viewers by extension, and `.gpx`/`.kml`
889
+ open in the GPS map. When a text sample is provided, an `.xml` whose root is an
890
+ OpenVINO IR, a JUnit test report, a namespaced GPX/KML document or a draw.io
891
+ `<mxfile>`/`<mxGraphModel>` goes to the OpenVINO, JUnit, GPS or draw.io viewer;
892
+ a GPX/KML root without its namespace stays on this viewer.
857
893
 
858
894
  ### Archive host integration
859
895
 
@@ -0,0 +1,256 @@
1
+ // Spectrogram computation, DOM-free and decoder-free.
2
+ //
3
+ // Ported from `native/audio-engine/audio_engine.c` (`generate_spectrogram`),
4
+ // which in turn replicates the WaveSurfer plugin's pipeline: Hann window →
5
+ // FFT → frequency-scale filter bank → dB → 8-bit colour index. Keeping the
6
+ // same pipeline means the windowed renderer looks like the whole-file one it
7
+ // replaces.
8
+ //
9
+ // The point of doing it here rather than in the plugin is the input: this takes
10
+ // *a window of samples*, so cost follows the viewport instead of the file. A
11
+ // two-hour track costs the same as a ten-second one.
12
+ const sinc = (x) => (x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x));
13
+ /** Window coefficients. `alpha` is only consulted by blackman and gauss. */
14
+ export function windowCoefficients(size, kind = 'hann', alpha) {
15
+ const out = new Float32Array(size);
16
+ const last = size - 1;
17
+ for (let i = 0; i < size; i++) {
18
+ switch (kind) {
19
+ case 'rectangular':
20
+ out[i] = 1;
21
+ break;
22
+ case 'hann':
23
+ out[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / last));
24
+ break;
25
+ case 'hamming':
26
+ out[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / last);
27
+ break;
28
+ case 'bartlett':
29
+ out[i] = (2 / last) * (last / 2 - Math.abs(i - last / 2));
30
+ break;
31
+ case 'triangular':
32
+ out[i] = (2 / size) * (size / 2 - Math.abs(i - last / 2));
33
+ break;
34
+ case 'bartlettHann':
35
+ out[i] = 0.62 - 0.48 * Math.abs(i / last - 0.5) - 0.38 * Math.cos((2 * Math.PI * i) / last);
36
+ break;
37
+ case 'blackman': {
38
+ const a = alpha ?? 0.16;
39
+ out[i] = (1 - a) / 2
40
+ - 0.5 * Math.cos((2 * Math.PI * i) / last)
41
+ + (a / 2) * Math.cos((4 * Math.PI * i) / last);
42
+ break;
43
+ }
44
+ case 'cosine':
45
+ out[i] = Math.cos((i * Math.PI) / last - Math.PI / 2);
46
+ break;
47
+ case 'gauss': {
48
+ const a = alpha ?? 0.25;
49
+ out[i] = Math.exp(-0.5 * ((i - last / 2) / ((a * last) / 2)) ** 2);
50
+ break;
51
+ }
52
+ case 'lanczoz':
53
+ out[i] = sinc((2 * i) / last - 1);
54
+ break;
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+ const DEFAULT_FFT_SIZE = 1024;
60
+ const DEFAULT_GAIN_DB = 20;
61
+ const DEFAULT_RANGE_DB = 80;
62
+ /** Floor for the log: silence would otherwise be -Infinity dB. */
63
+ const MIN_MAGNITUDE = 1e-12;
64
+ // Scale conversions, matching the plugin's own set.
65
+ const toMel = (hz) => 2595 * Math.log10(1 + hz / 700);
66
+ const fromMel = (mel) => 700 * (10 ** (mel / 2595) - 1);
67
+ const toBark = (hz) => {
68
+ let bark = (26.81 * hz) / (1960 + hz) - 0.53;
69
+ if (bark < 2)
70
+ bark += 0.15 * (2 - bark);
71
+ if (bark > 20.1)
72
+ bark += 0.22 * (bark - 20.1);
73
+ return bark;
74
+ };
75
+ const fromBark = (bark) => {
76
+ let value = bark;
77
+ if (value < 2)
78
+ value = (value - 0.3) / 0.85;
79
+ if (value > 20.1)
80
+ value = (value + 4.422) / 1.22;
81
+ return ((value + 0.53) / (26.28 - value)) * 1960;
82
+ };
83
+ const toLog = (hz) => Math.log10(Math.max(1, hz));
84
+ const fromLog = (value) => 10 ** value;
85
+ const ERB_SCALE = (1000 * Math.LN10) / 107.939;
86
+ const toErb = (hz) => ERB_SCALE * Math.log10(1 + 0.00437 * hz);
87
+ const fromErb = (erb) => (10 ** (erb / ERB_SCALE) - 1) / 0.00437;
88
+ const forward = (scale) => scale === 'mel' ? toMel : scale === 'bark' ? toBark : scale === 'erb' ? toErb
89
+ : scale === 'logarithmic' ? toLog : (hz) => hz;
90
+ const inverse = (scale) => scale === 'mel' ? fromMel : scale === 'bark' ? fromBark : scale === 'erb' ? fromErb
91
+ : scale === 'logarithmic' ? fromLog : (value) => value;
92
+ /**
93
+ * Frequency at a fraction of the height, 0 at the bottom.
94
+ *
95
+ * The axis is only readable with labels on it, and the labels have to follow
96
+ * whichever scale drew the picture — evenly spaced rows are far from evenly
97
+ * spaced hertz on mel or log.
98
+ */
99
+ export function frequencyAtFraction(fraction, scale, minHz, maxHz) {
100
+ const toScale = forward(scale);
101
+ const fromScale = inverse(scale);
102
+ const low = toScale(Math.max(0, minHz));
103
+ const high = toScale(Math.max(minHz, maxHz));
104
+ return fromScale(low + Math.min(1, Math.max(0, fraction)) * (high - low));
105
+ }
106
+ /** In-place radix-2 FFT. Tables are precomputed per size and reused. */
107
+ function createFft(size) {
108
+ if (size < 2 || (size & (size - 1)) !== 0)
109
+ throw new Error(`spectrogram: fftSize must be a power of two, got ${size}`);
110
+ const levels = Math.log2(size);
111
+ const cos = new Float32Array(size / 2);
112
+ const sin = new Float32Array(size / 2);
113
+ for (let i = 0; i < size / 2; i++) {
114
+ cos[i] = Math.cos((2 * Math.PI * i) / size);
115
+ sin[i] = Math.sin((2 * Math.PI * i) / size);
116
+ }
117
+ // Bit-reversal permutation, precomputed so the hot loop stays arithmetic.
118
+ const reversed = new Uint32Array(size);
119
+ for (let i = 0; i < size; i++) {
120
+ let bits = 0;
121
+ for (let bit = 0; bit < levels; bit++)
122
+ bits = (bits << 1) | ((i >>> bit) & 1);
123
+ reversed[i] = bits;
124
+ }
125
+ return (real, imag) => {
126
+ for (let i = 0; i < size; i++) {
127
+ const j = reversed[i];
128
+ if (j > i) {
129
+ let swap = real[i];
130
+ real[i] = real[j];
131
+ real[j] = swap;
132
+ swap = imag[i];
133
+ imag[i] = imag[j];
134
+ imag[j] = swap;
135
+ }
136
+ }
137
+ for (let span = 2; span <= size; span *= 2) {
138
+ const half = span / 2;
139
+ const step = size / span;
140
+ for (let start = 0; start < size; start += span) {
141
+ for (let i = start, k = 0; i < start + half; i++, k += step) {
142
+ const pair = i + half;
143
+ const tre = real[pair] * cos[k] + imag[pair] * sin[k];
144
+ const tim = -real[pair] * sin[k] + imag[pair] * cos[k];
145
+ real[pair] = real[i] - tre;
146
+ imag[pair] = imag[i] - tim;
147
+ real[i] = real[i] + tre;
148
+ imag[i] = imag[i] + tim;
149
+ }
150
+ }
151
+ }
152
+ };
153
+ }
154
+ /** FFT tables per size. A viewport is transformed as many small batches, and
155
+ * rebuilding the trig and bit-reversal tables for each one is pure overhead. */
156
+ const fftCache = new Map();
157
+ function fftFor(size) {
158
+ let fft = fftCache.get(size);
159
+ if (!fft) {
160
+ fft = createFft(size);
161
+ fftCache.set(size, fft);
162
+ }
163
+ return fft;
164
+ }
165
+ /** Where each output row samples the linear FFT bins, with the interpolation
166
+ * weights between the two neighbours. */
167
+ function filterBank(bins, fftSize, sampleRate, scale, minHz, maxHz) {
168
+ const toScale = forward(scale);
169
+ const fromScale = inverse(scale);
170
+ const nyquist = sampleRate / 2;
171
+ const low = toScale(Math.max(0, Math.min(minHz, nyquist)));
172
+ const high = toScale(Math.max(low, Math.min(maxHz, nyquist)));
173
+ const hzPerBin = sampleRate / fftSize;
174
+ const bank = new Array(bins);
175
+ for (let row = 0; row < bins; row++) {
176
+ const centerHz = fromScale(low + (row / bins) * (high - low));
177
+ const exact = centerHz / hzPerBin;
178
+ const lo = Math.floor(exact);
179
+ const frac = exact - lo;
180
+ bank[row] = { lo, weightLo: 1 - frac, weightHi: frac };
181
+ }
182
+ return bank;
183
+ }
184
+ /**
185
+ * Intensities for one window of interleaved-free mono samples.
186
+ *
187
+ * Channels are the caller's business: pass a single channel, or a downmix.
188
+ * Columns run left to right in time, rows low to high in frequency.
189
+ */
190
+ export function computeSpectrogram(samples, sampleRate, options = {}) {
191
+ const fftSize = options.fftSize ?? DEFAULT_FFT_SIZE;
192
+ const hopSize = options.hopSize ?? Math.max(1, Math.floor(fftSize / 4));
193
+ const bins = options.bins ?? fftSize / 2;
194
+ const scale = options.scale ?? 'mel';
195
+ const gainDb = options.gainDb ?? DEFAULT_GAIN_DB;
196
+ const rangeDb = options.rangeDb ?? DEFAULT_RANGE_DB;
197
+ if (hopSize < 1)
198
+ throw new Error('spectrogram: hopSize must be >= 1');
199
+ if (bins < 1)
200
+ throw new Error('spectrogram: bins must be >= 1');
201
+ if (rangeDb <= 0)
202
+ throw new Error('spectrogram: rangeDb must be > 0');
203
+ const fft = fftFor(fftSize);
204
+ const columns = samples.length >= fftSize
205
+ ? Math.floor((samples.length - fftSize) / hopSize) + 1
206
+ : 0;
207
+ const data = new Uint8Array(Math.max(0, columns) * bins);
208
+ if (columns <= 0)
209
+ return { columns: 0, bins, data };
210
+ const window = windowCoefficients(fftSize, options.windowFunc ?? 'hann', options.alpha);
211
+ const real = new Float32Array(fftSize);
212
+ const imag = new Float32Array(fftSize);
213
+ const halfBins = fftSize / 2;
214
+ const magnitudes = new Float32Array(halfBins);
215
+ const bank = filterBank(bins, fftSize, sampleRate, scale, options.frequencyMin ?? 0, options.frequencyMax ?? sampleRate / 2);
216
+ const norm = 2 / fftSize;
217
+ // White point sits `gainDb` below full scale and `rangeDb` more reaches
218
+ // black. This is the WaveSurfer plugin's convention (it maps with a white
219
+ // point of `-gainDB`); taking `gainDb` as an absolute ceiling instead put
220
+ // the floor 40 dB higher and rendered ordinary material almost entirely
221
+ // black.
222
+ const whiteDb = -gainDb;
223
+ const floorDb = whiteDb - rangeDb;
224
+ for (let column = 0; column < columns; column++) {
225
+ const offset = column * hopSize;
226
+ for (let i = 0; i < fftSize; i++) {
227
+ real[i] = samples[offset + i] * window[i];
228
+ imag[i] = 0;
229
+ }
230
+ fft(real, imag);
231
+ for (let bin = 0; bin < halfBins; bin++) {
232
+ const re = real[bin];
233
+ const im = imag[bin];
234
+ magnitudes[bin] = norm * Math.sqrt(re * re + im * im);
235
+ }
236
+ const base = column * bins;
237
+ for (let row = 0; row < bins; row++) {
238
+ const { lo, weightLo, weightHi } = bank[row];
239
+ const hi = lo + 1;
240
+ let magnitude;
241
+ if (lo >= halfBins)
242
+ magnitude = MIN_MAGNITUDE;
243
+ else if (hi >= halfBins)
244
+ magnitude = magnitudes[lo] * weightLo;
245
+ else
246
+ magnitude = magnitudes[lo] * weightLo + magnitudes[hi] * weightHi;
247
+ if (magnitude < MIN_MAGNITUDE)
248
+ magnitude = MIN_MAGNITUDE;
249
+ const db = 20 * Math.log10(magnitude);
250
+ data[base + row] = db <= floorDb ? 0
251
+ : db >= whiteDb ? 255
252
+ : Math.round(((db - floorDb) / rangeDb) * 255);
253
+ }
254
+ }
255
+ return { columns, bins, data };
256
+ }
@@ -0,0 +1,24 @@
1
+ // Worker shell for the spectrogram transform. Deliberately dumb, like
2
+ // audio_engine_worker.mjs: it owns no analysis logic, only the message
3
+ // plumbing, because everything here is outside the TypeScript build and
4
+ // therefore untypechecked and untested.
5
+ //
6
+ // The transform is the compiled `viewers/audio/spectrogram.ts`, shipped beside
7
+ // this file as `spectrogram_kernel.mjs` (see scripts/build-styles.mjs). Its URL
8
+ // arrives with each message rather than being resolved relative to this file:
9
+ // adapters that serve the core from blob URLs (obsidian) have no usable
10
+ // sibling paths.
11
+
12
+ let kernelPromise;
13
+
14
+ self.onmessage = async (event) => {
15
+ const { id, kernelUrl, samples, sampleRate, options } = event.data;
16
+ try {
17
+ kernelPromise ??= import(kernelUrl);
18
+ const { computeSpectrogram } = await kernelPromise;
19
+ const result = computeSpectrogram(samples, sampleRate, options);
20
+ self.postMessage({ id, ok: true, result }, [result.data.buffer]);
21
+ } catch (error) {
22
+ self.postMessage({ id, ok: false, error: String(error?.message ?? error) });
23
+ }
24
+ };
@@ -4,6 +4,71 @@
4
4
  // this catalog at build time. Diagnostics (§3-① messageKey) and viewer UI
5
5
  // strings share this key space.
6
6
  export const CATALOG_EN = {
7
+ // Read-only file previews
8
+ "preview.invalid": "The file does not match this preview format.",
9
+ "preview.encrypted": "This file contains encrypted content.",
10
+ "preview.unsupported": "Some content uses features outside this preview’s support.",
11
+ "preview.truncated": "Some content was omitted because it exceeded the preview limit.",
12
+ "preview.skipped": "Some records could not be read and were skipped.",
13
+ "preview.previous": "Previous",
14
+ "preview.next": "Next",
15
+ "preview.page": "{current} / {total}",
16
+ "preview.search": "Search",
17
+ "preview.all": "All",
18
+ "preview.matches": "{count} of {total} items",
19
+ "preview.file": "File",
20
+ "preview.message": "Message",
21
+ "preview.description": "Description",
22
+ "preview.name": "Name",
23
+ "preview.zoomIn": "Zoom in",
24
+ "preview.zoomOut": "Zoom out",
25
+ "preview.fit": "Fit",
26
+ "preview.empty": "No items to display.",
27
+ "sarif.title": "SARIF analysis report",
28
+ "sarif.summary": "{runs} analysis runs · {count} results",
29
+ "sarif.resultsUnavailable": "Results are unavailable for one or more analysis runs.",
30
+ "sarif.level": "Severity",
31
+ "sarif.rule": "Rule",
32
+ "sarif.tool": "Tool",
33
+ "sarif.error": "Error",
34
+ "sarif.warning": "Warning",
35
+ "sarif.note": "Note",
36
+ "sarif.none": "None",
37
+ "gps.title": "GPS map",
38
+ "gps.summary": "{features} features · {points} points",
39
+ "gps.distance": "Distance",
40
+ "gps.elapsed": "Elapsed time",
41
+ "gps.elevation": "Elevation",
42
+ "gps.km": "{value} km",
43
+ "gps.seconds": "{value} s",
44
+ "gps.meters": "{min}–{max} m",
45
+ "trace.title": "Performance trace",
46
+ "trace.summary": "{tracks} tracks · {events} events",
47
+ "trace.range": "Visible range {start}–{end}",
48
+ "trace.position": "Timeline position",
49
+ "trace.track": "Track",
50
+ "trace.category": "Category",
51
+ "trace.start": "Start",
52
+ "trace.duration": "Duration",
53
+ "trace.dense": "Some events are hidden. Zoom in or search to show fewer events at once.",
54
+ "sbom.title": "CycloneDX SBOM",
55
+ "sbom.summary": "CycloneDX {version} · {count} components",
56
+ "sbom.version": "Version",
57
+ "sbom.type": "Type",
58
+ "sbom.license": "License",
59
+ "sbom.purl": "Package URL",
60
+ "sbom.dependencies": "Direct dependencies",
61
+ "sbom.dependenciesNone": "None",
62
+ "sbom.dependenciesUnknown": "Unknown",
63
+ "msg.title": "Outlook message",
64
+ "msg.from": "From",
65
+ "msg.to": "To",
66
+ "msg.cc": "Cc",
67
+ "msg.date": "Date",
68
+ "drawio.title": "draw.io diagram",
69
+ "drawio.layer": "Layer {index}",
70
+ "drawio.page": "Page {index}",
71
+ "opendocument.title": "OpenDocument",
7
72
  // Plain text
8
73
  'text.title': 'Plain text',
9
74
  'text.encoding': 'Encoding: {encoding}',
@@ -1,5 +1,70 @@
1
1
  // Japanese messages owned by core. Keep the key set aligned with CATALOG_EN.
2
2
  export const CATALOG_JA = {
3
+ // Read-only file previews
4
+ "preview.invalid": "このプレビュー形式に一致しないファイルです。",
5
+ "preview.encrypted": "このファイルには暗号化された内容があります。",
6
+ "preview.unsupported": "一部にプレビュー非対応の機能があります。",
7
+ "preview.truncated": "プレビューの上限を超えた内容は省略されました。",
8
+ "preview.skipped": "読み取れないレコードをスキップしました。",
9
+ "preview.previous": "前へ",
10
+ "preview.next": "次へ",
11
+ "preview.page": "{current} / {total}",
12
+ "preview.search": "検索",
13
+ "preview.all": "すべて",
14
+ "preview.matches": "{total} 件中 {count} 件",
15
+ "preview.file": "ファイル",
16
+ "preview.message": "メッセージ",
17
+ "preview.description": "説明",
18
+ "preview.name": "名前",
19
+ "preview.zoomIn": "拡大",
20
+ "preview.zoomOut": "縮小",
21
+ "preview.fit": "画面に合わせる",
22
+ "preview.empty": "表示する項目がありません。",
23
+ "sarif.title": "SARIF 分析レポート",
24
+ "sarif.summary": "分析実行 {runs} 件 · 結果 {count} 件",
25
+ "sarif.resultsUnavailable": "一部の分析実行の結果を確認できません。",
26
+ "sarif.level": "重要度",
27
+ "sarif.rule": "ルール",
28
+ "sarif.tool": "ツール",
29
+ "sarif.error": "エラー",
30
+ "sarif.warning": "警告",
31
+ "sarif.note": "注記",
32
+ "sarif.none": "なし",
33
+ "gps.title": "GPS マップ",
34
+ "gps.summary": "地物 {features} 件 · 地点 {points} 件",
35
+ "gps.distance": "距離",
36
+ "gps.elapsed": "経過時間",
37
+ "gps.elevation": "標高",
38
+ "gps.km": "{value} km",
39
+ "gps.seconds": "{value} 秒",
40
+ "gps.meters": "{min}〜{max} m",
41
+ "trace.title": "パフォーマンストレース",
42
+ "trace.summary": "トラック {tracks} 件 · イベント {events} 件",
43
+ "trace.range": "表示範囲 {start}〜{end}",
44
+ "trace.position": "タイムラインの位置",
45
+ "trace.track": "トラック",
46
+ "trace.category": "カテゴリ",
47
+ "trace.start": "開始",
48
+ "trace.duration": "継続時間",
49
+ "trace.dense": "一部のイベントは非表示です。拡大または検索で絞り込めます。",
50
+ "sbom.title": "CycloneDX SBOM",
51
+ "sbom.summary": "CycloneDX {version} · コンポーネント {count} 件",
52
+ "sbom.version": "バージョン",
53
+ "sbom.type": "種類",
54
+ "sbom.license": "ライセンス",
55
+ "sbom.purl": "パッケージ URL",
56
+ "sbom.dependencies": "直接の依存関係",
57
+ "sbom.dependenciesNone": "なし",
58
+ "sbom.dependenciesUnknown": "不明",
59
+ "msg.title": "Outlook メッセージ",
60
+ "msg.from": "差出人",
61
+ "msg.to": "宛先",
62
+ "msg.cc": "Cc",
63
+ "msg.date": "日時",
64
+ "drawio.title": "draw.io ダイアグラム",
65
+ "drawio.layer": "レイヤー {index}",
66
+ "drawio.page": "ページ {index}",
67
+ "opendocument.title": "OpenDocument",
3
68
  // Plain text
4
69
  'text.title': 'プレーンテキスト',
5
70
  'text.encoding': '文字コード: {encoding}',
@@ -2,6 +2,71 @@
2
2
  // that is ever missing intentionally falls back to the English source catalog in
3
3
  // i18n/index.ts. Format names and proto keywords are intentionally left verbatim.
4
4
  export const CATALOG_KO = {
5
+ // Read-only file previews
6
+ "preview.invalid": "이 미리보기 형식에 맞지 않는 파일입니다.",
7
+ "preview.encrypted": "이 파일에는 암호화된 콘텐츠가 있습니다.",
8
+ "preview.unsupported": "미리보기에서 지원하지 않는 요소가 있습니다.",
9
+ "preview.truncated": "미리보기 한도를 초과한 일부 내용을 생략했습니다.",
10
+ "preview.skipped": "읽을 수 없는 일부 레코드를 건너뛰었습니다.",
11
+ "preview.previous": "이전",
12
+ "preview.next": "다음",
13
+ "preview.page": "{current} / {total}",
14
+ "preview.search": "검색",
15
+ "preview.all": "전체",
16
+ "preview.matches": "전체 {total}개 중 {count}개",
17
+ "preview.file": "파일",
18
+ "preview.message": "메시지",
19
+ "preview.description": "설명",
20
+ "preview.name": "이름",
21
+ "preview.zoomIn": "확대",
22
+ "preview.zoomOut": "축소",
23
+ "preview.fit": "화면에 맞추기",
24
+ "preview.empty": "표시할 항목이 없습니다.",
25
+ "sarif.title": "SARIF 분석 결과",
26
+ "sarif.summary": "분석 실행 {runs}회 · 결과 {count}개",
27
+ "sarif.resultsUnavailable": "일부 분석 실행의 결과를 확인할 수 없습니다.",
28
+ "sarif.level": "심각도",
29
+ "sarif.rule": "규칙",
30
+ "sarif.tool": "분석 도구",
31
+ "sarif.error": "오류",
32
+ "sarif.warning": "경고",
33
+ "sarif.note": "참고",
34
+ "sarif.none": "없음",
35
+ "gps.title": "GPS 지도",
36
+ "gps.summary": "지도 요소 {features}개 · 좌표 {points}개",
37
+ "gps.distance": "거리",
38
+ "gps.elapsed": "경과 시간",
39
+ "gps.elevation": "고도",
40
+ "gps.km": "{value} km",
41
+ "gps.seconds": "{value}초",
42
+ "gps.meters": "{min}~{max} m",
43
+ "trace.title": "성능 트레이스",
44
+ "trace.summary": "트랙 {tracks}개 · 이벤트 {events}개",
45
+ "trace.range": "표시 구간 {start}~{end}",
46
+ "trace.position": "타임라인 위치",
47
+ "trace.track": "트랙",
48
+ "trace.category": "범주",
49
+ "trace.start": "시작",
50
+ "trace.duration": "지속 시간",
51
+ "trace.dense": "일부 이벤트가 숨겨져 있습니다. 확대하거나 검색하여 표시 범위를 좁힐 수 있습니다.",
52
+ "sbom.title": "CycloneDX SBOM",
53
+ "sbom.summary": "CycloneDX {version} · 컴포넌트 {count}개",
54
+ "sbom.version": "버전",
55
+ "sbom.type": "유형",
56
+ "sbom.license": "라이선스",
57
+ "sbom.purl": "패키지 URL",
58
+ "sbom.dependencies": "직접 의존성",
59
+ "sbom.dependenciesNone": "없음",
60
+ "sbom.dependenciesUnknown": "알 수 없음",
61
+ "msg.title": "Outlook 메일",
62
+ "msg.from": "보낸 사람",
63
+ "msg.to": "받는 사람",
64
+ "msg.cc": "참조",
65
+ "msg.date": "날짜",
66
+ "drawio.title": "draw.io 다이어그램",
67
+ "drawio.layer": "레이어 {index}",
68
+ "drawio.page": "페이지 {index}",
69
+ "opendocument.title": "OpenDocument",
5
70
  // Plain text
6
71
  'text.title': '일반 텍스트',
7
72
  'text.encoding': '인코딩: {encoding}',
@@ -1,5 +1,70 @@
1
1
  // Simplified Chinese messages owned by core. Keep the key set aligned with CATALOG_EN.
2
2
  export const CATALOG_ZH_CN = {
3
+ // Read-only file previews
4
+ "preview.invalid": "文件与此预览格式不符。",
5
+ "preview.encrypted": "此文件包含加密内容。",
6
+ "preview.unsupported": "部分内容使用了此预览不支持的功能。",
7
+ "preview.truncated": "超出预览限制的部分内容已省略。",
8
+ "preview.skipped": "已跳过无法读取的部分记录。",
9
+ "preview.previous": "上一页",
10
+ "preview.next": "下一页",
11
+ "preview.page": "{current} / {total}",
12
+ "preview.search": "搜索",
13
+ "preview.all": "全部",
14
+ "preview.matches": "共 {total} 项,显示 {count} 项",
15
+ "preview.file": "文件",
16
+ "preview.message": "消息",
17
+ "preview.description": "说明",
18
+ "preview.name": "名称",
19
+ "preview.zoomIn": "放大",
20
+ "preview.zoomOut": "缩小",
21
+ "preview.fit": "适应窗口",
22
+ "preview.empty": "没有可显示的项目。",
23
+ "sarif.title": "SARIF 分析报告",
24
+ "sarif.summary": "{runs} 次分析 · {count} 条结果",
25
+ "sarif.resultsUnavailable": "部分分析运行的结果不可用。",
26
+ "sarif.level": "严重程度",
27
+ "sarif.rule": "规则",
28
+ "sarif.tool": "工具",
29
+ "sarif.error": "错误",
30
+ "sarif.warning": "警告",
31
+ "sarif.note": "提示",
32
+ "sarif.none": "无",
33
+ "gps.title": "GPS 地图",
34
+ "gps.summary": "{features} 个要素 · {points} 个点",
35
+ "gps.distance": "距离",
36
+ "gps.elapsed": "经过时间",
37
+ "gps.elevation": "海拔",
38
+ "gps.km": "{value} 公里",
39
+ "gps.seconds": "{value} 秒",
40
+ "gps.meters": "{min}–{max} 米",
41
+ "trace.title": "性能跟踪",
42
+ "trace.summary": "{tracks} 条轨道 · {events} 个事件",
43
+ "trace.range": "可见范围 {start}–{end}",
44
+ "trace.position": "时间轴位置",
45
+ "trace.track": "轨道",
46
+ "trace.category": "类别",
47
+ "trace.start": "开始",
48
+ "trace.duration": "持续时间",
49
+ "trace.dense": "部分事件已隐藏。请放大或搜索以缩小显示范围。",
50
+ "sbom.title": "CycloneDX SBOM",
51
+ "sbom.summary": "CycloneDX {version} · {count} 个组件",
52
+ "sbom.version": "版本",
53
+ "sbom.type": "类型",
54
+ "sbom.license": "许可证",
55
+ "sbom.purl": "软件包 URL",
56
+ "sbom.dependencies": "直接依赖",
57
+ "sbom.dependenciesNone": "无",
58
+ "sbom.dependenciesUnknown": "未知",
59
+ "msg.title": "Outlook 邮件",
60
+ "msg.from": "发件人",
61
+ "msg.to": "收件人",
62
+ "msg.cc": "抄送",
63
+ "msg.date": "日期",
64
+ "drawio.title": "draw.io 图表",
65
+ "drawio.layer": "图层 {index}",
66
+ "drawio.page": "第 {index} 页",
67
+ "opendocument.title": "OpenDocument",
3
68
  // Plain text
4
69
  'text.title': '纯文本',
5
70
  'text.encoding': '编码:{encoding}',