@crewhaus/tool-media 0.7.0 → 0.8.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +34 -8
- package/dist/index.js +149 -26
- package/dist/lib/color.d.ts +2 -0
- package/dist/lib/color.js +17 -3
- package/dist/lib/jpeg.d.ts +141 -3
- package/dist/lib/jpeg.js +527 -10
- package/dist/lib/subtitle.d.ts +13 -0
- package/dist/lib/subtitle.js +27 -7
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/package.json +10 -6
- package/dist/fixtures.d.ts +0 -81
- package/dist/fixtures.js +0 -385
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CrewHaus Factory
|
|
2
|
+
Copyright 2026 Max Meier
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Max Meier
|
|
5
|
+
(operating as StudioMax — https://studiomax.io) and contributors.
|
|
6
|
+
|
|
7
|
+
The CrewHaus name and logo are trademarks of Max Meier. See
|
|
8
|
+
https://github.com/crewhaus/factory/blob/main/TRADEMARK.md for the
|
|
9
|
+
use-of-marks policy.
|
package/README.md
CHANGED
|
@@ -14,8 +14,9 @@ not.
|
|
|
14
14
|
|
|
15
15
|
```yaml
|
|
16
16
|
tools:
|
|
17
|
-
- all-media # every tool below
|
|
18
|
-
- -
|
|
17
|
+
- all-media # every tool below, plus ReadImage and ImageGenerate
|
|
18
|
+
- -mediaProbe # ...except the one that shells out
|
|
19
|
+
- -imageGenerate # ...and the one that bills a remote model
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
| Tool | What it does |
|
|
@@ -104,10 +105,18 @@ reported in `backEdges`.
|
|
|
104
105
|
|
|
105
106
|
**They will not re-encode a photograph.** `ExifStrip` rewrites the segment
|
|
106
107
|
list and copies the entropy-coded scan data verbatim, so the picture is
|
|
107
|
-
bit-for-bit what it was. It
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
bit-for-bit what it was. It walks the whole file: metadata segments between
|
|
109
|
+
the scans of a progressive JPEG are dropped too, and so is everything after
|
|
110
|
+
the end-of-image marker (an appended preview, second view, HDR gain map or
|
|
111
|
+
motion-photo video, each of which can carry its own EXIF and location). The
|
|
112
|
+
result says how many trailing bytes went and what they looked like. A file
|
|
113
|
+
that breaks off after its first scan (a partial download) is stripped up to
|
|
114
|
+
the break and the rest kept as it was, as 0.7.0 did; a cut-off metadata
|
|
115
|
+
segment there is dropped, and unframed bytes that look like metadata are
|
|
116
|
+
refused rather than written into a file that claims to be clean. It keeps
|
|
117
|
+
the ICC colour profile by default, because a profile is not metadata about
|
|
118
|
+
the photographer: it is what tells a display how to interpret the colours,
|
|
119
|
+
and dropping it visibly shifts the image.
|
|
111
120
|
|
|
112
121
|
**They will not guess.** A 16-bit or interlaced PNG is refused by name
|
|
113
122
|
rather than truncated or de-interlaced badly. A payload past a QR code's
|
|
@@ -140,8 +149,9 @@ too. No EAN-8, and no 2- or 5-digit add-ons. Bars only — the
|
|
|
140
149
|
human-readable digits underneath would need a font.
|
|
141
150
|
|
|
142
151
|
**EXIF.** IFD0, the Exif sub-IFD and the GPS sub-IFD of an `APP1` segment,
|
|
143
|
-
both byte orders. Not MakerNotes, IFD1 thumbnails,
|
|
144
|
-
|
|
152
|
+
both byte orders. Not MakerNotes, IFD1 thumbnails, or EXIF in a TIFF or HEIC.
|
|
153
|
+
XMP (Extended XMP reassembled) and a Photoshop `APP13` block are searched for
|
|
154
|
+
a location, not parsed field by field.
|
|
145
155
|
|
|
146
156
|
**Subtitles.** SRT and WebVTT, through a BOM and either line ending, with
|
|
147
157
|
cue ids and cue settings preserved. Not WebVTT chapter or metadata tracks.
|
|
@@ -154,3 +164,19 @@ A photograph taken on a phone usually records where it was taken.
|
|
|
154
164
|
`ExifRead` reports that as `hasGps: true` with a `privacyWarning`, rather
|
|
155
165
|
than burying the coordinates in a field list, because publishing the file
|
|
156
166
|
publishes the location. `ExifStrip` is the tool that removes it.
|
|
167
|
+
|
|
168
|
+
`ExifRead` reads the whole file, not just its first megabyte, and counts
|
|
169
|
+
GPS found anywhere: before the first scan, between scans, in a JPEG appended
|
|
170
|
+
after the main one, or in an EXIF block sitting in bytes no walk accounts
|
|
171
|
+
for (a video, data it does not recognise, an image that would not walk). It
|
|
172
|
+
reports `trailingBytes`, `interScanMetadataSegments` and every EXIF block
|
|
173
|
+
found outside the main image's own. When it cannot tell and found no GPS
|
|
174
|
+
elsewhere, `hasGps` is `null` with the reason in `gpsUndetermined`, never
|
|
175
|
+
`false`: an EXIF block that does not parse, more appended images than it
|
|
176
|
+
walks, bytes after the image that no walk accounts for (padding aside), a
|
|
177
|
+
file that breaks off after its first scan, a file past 64 MiB, which is
|
|
178
|
+
read that far and answered from what was read, or a metadata segment it does
|
|
179
|
+
not read (a C2PA manifest in `APP11`, a vendor `APPn`), which it names. GPS
|
|
180
|
+
written only in XMP, or in the EXIF or XMP a Photoshop `APP13` block keeps,
|
|
181
|
+
is a definite `true`, listed in `gpsInOtherMetadata`; JFIF, ICC, MPF, Adobe
|
|
182
|
+
and Ducky segments and an IPTC record have no place for coordinates.
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { renderDiagram } from "./lib/diagram";
|
|
|
33
33
|
import { parseProbeJson } from "./lib/ffprobe";
|
|
34
34
|
import { HEADER_FORMATS, detectKind, kindsForExtension, knownKinds, readImageHeader, } from "./lib/identify";
|
|
35
35
|
import { cropImage, diffImages, differenceHash, fitDimensions, hammingDistance, imageStats, resizeImage, } from "./lib/imageops";
|
|
36
|
-
import {
|
|
36
|
+
import { inspectJpegMetadata, stripJpegMetadata } from "./lib/jpeg";
|
|
37
37
|
import { DEFAULT_DECODE_LIMITS, decodePng, encodePng, } from "./lib/png";
|
|
38
38
|
import { ECC_LEVELS, MAX_VERSION, byteCapacity, encodeQr, symbolToText, } from "./lib/qr";
|
|
39
39
|
import { renderModuleGrid, renderModuleStrip } from "./lib/raster";
|
|
@@ -111,6 +111,9 @@ function readWhole(toolName, rel, maxBytes = MAX_FILE_BYTES) {
|
|
|
111
111
|
const stats = statSync(path.real);
|
|
112
112
|
if (stats.isDirectory())
|
|
113
113
|
throw new ToolInputError(`"${rel}" is a directory, not a file`);
|
|
114
|
+
// Opening a FIFO blocks until something writes to it; a device may never end.
|
|
115
|
+
if (!stats.isFile())
|
|
116
|
+
throw new ToolInputError(`"${rel}" is not a regular file`);
|
|
114
117
|
if (stats.size > maxBytes) {
|
|
115
118
|
throw new ToolInputError(`"${rel}" is ${stats.size} bytes, over the ${maxBytes}-byte limit for this tool`);
|
|
116
119
|
}
|
|
@@ -139,6 +142,8 @@ function readHead(toolName, rel, window = HEADER_WINDOW_BYTES) {
|
|
|
139
142
|
const stats = statSync(path.real);
|
|
140
143
|
if (stats.isDirectory())
|
|
141
144
|
throw new ToolInputError(`"${rel}" is a directory, not a file`);
|
|
145
|
+
if (!stats.isFile())
|
|
146
|
+
throw new ToolInputError(`"${rel}" is not a regular file`);
|
|
142
147
|
const want = Math.min(window, stats.size);
|
|
143
148
|
const fd = openSync(path.real, "r");
|
|
144
149
|
try {
|
|
@@ -321,40 +326,127 @@ export const pngRead = buildTool({
|
|
|
321
326
|
});
|
|
322
327
|
export const exifRead = buildTool({
|
|
323
328
|
name: "ExifRead",
|
|
324
|
-
description: "Read a JPEG's EXIF metadata: capture time, camera, lens, exposure, orientation, and GPS coordinates when the file carries them. Use before publishing or sharing a photograph, because `hasGps: true` means the file is carrying the location it was taken at.",
|
|
329
|
+
description: "Read a JPEG's EXIF metadata: capture time, camera, lens, exposure, orientation, and GPS coordinates when the file carries them. Use before publishing or sharing a photograph, because `hasGps: true` means the file is carrying the location it was taken at. The whole file is read (a file past 64 MiB up to that point), so EXIF between scans or in an image appended after the main one (a preview, a gain map) counts too, and so does GPS written in XMP; when it cannot tell, hasGps is null with the reason.",
|
|
325
330
|
inputSchema: z.object({ path: pathField.describe("a JPEG inside the workspace") }),
|
|
326
331
|
readOnly: true,
|
|
327
332
|
concurrencySafe: true,
|
|
328
333
|
execute: async (input) => attempt(() => {
|
|
329
|
-
|
|
334
|
+
// The whole file, not a head window: GPS can sit between scans or in
|
|
335
|
+
// an appended image, and "no GPS" must not be said about bytes that
|
|
336
|
+
// were never read. A file past the read limit is read as far as the
|
|
337
|
+
// limit and answered from that, with GPS undetermined unless found.
|
|
338
|
+
const { path, bytes, size } = readHead("ExifRead", input.path, MAX_FILE_BYTES);
|
|
339
|
+
const partial = bytes.length < size;
|
|
330
340
|
const kind = detectKind(bytes);
|
|
331
341
|
if (kind?.kind !== "jpeg") {
|
|
332
342
|
throw new ToolInputError(`"${input.path}" is ${kind === undefined ? "not a recognised format" : kind.kind}; EXIF reading here covers JPEG only`);
|
|
333
343
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
const report = inspectJpegMetadata(bytes);
|
|
345
|
+
// The main image's own EXIF: never a block found after it.
|
|
346
|
+
const primary = report.exifBlocks.find((b) => b.where === "before the first scan" || b.where === "between scans");
|
|
347
|
+
const withGps = report.exifBlocks.filter((b) => b.exif?.hasGps === true);
|
|
348
|
+
const unreadableBlocks = report.exifBlocks.filter((b) => b.exif === null);
|
|
349
|
+
// XMP and Photoshop resources are read for a location; a segment this
|
|
350
|
+
// reader does not parse (a C2PA manifest in APP11, a vendor APPn) may
|
|
351
|
+
// hold one, so it leaves "no GPS" undetermined, as an unparsed EXIF
|
|
352
|
+
// block does.
|
|
353
|
+
const locatedElsewhere = report.otherMetadata.filter((m) => m.location === true);
|
|
354
|
+
const notRead = report.otherMetadata.filter((m) => m.location === null);
|
|
355
|
+
// GPS found anywhere is a definite yes. No GPS is a definite no only
|
|
356
|
+
// when every byte was read, every EXIF block parsed, and every byte
|
|
357
|
+
// after the image accounted for by an appended image that was walked.
|
|
358
|
+
const unknowns = [];
|
|
359
|
+
if (partial) {
|
|
360
|
+
unknowns.push(`only the first ${bytes.length} of the file's ${size} bytes were read, the most this tool reads`);
|
|
361
|
+
}
|
|
362
|
+
if (report.breaksOffAt !== undefined) {
|
|
363
|
+
// A walk that broke off has no trailer: its unaccounted bytes are
|
|
364
|
+
// the part after the break.
|
|
365
|
+
unknowns.push(`the file breaks off at offset ${report.breaksOffAt.offset} (${report.breaksOffAt.reason}), and the ${bytes.length - report.breaksOffAt.offset} bytes from there could not be walked`);
|
|
366
|
+
}
|
|
367
|
+
else if (report.unaccountedBytes > 0) {
|
|
368
|
+
unknowns.push(`${report.unaccountedBytes} bytes after the image that no walk accounts for (${report.trailingKind ?? "unrecognised data"}) were searched for EXIF blocks but not parsed, and may carry a location`);
|
|
369
|
+
}
|
|
370
|
+
if (report.embeddedImagesNotInspected > 0) {
|
|
371
|
+
unknowns.push(`${report.embeddedImagesNotInspected} more appended images were not walked`);
|
|
372
|
+
}
|
|
373
|
+
if (unreadableBlocks.length > 0)
|
|
374
|
+
unknowns.push("an EXIF block in the file could not be parsed");
|
|
375
|
+
if (notRead.length > 0) {
|
|
376
|
+
const named = notRead
|
|
377
|
+
.slice(0, 8)
|
|
378
|
+
.map((m) => `${m.segment} at offset ${m.offset}`)
|
|
379
|
+
.join(", ");
|
|
380
|
+
const more = notRead.length > 8 ? ` and ${notRead.length - 8} more` : "";
|
|
381
|
+
unknowns.push(`metadata this reader does not parse may carry a location: ${named}${more}`);
|
|
382
|
+
}
|
|
383
|
+
const found = withGps.length > 0 || locatedElsewhere.length > 0;
|
|
384
|
+
const undetermined = !found && unknowns.length > 0;
|
|
385
|
+
const hasGps = found ? true : undetermined ? null : false;
|
|
386
|
+
const where = {
|
|
387
|
+
...(partial ? { bytesRead: bytes.length, fileBytes: size } : {}),
|
|
388
|
+
trailingBytes: report.trailingBytes,
|
|
389
|
+
...(report.trailingKind === undefined ? {} : { trailingKind: report.trailingKind }),
|
|
390
|
+
embeddedImages: report.embeddedImages,
|
|
391
|
+
interScanMetadataSegments: report.interScanMetadataSegments,
|
|
392
|
+
...(report.eoiMissing && !partial ? { eoiMissing: true } : {}),
|
|
393
|
+
...(report.breaksOffAt === undefined ? {} : { breaksOffAt: report.breaksOffAt.offset }),
|
|
394
|
+
...(report.unaccountedBytes > 0 ? { unaccountedBytes: report.unaccountedBytes } : {}),
|
|
395
|
+
...(report.exifBlocks.length > 1 ||
|
|
396
|
+
(report.exifBlocks.length === 1 && primary === undefined)
|
|
397
|
+
? {
|
|
398
|
+
exifBlocks: report.exifBlocks.map((b) => ({
|
|
399
|
+
where: b.where,
|
|
400
|
+
offset: b.offset,
|
|
401
|
+
hasGps: b.exif?.hasGps ?? null,
|
|
402
|
+
...(b.exif?.gps === undefined ? {} : { gps: b.exif.gps }),
|
|
403
|
+
...(b.unreadable === undefined ? {} : { unreadable: b.unreadable }),
|
|
404
|
+
})),
|
|
405
|
+
}
|
|
406
|
+
: {}),
|
|
407
|
+
...(locatedElsewhere.length > 0
|
|
408
|
+
? {
|
|
409
|
+
gpsInOtherMetadata: locatedElsewhere.map((m) => ({
|
|
410
|
+
segment: m.segment,
|
|
411
|
+
where: m.where,
|
|
412
|
+
offset: m.offset,
|
|
413
|
+
})),
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
416
|
+
...(undetermined ? { gpsUndetermined: unknowns.join("; ") } : {}),
|
|
417
|
+
...(report.trailingKind?.startsWith("an appended video") === true
|
|
418
|
+
? {
|
|
419
|
+
videoNote: "the appended video is not read here and may carry its own location; ExifStrip removes it",
|
|
420
|
+
}
|
|
421
|
+
: {}),
|
|
422
|
+
};
|
|
423
|
+
const privacy = hasGps === true
|
|
424
|
+
? {
|
|
425
|
+
privacyWarning: "this file records where it was taken; publishing it publishes that location. ExifStrip removes it.",
|
|
426
|
+
}
|
|
427
|
+
: {};
|
|
428
|
+
if (primary === undefined || primary.exif === null) {
|
|
337
429
|
return json({
|
|
338
430
|
path: path.rel,
|
|
339
|
-
hasExif:
|
|
340
|
-
hasGps
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
431
|
+
hasExif: report.exifBlocks.length > 0,
|
|
432
|
+
hasGps,
|
|
433
|
+
...(report.exifBlocks.length === 0
|
|
434
|
+
? {
|
|
435
|
+
note: "no APP1 EXIF segment; the file carries no EXIF metadata this reader can see",
|
|
436
|
+
}
|
|
437
|
+
: {}),
|
|
438
|
+
metadataSegments: report.metadataSegments,
|
|
439
|
+
...where,
|
|
440
|
+
...privacy,
|
|
346
441
|
});
|
|
347
442
|
}
|
|
348
|
-
const exif = parseExif(segment.payload);
|
|
349
443
|
return json({
|
|
350
444
|
path: path.rel,
|
|
351
445
|
hasExif: true,
|
|
352
|
-
...exif,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
: {}),
|
|
446
|
+
...primary.exif,
|
|
447
|
+
hasGps,
|
|
448
|
+
...where,
|
|
449
|
+
...privacy,
|
|
358
450
|
});
|
|
359
451
|
}),
|
|
360
452
|
});
|
|
@@ -417,6 +509,7 @@ const colorTypeField = z
|
|
|
417
509
|
.describe("`auto` (the default) picks the smallest type that is lossless for these pixels");
|
|
418
510
|
export const pngWrite = buildTool({
|
|
419
511
|
name: "PngWrite",
|
|
512
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
420
513
|
description: "Encode raw RGBA pixels as a valid PNG file, choosing the smallest lossless colour type by default. Use to write out an image a tool or a script has computed pixel by pixel, without adding an image library to the project.",
|
|
421
514
|
inputSchema: z.object({
|
|
422
515
|
path: pathField.describe("where to write the PNG, inside the workspace"),
|
|
@@ -456,6 +549,10 @@ export const pngWrite = buildTool({
|
|
|
456
549
|
});
|
|
457
550
|
export const imageResize = buildTool({
|
|
458
551
|
name: "ImageResize",
|
|
552
|
+
operativeArgs: [
|
|
553
|
+
{ field: "path", kind: "path", reads: true },
|
|
554
|
+
{ field: "output", kind: "path" },
|
|
555
|
+
],
|
|
459
556
|
description: "Resize a PNG to a new size and write it out as a PNG, stating the resampling method used. Use to produce a thumbnail or a fixed-width asset; pass only a width or only a height to keep the aspect ratio.",
|
|
460
557
|
inputSchema: z.object({
|
|
461
558
|
path: pathField.describe("the source PNG"),
|
|
@@ -503,6 +600,10 @@ export const imageResize = buildTool({
|
|
|
503
600
|
});
|
|
504
601
|
export const imageCrop = buildTool({
|
|
505
602
|
name: "ImageCrop",
|
|
603
|
+
operativeArgs: [
|
|
604
|
+
{ field: "path", kind: "path", reads: true },
|
|
605
|
+
{ field: "output", kind: "path" },
|
|
606
|
+
],
|
|
506
607
|
description: "Cut a rectangle out of a PNG and write it out as a PNG. Use to isolate a region of a screenshot — the part a diff flagged, or one panel of a wider capture — before comparing or reading it.",
|
|
507
608
|
inputSchema: z.object({
|
|
508
609
|
path: pathField.describe("the source PNG"),
|
|
@@ -533,7 +634,11 @@ export const imageCrop = buildTool({
|
|
|
533
634
|
});
|
|
534
635
|
export const exifStrip = buildTool({
|
|
535
636
|
name: "ExifStrip",
|
|
536
|
-
|
|
637
|
+
operativeArgs: [
|
|
638
|
+
{ field: "path", kind: "path", reads: true },
|
|
639
|
+
{ field: "output", kind: "path" },
|
|
640
|
+
],
|
|
641
|
+
description: "Write a copy of a JPEG with its metadata segments removed, keeping the image data byte for byte. Use before publishing a photograph, to drop the GPS coordinates, camera serial and capture time without re-encoding and losing quality. Metadata between scans is removed too, and so is anything appended after the image (a preview, gain map or motion-photo video, each of which can carry its own location).",
|
|
537
642
|
inputSchema: z.object({
|
|
538
643
|
path: pathField.describe("the source JPEG"),
|
|
539
644
|
output: pathField.describe("where to write the stripped JPEG"),
|
|
@@ -563,7 +668,17 @@ export const exifStrip = buildTool({
|
|
|
563
668
|
bytesAfter: result.bytes.length,
|
|
564
669
|
bytesRemoved: result.bytesRemoved,
|
|
565
670
|
removed: result.removed,
|
|
566
|
-
|
|
671
|
+
trailingBytesRemoved: result.trailingBytesRemoved,
|
|
672
|
+
...(result.trailingKind === undefined ? {} : { trailingKind: result.trailingKind }),
|
|
673
|
+
...(result.eoiMissing ? { eoiMissing: true } : {}),
|
|
674
|
+
...(result.breaksOffAt === undefined ? {} : { breaksOffAt: result.breaksOffAt.offset }),
|
|
675
|
+
note: result.breaksOffAt !== undefined
|
|
676
|
+
? `the file breaks off at offset ${result.breaksOffAt.offset} (${result.breaksOffAt.reason}); the scan data up to there was copied verbatim, and the cut-off part after it was ${result.removed.some((r) => r.name.endsWith("(cut off)")) ? "a metadata segment, and was dropped" : "kept as it was"}`
|
|
677
|
+
: result.trailingBytesRemoved > 0
|
|
678
|
+
? `the main image's scan data was copied verbatim, so that picture is unchanged; the ${result.trailingBytesRemoved} bytes after its end-of-image marker were dropped with any metadata in them: ${result.trailingKind}`
|
|
679
|
+
: result.eoiMissing
|
|
680
|
+
? "the scan data was copied verbatim; the file has no end-of-image marker, so it was walked to its end and kept as it was"
|
|
681
|
+
: "the entropy-coded scan data was copied verbatim; the picture is unchanged",
|
|
567
682
|
});
|
|
568
683
|
}),
|
|
569
684
|
});
|
|
@@ -571,6 +686,7 @@ export const exifStrip = buildTool({
|
|
|
571
686
|
// Codes
|
|
572
687
|
export const qrEncode = buildTool({
|
|
573
688
|
name: "QrEncode",
|
|
689
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
574
690
|
description: "Generate a QR code from text, as a PNG file or as a text matrix, with a chosen error-correction level. Use to turn a URL, a Wi-Fi string or a payload into a scannable code without a service call; versions 1 to 10 are supported, which is up to 271 bytes at level L.",
|
|
575
691
|
inputSchema: z.object({
|
|
576
692
|
text: z.string().min(1).describe("the payload; encoded as UTF-8 in byte mode"),
|
|
@@ -647,6 +763,7 @@ export const qrEncode = buildTool({
|
|
|
647
763
|
});
|
|
648
764
|
export const barcodeEncode = buildTool({
|
|
649
765
|
name: "BarcodeEncode",
|
|
766
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
650
767
|
description: "Generate a Code 128 or EAN-13 barcode, as a PNG file or as the raw module pattern, computing the check digit. Use for a label, a product code or an asset tag; EAN-13 accepts twelve digits and computes the thirteenth, or thirteen and verifies it.",
|
|
651
768
|
inputSchema: z.object({
|
|
652
769
|
symbology: z.enum(["code128", "ean13"]),
|
|
@@ -736,6 +853,7 @@ function emitSvg(toolName, svg, rel, overwrite, extra) {
|
|
|
736
853
|
}
|
|
737
854
|
export const chartRender = buildTool({
|
|
738
855
|
name: "ChartRender",
|
|
856
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
739
857
|
description: "Render bar, line, scatter or pie data as an SVG chart with axes, ticks, labels and a legend. Use to turn a query result or a metric series into a picture for a report; the layout is fixed arithmetic, so the same data always produces the same bytes.",
|
|
740
858
|
inputSchema: z.object({
|
|
741
859
|
type: z.enum(["bar", "line", "scatter", "pie"]),
|
|
@@ -833,6 +951,7 @@ export const sparklineRender = buildTool({
|
|
|
833
951
|
});
|
|
834
952
|
export const diagramRender = buildTool({
|
|
835
953
|
name: "DiagramRender",
|
|
954
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
836
955
|
description: "Render a node and edge list as a box-and-arrow SVG diagram with a layered layout. Use for a pipeline, a state machine or a service sketch; nodes are placed by layer and declaration order, so the same graph always draws the same, and edges that close a cycle are drawn dashed and reported.",
|
|
837
956
|
inputSchema: z.object({
|
|
838
957
|
nodes: z
|
|
@@ -993,6 +1112,7 @@ export const subtitleParse = buildTool({
|
|
|
993
1112
|
});
|
|
994
1113
|
export const subtitleWrite = buildTool({
|
|
995
1114
|
name: "SubtitleWrite",
|
|
1115
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
996
1116
|
description: "Write cues out as SRT or WebVTT, optionally shifting every time and re-wrapping the text. Use to fix a track that runs early or late, to convert between the two formats, or to re-wrap long lines to a readable width.",
|
|
997
1117
|
inputSchema: z.object({
|
|
998
1118
|
cues: z
|
|
@@ -1050,6 +1170,7 @@ export const subtitleWrite = buildTool({
|
|
|
1050
1170
|
});
|
|
1051
1171
|
export const mediaProbe = buildTool({
|
|
1052
1172
|
name: "MediaProbe",
|
|
1173
|
+
operativeArgs: [{ field: "path", kind: "path" }],
|
|
1053
1174
|
description: "Probe an audio or video file with ffprobe and return its duration, streams, codecs, resolution and bitrate as structured data. Use to learn what a media file actually contains before transcoding or embedding it; ffprobe must be installed, and the tool says so plainly when it is not.",
|
|
1054
1175
|
inputSchema: z.object({
|
|
1055
1176
|
path: pathField.describe("an audio or video file inside the workspace"),
|
|
@@ -1062,10 +1183,12 @@ export const mediaProbe = buildTool({
|
|
|
1062
1183
|
.describe(`milliseconds before ffprobe is killed; default ${DEFAULT_PROCESS_TIMEOUT_MS}`),
|
|
1063
1184
|
}),
|
|
1064
1185
|
// NOT `readOnly`. ffprobe only reads, but `readOnly` is what the
|
|
1065
|
-
// permission engine auto-allows in `plan` mode, and
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1186
|
+
// permission engine auto-allows in `plan` mode, and this tool keeps a plan
|
|
1187
|
+
// from spawning it. (Not every spawning tool makes that call: the git
|
|
1188
|
+
// readers and system probes are read-only because the program they run is
|
|
1189
|
+
// fixed by the tool, not supplied by the workspace — the rule, and the list
|
|
1190
|
+
// it allows, are in apps/cli/src/flag-rules.test.ts.) `destructive` stays
|
|
1191
|
+
// false, because nothing this runs can change a file.
|
|
1069
1192
|
// Spawning a process crosses a boundary the runtime cannot re-classify
|
|
1070
1193
|
// afterwards, so this declares both the policy and the capability.
|
|
1071
1194
|
scope: "external",
|
package/dist/lib/color.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type Rgb = {
|
|
|
4
4
|
readonly b: number;
|
|
5
5
|
readonly a: number;
|
|
6
6
|
};
|
|
7
|
+
export declare const RGB_FN: RegExp;
|
|
8
|
+
export declare const HSL_FN: RegExp;
|
|
7
9
|
/** Colour keywords this package accepts, sorted. */
|
|
8
10
|
export declare function colorKeywords(): ReadonlyArray<string>;
|
|
9
11
|
/**
|
package/dist/lib/color.js
CHANGED
|
@@ -11,8 +11,19 @@ const HEX3 = /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i;
|
|
|
11
11
|
const HEX4 = /^#?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])$/i;
|
|
12
12
|
const HEX6 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
13
13
|
const HEX8 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/*
|
|
15
|
+
* `rgb(…)` and `hsl(…)`, alpha forms included. The capture is the text
|
|
16
|
+
* between the parentheses without its leading whitespace, or its last
|
|
17
|
+
* character when it is all whitespace.
|
|
18
|
+
*
|
|
19
|
+
* These were `\(\s*([^)]+)\)$`, which captures the same. Because the capture
|
|
20
|
+
* could start with whitespace too, a failed match gave a run of whitespace
|
|
21
|
+
* back one character at a time and re-read the rest of the run from each:
|
|
22
|
+
* quadratic in the run, on a colour the model wrote. The capture now starts
|
|
23
|
+
* at a character that is not whitespace, or is a single whitespace character.
|
|
24
|
+
*/
|
|
25
|
+
export const RGB_FN = /^rgba?\(\s*([^)\s][^)]*|\s)\)$/i;
|
|
26
|
+
export const HSL_FN = /^hsla?\(\s*([^)\s][^)]*|\s)\)$/i;
|
|
16
27
|
/**
|
|
17
28
|
* The 16 colour keywords every CSS level has had, lowercase. The full
|
|
18
29
|
* 148-name list is deliberately not carried here: a caller who means
|
|
@@ -77,7 +88,10 @@ function alphaValue(text) {
|
|
|
77
88
|
*/
|
|
78
89
|
export function parseColor(input) {
|
|
79
90
|
const text = input.trim();
|
|
80
|
-
|
|
91
|
+
// Own entries only: `constructor` read Object, and the recursion below
|
|
92
|
+
// failed with "input.trim is not a function" instead of "not a colour".
|
|
93
|
+
const name = text.toLowerCase();
|
|
94
|
+
const keyword = Object.hasOwn(KEYWORDS, name) ? KEYWORDS[name] : undefined;
|
|
81
95
|
if (keyword !== undefined)
|
|
82
96
|
return parseColor(keyword);
|
|
83
97
|
const hex8 = HEX8.exec(text);
|