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.
- package/README.md +45 -9
- package/dist/assets/audio-engine/spectrogram_kernel.mjs +256 -0
- package/dist/assets/audio-engine/spectrogram_worker.mjs +24 -0
- package/dist/i18n/catalog.en.js +65 -0
- package/dist/i18n/catalog.ja.js +65 -0
- package/dist/i18n/catalog.ko.js +65 -0
- package/dist/i18n/catalog.zh-cn.js +65 -0
- package/dist/parsers/cfb.d.ts +21 -0
- package/dist/parsers/cfb.js +122 -0
- package/dist/parsers/doc-binary/index.js +2 -76
- package/dist/parsers/drawio/index.d.ts +46 -0
- package/dist/parsers/drawio/index.js +193 -0
- package/dist/parsers/epub/index.d.ts +2 -0
- package/dist/parsers/epub/index.js +16 -12
- package/dist/parsers/epub/zip.d.ts +2 -1
- package/dist/parsers/epub/zip.js +88 -44
- package/dist/parsers/file-preview.d.ts +45 -0
- package/dist/parsers/file-preview.js +179 -0
- package/dist/parsers/gps/index.d.ts +18 -0
- package/dist/parsers/gps/index.js +264 -0
- package/dist/parsers/har/index.d.ts +1 -7
- package/dist/parsers/har/index.js +2 -42
- package/dist/parsers/iso8601.d.ts +7 -0
- package/dist/parsers/iso8601.js +42 -0
- package/dist/parsers/junit/sniff.js +3 -61
- package/dist/parsers/msg/index.d.ts +17 -0
- package/dist/parsers/msg/index.js +222 -0
- package/dist/parsers/opendocument/index.d.ts +44 -0
- package/dist/parsers/opendocument/index.js +355 -0
- package/dist/parsers/sarif/index.d.ts +19 -0
- package/dist/parsers/sarif/index.js +238 -0
- package/dist/parsers/sbom/index.d.ts +19 -0
- package/dist/parsers/sbom/index.js +78 -0
- package/dist/parsers/trace/index.d.ts +23 -0
- package/dist/parsers/trace/index.js +129 -0
- package/dist/parsers/xml/prologue.d.ts +3 -0
- package/dist/parsers/xml/prologue.js +59 -0
- package/dist/registry/container.js +4 -0
- package/dist/registry/file-preview-sniff.d.ts +5 -0
- package/dist/registry/file-preview-sniff.js +112 -0
- package/dist/registry/file-previews.d.ts +57 -0
- package/dist/registry/file-previews.js +30 -0
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +22 -2
- package/dist/registry/probe.js +74 -145
- package/dist/styles/audio.css +4 -4
- package/dist/styles/file-preview.css +34 -0
- package/dist/viewers/audio/audio-window.d.ts +16 -0
- package/dist/viewers/audio/audio-window.js +141 -49
- package/dist/viewers/audio/index.d.ts +9 -0
- package/dist/viewers/audio/index.js +60 -16
- package/dist/viewers/audio/spectrogram-view.d.ts +40 -3
- package/dist/viewers/audio/spectrogram-view.js +247 -38
- package/dist/viewers/audio/spectrogram-worker.d.ts +27 -0
- package/dist/viewers/audio/spectrogram-worker.js +117 -0
- package/dist/viewers/audio/spectrogram.d.ts +0 -6
- package/dist/viewers/audio/spectrogram.js +12 -12
- package/dist/viewers/audio/styles.js +4 -4
- package/dist/viewers/drawio/index.d.ts +15 -0
- package/dist/viewers/drawio/index.js +447 -0
- package/dist/viewers/file-preview-styles.d.ts +1 -0
- package/dist/viewers/file-preview-styles.js +35 -0
- package/dist/viewers/file-preview.d.ts +36 -0
- package/dist/viewers/file-preview.js +248 -0
- package/dist/viewers/geojson/index.d.ts +4 -1
- package/dist/viewers/geojson/index.js +9 -4
- package/dist/viewers/gps/index.d.ts +15 -0
- package/dist/viewers/gps/index.js +170 -0
- package/dist/viewers/msg/index.d.ts +18 -0
- package/dist/viewers/msg/index.js +83 -0
- package/dist/viewers/opendocument/index.d.ts +15 -0
- package/dist/viewers/opendocument/index.js +122 -0
- package/dist/viewers/sarif/index.d.ts +14 -0
- package/dist/viewers/sarif/index.js +35 -0
- package/dist/viewers/sbom/index.d.ts +14 -0
- package/dist/viewers/sbom/index.js +44 -0
- package/dist/viewers/trace/args-preview.d.ts +4 -0
- package/dist/viewers/trace/args-preview.js +60 -0
- package/dist/viewers/trace/index.d.ts +14 -0
- package/dist/viewers/trace/index.js +167 -0
- 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
|
|
26
|
-
|
|
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
|
|
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
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
};
|
package/dist/i18n/catalog.en.js
CHANGED
|
@@ -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}',
|
package/dist/i18n/catalog.ja.js
CHANGED
|
@@ -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}',
|
package/dist/i18n/catalog.ko.js
CHANGED
|
@@ -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}',
|