@utilix-tech/sdk 0.5.0 → 0.6.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 +10 -4
- package/dist/{chunk-WWKPLYEP.js → chunk-FXBB7O5A.js} +191 -1
- package/dist/{chunk-FL53T24A.js → chunk-GEFVMGZR.js} +2 -8
- package/dist/index.cjs +192 -8
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{media-D-K5aZnq.d.cts → media-CHaBS1dI.d.cts} +31 -2
- package/dist/{media-D-K5aZnq.d.ts → media-CHaBS1dI.d.ts} +31 -2
- package/dist/tools/hashing.cjs +2 -8
- package/dist/tools/hashing.js +1 -1
- package/dist/tools/media.cjs +190 -0
- package/dist/tools/media.d.cts +1 -1
- package/dist/tools/media.d.ts +1 -1
- package/dist/tools/media.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**524 developer utility functions for Node.js: runs locally, no API key required.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
6
6
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
@@ -416,10 +416,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
|
|
|
416
416
|
|
|
417
417
|
---
|
|
418
418
|
|
|
419
|
-
### `/media`: Image Header Parsing & PDF Metadata
|
|
419
|
+
### `/media`: Image Header Parsing, EXIF & PDF Metadata
|
|
420
420
|
|
|
421
421
|
```ts
|
|
422
|
-
import { readImageInfo, readPdfMetadata } from "@utilix-tech/sdk/media";
|
|
422
|
+
import { readImageInfo, readExifData, readPdfMetadata } from "@utilix-tech/sdk/media";
|
|
423
423
|
|
|
424
424
|
// Reads format, dimensions, bit depth, and alpha channel straight from
|
|
425
425
|
// file bytes: no decoding, no canvas, no image library.
|
|
@@ -427,6 +427,12 @@ const bytes = await fs.promises.readFile("photo.png");
|
|
|
427
427
|
readImageInfo(new Uint8Array(bytes));
|
|
428
428
|
// { format: "png", width: 1920, height: 1080, bitDepth: 8, colorType: "rgba", hasAlpha: true }
|
|
429
429
|
|
|
430
|
+
// Reads camera make/model, orientation, timestamps, exposure settings, and
|
|
431
|
+
// GPS coordinates directly from a JPEG's EXIF data. JPEG only.
|
|
432
|
+
const jpegBytes = await fs.promises.readFile("photo.jpg");
|
|
433
|
+
readExifData(new Uint8Array(jpegBytes));
|
|
434
|
+
// { make: "Canon", model: "EOS R5", orientation: 1, exposureTime: "1/125", fNumber: 2.8, ... }
|
|
435
|
+
|
|
430
436
|
// Reads title, author, dates, page count, and encryption flag from a PDF's
|
|
431
437
|
// trailer/Info dictionary: no pdf.js or other PDF library required.
|
|
432
438
|
const pdfBytes = await fs.promises.readFile("report.pdf");
|
|
@@ -434,7 +440,7 @@ readPdfMetadata(new Uint8Array(pdfBytes));
|
|
|
434
440
|
// { version: "1.7", pageCount: 12, title: "Q3 Report", author: "Jane Doe", ... }
|
|
435
441
|
```
|
|
436
442
|
|
|
437
|
-
|
|
443
|
+
`readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only.
|
|
438
444
|
|
|
439
445
|
---
|
|
440
446
|
|
|
@@ -3,6 +3,7 @@ import { __export } from './chunk-MLKGABMK.js';
|
|
|
3
3
|
// src/tools/media.ts
|
|
4
4
|
var media_exports = {};
|
|
5
5
|
__export(media_exports, {
|
|
6
|
+
readExifData: () => readExifData,
|
|
6
7
|
readImageInfo: () => readImageInfo,
|
|
7
8
|
readPdfMetadata: () => readPdfMetadata
|
|
8
9
|
});
|
|
@@ -131,6 +132,195 @@ function readImageInfo(bytes) {
|
|
|
131
132
|
}
|
|
132
133
|
return { error: "Unrecognized image format. Supported formats: PNG, JPEG, GIF, WebP, BMP" };
|
|
133
134
|
}
|
|
135
|
+
var ORIENTATION_LABELS = {
|
|
136
|
+
1: "Normal",
|
|
137
|
+
2: "Mirrored horizontal",
|
|
138
|
+
3: "Rotated 180\xB0",
|
|
139
|
+
4: "Mirrored vertical",
|
|
140
|
+
5: "Mirrored horizontal, rotated 90\xB0 CCW",
|
|
141
|
+
6: "Rotated 90\xB0 CW",
|
|
142
|
+
7: "Mirrored horizontal, rotated 90\xB0 CW",
|
|
143
|
+
8: "Rotated 90\xB0 CCW"
|
|
144
|
+
};
|
|
145
|
+
function tu16(r, o) {
|
|
146
|
+
return r.view.getUint16(o, r.little);
|
|
147
|
+
}
|
|
148
|
+
function tu32(r, o) {
|
|
149
|
+
return r.view.getUint32(o, r.little);
|
|
150
|
+
}
|
|
151
|
+
function readIfdEntries(r, ifdOffset) {
|
|
152
|
+
if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
|
|
153
|
+
const numEntries = tu16(r, ifdOffset);
|
|
154
|
+
const entries = [];
|
|
155
|
+
for (let i = 0; i < numEntries; i++) {
|
|
156
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
157
|
+
if (entryOffset + 12 > r.bytes.length) break;
|
|
158
|
+
entries.push({
|
|
159
|
+
tag: tu16(r, entryOffset),
|
|
160
|
+
type: tu16(r, entryOffset + 2),
|
|
161
|
+
count: tu32(r, entryOffset + 4),
|
|
162
|
+
valueOffsetField: entryOffset + 8
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return { entries };
|
|
166
|
+
}
|
|
167
|
+
function findEntry(entries, tag) {
|
|
168
|
+
return entries.find((e) => e.tag === tag);
|
|
169
|
+
}
|
|
170
|
+
function readAscii(r, entry) {
|
|
171
|
+
const size = entry.count;
|
|
172
|
+
const inline = size <= 4;
|
|
173
|
+
const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
174
|
+
if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
|
|
175
|
+
let out = "";
|
|
176
|
+
for (let i = 0; i < size; i++) {
|
|
177
|
+
const c = r.bytes[dataOffset + i];
|
|
178
|
+
if (c === 0) break;
|
|
179
|
+
out += String.fromCharCode(c);
|
|
180
|
+
}
|
|
181
|
+
const trimmed = out.trim();
|
|
182
|
+
return trimmed || null;
|
|
183
|
+
}
|
|
184
|
+
function readRational(r, entry) {
|
|
185
|
+
const dataOffset = r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
186
|
+
if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
|
|
187
|
+
return { num: tu32(r, dataOffset), den: tu32(r, dataOffset + 4) };
|
|
188
|
+
}
|
|
189
|
+
function readGpsCoordinate(r, entries, valueTag, refTag) {
|
|
190
|
+
const valueEntry = findEntry(entries, valueTag);
|
|
191
|
+
if (!valueEntry) return null;
|
|
192
|
+
const dataOffset = r.tiffStart + tu32(r, valueEntry.valueOffsetField);
|
|
193
|
+
if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
|
|
194
|
+
const deg = tu32(r, dataOffset) / (tu32(r, dataOffset + 4) || 1);
|
|
195
|
+
const min = tu32(r, dataOffset + 8) / (tu32(r, dataOffset + 12) || 1);
|
|
196
|
+
const sec = tu32(r, dataOffset + 16) / (tu32(r, dataOffset + 20) || 1);
|
|
197
|
+
let decimal = deg + min / 60 + sec / 3600;
|
|
198
|
+
const refEntry = findEntry(entries, refTag);
|
|
199
|
+
const ref = refEntry ? readAscii(r, refEntry) : null;
|
|
200
|
+
if (ref === "S" || ref === "W") decimal = -decimal;
|
|
201
|
+
return Math.round(decimal * 1e6) / 1e6;
|
|
202
|
+
}
|
|
203
|
+
function gcd(a, b) {
|
|
204
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
205
|
+
}
|
|
206
|
+
function round2(n) {
|
|
207
|
+
return Math.round(n * 100) / 100;
|
|
208
|
+
}
|
|
209
|
+
function formatExposureTime(num, den) {
|
|
210
|
+
if (num === 0) return "0s";
|
|
211
|
+
if (num >= den) return `${round2(num / den)}s`;
|
|
212
|
+
const divisor = gcd(num, den) || 1;
|
|
213
|
+
const n = num / divisor;
|
|
214
|
+
const d = den / divisor;
|
|
215
|
+
return `1/${Math.round(d / n)}`;
|
|
216
|
+
}
|
|
217
|
+
function readExifData(bytes) {
|
|
218
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
219
|
+
return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
|
|
220
|
+
}
|
|
221
|
+
let offset = 2;
|
|
222
|
+
let exifStart = -1;
|
|
223
|
+
while (offset + 4 <= bytes.length) {
|
|
224
|
+
if (bytes[offset] !== 255) {
|
|
225
|
+
offset++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const marker = bytes[offset + 1];
|
|
229
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
230
|
+
offset += 2;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (marker === 217 || marker === 218) break;
|
|
234
|
+
const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
235
|
+
if (marker === 225) {
|
|
236
|
+
const idOffset = offset + 4;
|
|
237
|
+
if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
|
|
238
|
+
exifStart = idOffset + 6;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
offset += 2 + segmentLength;
|
|
243
|
+
}
|
|
244
|
+
if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
|
|
245
|
+
if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
|
|
246
|
+
const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
|
|
247
|
+
if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
|
|
248
|
+
const little = byteOrder === "II";
|
|
249
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
250
|
+
const r = { bytes, view, little, tiffStart: exifStart };
|
|
251
|
+
if (tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
|
|
252
|
+
const ifd0Offset = exifStart + tu32(r, exifStart + 4);
|
|
253
|
+
const ifd0 = readIfdEntries(r, ifd0Offset);
|
|
254
|
+
if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
|
|
255
|
+
const result = { tagCount: ifd0.entries.length };
|
|
256
|
+
const makeEntry = findEntry(ifd0.entries, 271);
|
|
257
|
+
if (makeEntry) {
|
|
258
|
+
const v = readAscii(r, makeEntry);
|
|
259
|
+
if (v) result.make = v;
|
|
260
|
+
}
|
|
261
|
+
const modelEntry = findEntry(ifd0.entries, 272);
|
|
262
|
+
if (modelEntry) {
|
|
263
|
+
const v = readAscii(r, modelEntry);
|
|
264
|
+
if (v) result.model = v;
|
|
265
|
+
}
|
|
266
|
+
const softwareEntry = findEntry(ifd0.entries, 305);
|
|
267
|
+
if (softwareEntry) {
|
|
268
|
+
const v = readAscii(r, softwareEntry);
|
|
269
|
+
if (v) result.software = v;
|
|
270
|
+
}
|
|
271
|
+
const dateTimeEntry = findEntry(ifd0.entries, 306);
|
|
272
|
+
if (dateTimeEntry) {
|
|
273
|
+
const v = readAscii(r, dateTimeEntry);
|
|
274
|
+
if (v) result.dateTime = v;
|
|
275
|
+
}
|
|
276
|
+
const orientEntry = findEntry(ifd0.entries, 274);
|
|
277
|
+
if (orientEntry) {
|
|
278
|
+
const v = tu16(r, orientEntry.valueOffsetField);
|
|
279
|
+
result.orientation = v;
|
|
280
|
+
result.orientationLabel = ORIENTATION_LABELS[v] ?? "Unknown";
|
|
281
|
+
}
|
|
282
|
+
const exifIfdPtr = findEntry(ifd0.entries, 34665);
|
|
283
|
+
if (exifIfdPtr) {
|
|
284
|
+
const subOffset = exifStart + tu32(r, exifIfdPtr.valueOffsetField);
|
|
285
|
+
const sub = readIfdEntries(r, subOffset);
|
|
286
|
+
if (sub) {
|
|
287
|
+
const dtoEntry = findEntry(sub.entries, 36867);
|
|
288
|
+
if (dtoEntry) {
|
|
289
|
+
const v = readAscii(r, dtoEntry);
|
|
290
|
+
if (v) result.dateTimeOriginal = v;
|
|
291
|
+
}
|
|
292
|
+
const expEntry = findEntry(sub.entries, 33434);
|
|
293
|
+
if (expEntry) {
|
|
294
|
+
const rat = readRational(r, expEntry);
|
|
295
|
+
if (rat && rat.den !== 0) result.exposureTime = formatExposureTime(rat.num, rat.den);
|
|
296
|
+
}
|
|
297
|
+
const fEntry = findEntry(sub.entries, 33437);
|
|
298
|
+
if (fEntry) {
|
|
299
|
+
const rat = readRational(r, fEntry);
|
|
300
|
+
if (rat && rat.den !== 0) result.fNumber = round2(rat.num / rat.den);
|
|
301
|
+
}
|
|
302
|
+
const isoEntry = findEntry(sub.entries, 34855);
|
|
303
|
+
if (isoEntry) result.isoSpeed = tu16(r, isoEntry.valueOffsetField);
|
|
304
|
+
const focalEntry = findEntry(sub.entries, 37386);
|
|
305
|
+
if (focalEntry) {
|
|
306
|
+
const rat = readRational(r, focalEntry);
|
|
307
|
+
if (rat && rat.den !== 0) result.focalLength = round2(rat.num / rat.den);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const gpsIfdPtr = findEntry(ifd0.entries, 34853);
|
|
312
|
+
if (gpsIfdPtr) {
|
|
313
|
+
const gpsOffset = exifStart + tu32(r, gpsIfdPtr.valueOffsetField);
|
|
314
|
+
const gps = readIfdEntries(r, gpsOffset);
|
|
315
|
+
if (gps) {
|
|
316
|
+
const lat = readGpsCoordinate(r, gps.entries, 2, 1);
|
|
317
|
+
const lon = readGpsCoordinate(r, gps.entries, 4, 3);
|
|
318
|
+
if (lat !== null) result.gpsLatitude = lat;
|
|
319
|
+
if (lon !== null) result.gpsLongitude = lon;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
134
324
|
function bytesToLatin1(bytes) {
|
|
135
325
|
let out = "";
|
|
136
326
|
const CHUNK = 32768;
|
|
@@ -274,4 +464,4 @@ function readPdfMetadata(bytes) {
|
|
|
274
464
|
};
|
|
275
465
|
}
|
|
276
466
|
|
|
277
|
-
export { media_exports, readImageInfo, readPdfMetadata };
|
|
467
|
+
export { media_exports, readExifData, readImageInfo, readPdfMetadata };
|
|
@@ -189,13 +189,7 @@ function generateMd5Hash(password) {
|
|
|
189
189
|
return "$apr1$" + md5(password);
|
|
190
190
|
}
|
|
191
191
|
function generateBcryptHash(password, rounds = 10) {
|
|
192
|
-
|
|
193
|
-
let salt = "";
|
|
194
|
-
for (let i = 0; i < 22; i++) salt += chars[Math.floor(Math.random() * chars.length)];
|
|
195
|
-
const roundStr = rounds.toString().padStart(2, "0");
|
|
196
|
-
let hash = "";
|
|
197
|
-
for (let i = 0; i < 31; i++) hash += chars[Math.floor(Math.random() * chars.length)];
|
|
198
|
-
return `$2b$${roundStr}$${salt}${hash}`;
|
|
192
|
+
return bcrypt.hashSync(password, rounds);
|
|
199
193
|
}
|
|
200
194
|
function generatePlain(password) {
|
|
201
195
|
return password;
|
|
@@ -204,7 +198,7 @@ function createEntry(username, password, algorithm = "bcrypt") {
|
|
|
204
198
|
let hashedPassword;
|
|
205
199
|
switch (algorithm) {
|
|
206
200
|
case "bcrypt":
|
|
207
|
-
hashedPassword = generateBcryptHash();
|
|
201
|
+
hashedPassword = generateBcryptHash(password);
|
|
208
202
|
break;
|
|
209
203
|
case "sha1":
|
|
210
204
|
hashedPassword = generateSha1Hash(password);
|
package/dist/index.cjs
CHANGED
|
@@ -1736,13 +1736,7 @@ function generateMd5Hash(password) {
|
|
|
1736
1736
|
return "$apr1$" + md5__default.default(password);
|
|
1737
1737
|
}
|
|
1738
1738
|
function generateBcryptHash(password, rounds = 10) {
|
|
1739
|
-
|
|
1740
|
-
let salt = "";
|
|
1741
|
-
for (let i = 0; i < 22; i++) salt += chars[Math.floor(Math.random() * chars.length)];
|
|
1742
|
-
const roundStr = rounds.toString().padStart(2, "0");
|
|
1743
|
-
let hash = "";
|
|
1744
|
-
for (let i = 0; i < 31; i++) hash += chars[Math.floor(Math.random() * chars.length)];
|
|
1745
|
-
return `$2b$${roundStr}$${salt}${hash}`;
|
|
1739
|
+
return bcrypt__default.default.hashSync(password, rounds);
|
|
1746
1740
|
}
|
|
1747
1741
|
function generatePlain(password) {
|
|
1748
1742
|
return password;
|
|
@@ -1751,7 +1745,7 @@ function createEntry(username, password, algorithm = "bcrypt") {
|
|
|
1751
1745
|
let hashedPassword;
|
|
1752
1746
|
switch (algorithm) {
|
|
1753
1747
|
case "bcrypt":
|
|
1754
|
-
hashedPassword = generateBcryptHash();
|
|
1748
|
+
hashedPassword = generateBcryptHash(password);
|
|
1755
1749
|
break;
|
|
1756
1750
|
case "sha1":
|
|
1757
1751
|
hashedPassword = generateSha1Hash(password);
|
|
@@ -16934,6 +16928,7 @@ function summarizeForLlm(text, maxTokens = 200, strategy = "extractive") {
|
|
|
16934
16928
|
// src/tools/media.ts
|
|
16935
16929
|
var media_exports = {};
|
|
16936
16930
|
__export(media_exports, {
|
|
16931
|
+
readExifData: () => readExifData,
|
|
16937
16932
|
readImageInfo: () => readImageInfo,
|
|
16938
16933
|
readPdfMetadata: () => readPdfMetadata
|
|
16939
16934
|
});
|
|
@@ -17062,6 +17057,195 @@ function readImageInfo(bytes) {
|
|
|
17062
17057
|
}
|
|
17063
17058
|
return { error: "Unrecognized image format. Supported formats: PNG, JPEG, GIF, WebP, BMP" };
|
|
17064
17059
|
}
|
|
17060
|
+
var ORIENTATION_LABELS = {
|
|
17061
|
+
1: "Normal",
|
|
17062
|
+
2: "Mirrored horizontal",
|
|
17063
|
+
3: "Rotated 180\xB0",
|
|
17064
|
+
4: "Mirrored vertical",
|
|
17065
|
+
5: "Mirrored horizontal, rotated 90\xB0 CCW",
|
|
17066
|
+
6: "Rotated 90\xB0 CW",
|
|
17067
|
+
7: "Mirrored horizontal, rotated 90\xB0 CW",
|
|
17068
|
+
8: "Rotated 90\xB0 CCW"
|
|
17069
|
+
};
|
|
17070
|
+
function tu16(r, o) {
|
|
17071
|
+
return r.view.getUint16(o, r.little);
|
|
17072
|
+
}
|
|
17073
|
+
function tu32(r, o) {
|
|
17074
|
+
return r.view.getUint32(o, r.little);
|
|
17075
|
+
}
|
|
17076
|
+
function readIfdEntries(r, ifdOffset) {
|
|
17077
|
+
if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
|
|
17078
|
+
const numEntries = tu16(r, ifdOffset);
|
|
17079
|
+
const entries = [];
|
|
17080
|
+
for (let i = 0; i < numEntries; i++) {
|
|
17081
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
17082
|
+
if (entryOffset + 12 > r.bytes.length) break;
|
|
17083
|
+
entries.push({
|
|
17084
|
+
tag: tu16(r, entryOffset),
|
|
17085
|
+
type: tu16(r, entryOffset + 2),
|
|
17086
|
+
count: tu32(r, entryOffset + 4),
|
|
17087
|
+
valueOffsetField: entryOffset + 8
|
|
17088
|
+
});
|
|
17089
|
+
}
|
|
17090
|
+
return { entries };
|
|
17091
|
+
}
|
|
17092
|
+
function findEntry(entries, tag) {
|
|
17093
|
+
return entries.find((e) => e.tag === tag);
|
|
17094
|
+
}
|
|
17095
|
+
function readAscii(r, entry) {
|
|
17096
|
+
const size = entry.count;
|
|
17097
|
+
const inline = size <= 4;
|
|
17098
|
+
const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
17099
|
+
if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
|
|
17100
|
+
let out = "";
|
|
17101
|
+
for (let i = 0; i < size; i++) {
|
|
17102
|
+
const c = r.bytes[dataOffset + i];
|
|
17103
|
+
if (c === 0) break;
|
|
17104
|
+
out += String.fromCharCode(c);
|
|
17105
|
+
}
|
|
17106
|
+
const trimmed = out.trim();
|
|
17107
|
+
return trimmed || null;
|
|
17108
|
+
}
|
|
17109
|
+
function readRational(r, entry) {
|
|
17110
|
+
const dataOffset = r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
17111
|
+
if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
|
|
17112
|
+
return { num: tu32(r, dataOffset), den: tu32(r, dataOffset + 4) };
|
|
17113
|
+
}
|
|
17114
|
+
function readGpsCoordinate(r, entries, valueTag, refTag) {
|
|
17115
|
+
const valueEntry = findEntry(entries, valueTag);
|
|
17116
|
+
if (!valueEntry) return null;
|
|
17117
|
+
const dataOffset = r.tiffStart + tu32(r, valueEntry.valueOffsetField);
|
|
17118
|
+
if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
|
|
17119
|
+
const deg = tu32(r, dataOffset) / (tu32(r, dataOffset + 4) || 1);
|
|
17120
|
+
const min = tu32(r, dataOffset + 8) / (tu32(r, dataOffset + 12) || 1);
|
|
17121
|
+
const sec = tu32(r, dataOffset + 16) / (tu32(r, dataOffset + 20) || 1);
|
|
17122
|
+
let decimal = deg + min / 60 + sec / 3600;
|
|
17123
|
+
const refEntry = findEntry(entries, refTag);
|
|
17124
|
+
const ref = refEntry ? readAscii(r, refEntry) : null;
|
|
17125
|
+
if (ref === "S" || ref === "W") decimal = -decimal;
|
|
17126
|
+
return Math.round(decimal * 1e6) / 1e6;
|
|
17127
|
+
}
|
|
17128
|
+
function gcd2(a, b) {
|
|
17129
|
+
return b === 0 ? a : gcd2(b, a % b);
|
|
17130
|
+
}
|
|
17131
|
+
function round2(n) {
|
|
17132
|
+
return Math.round(n * 100) / 100;
|
|
17133
|
+
}
|
|
17134
|
+
function formatExposureTime(num, den) {
|
|
17135
|
+
if (num === 0) return "0s";
|
|
17136
|
+
if (num >= den) return `${round2(num / den)}s`;
|
|
17137
|
+
const divisor = gcd2(num, den) || 1;
|
|
17138
|
+
const n = num / divisor;
|
|
17139
|
+
const d = den / divisor;
|
|
17140
|
+
return `1/${Math.round(d / n)}`;
|
|
17141
|
+
}
|
|
17142
|
+
function readExifData(bytes) {
|
|
17143
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
17144
|
+
return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
|
|
17145
|
+
}
|
|
17146
|
+
let offset = 2;
|
|
17147
|
+
let exifStart = -1;
|
|
17148
|
+
while (offset + 4 <= bytes.length) {
|
|
17149
|
+
if (bytes[offset] !== 255) {
|
|
17150
|
+
offset++;
|
|
17151
|
+
continue;
|
|
17152
|
+
}
|
|
17153
|
+
const marker = bytes[offset + 1];
|
|
17154
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
17155
|
+
offset += 2;
|
|
17156
|
+
continue;
|
|
17157
|
+
}
|
|
17158
|
+
if (marker === 217 || marker === 218) break;
|
|
17159
|
+
const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
17160
|
+
if (marker === 225) {
|
|
17161
|
+
const idOffset = offset + 4;
|
|
17162
|
+
if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
|
|
17163
|
+
exifStart = idOffset + 6;
|
|
17164
|
+
break;
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
offset += 2 + segmentLength;
|
|
17168
|
+
}
|
|
17169
|
+
if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
|
|
17170
|
+
if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
|
|
17171
|
+
const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
|
|
17172
|
+
if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
|
|
17173
|
+
const little = byteOrder === "II";
|
|
17174
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
17175
|
+
const r = { bytes, view, little, tiffStart: exifStart };
|
|
17176
|
+
if (tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
|
|
17177
|
+
const ifd0Offset = exifStart + tu32(r, exifStart + 4);
|
|
17178
|
+
const ifd0 = readIfdEntries(r, ifd0Offset);
|
|
17179
|
+
if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
|
|
17180
|
+
const result = { tagCount: ifd0.entries.length };
|
|
17181
|
+
const makeEntry = findEntry(ifd0.entries, 271);
|
|
17182
|
+
if (makeEntry) {
|
|
17183
|
+
const v = readAscii(r, makeEntry);
|
|
17184
|
+
if (v) result.make = v;
|
|
17185
|
+
}
|
|
17186
|
+
const modelEntry = findEntry(ifd0.entries, 272);
|
|
17187
|
+
if (modelEntry) {
|
|
17188
|
+
const v = readAscii(r, modelEntry);
|
|
17189
|
+
if (v) result.model = v;
|
|
17190
|
+
}
|
|
17191
|
+
const softwareEntry = findEntry(ifd0.entries, 305);
|
|
17192
|
+
if (softwareEntry) {
|
|
17193
|
+
const v = readAscii(r, softwareEntry);
|
|
17194
|
+
if (v) result.software = v;
|
|
17195
|
+
}
|
|
17196
|
+
const dateTimeEntry = findEntry(ifd0.entries, 306);
|
|
17197
|
+
if (dateTimeEntry) {
|
|
17198
|
+
const v = readAscii(r, dateTimeEntry);
|
|
17199
|
+
if (v) result.dateTime = v;
|
|
17200
|
+
}
|
|
17201
|
+
const orientEntry = findEntry(ifd0.entries, 274);
|
|
17202
|
+
if (orientEntry) {
|
|
17203
|
+
const v = tu16(r, orientEntry.valueOffsetField);
|
|
17204
|
+
result.orientation = v;
|
|
17205
|
+
result.orientationLabel = ORIENTATION_LABELS[v] ?? "Unknown";
|
|
17206
|
+
}
|
|
17207
|
+
const exifIfdPtr = findEntry(ifd0.entries, 34665);
|
|
17208
|
+
if (exifIfdPtr) {
|
|
17209
|
+
const subOffset = exifStart + tu32(r, exifIfdPtr.valueOffsetField);
|
|
17210
|
+
const sub = readIfdEntries(r, subOffset);
|
|
17211
|
+
if (sub) {
|
|
17212
|
+
const dtoEntry = findEntry(sub.entries, 36867);
|
|
17213
|
+
if (dtoEntry) {
|
|
17214
|
+
const v = readAscii(r, dtoEntry);
|
|
17215
|
+
if (v) result.dateTimeOriginal = v;
|
|
17216
|
+
}
|
|
17217
|
+
const expEntry = findEntry(sub.entries, 33434);
|
|
17218
|
+
if (expEntry) {
|
|
17219
|
+
const rat = readRational(r, expEntry);
|
|
17220
|
+
if (rat && rat.den !== 0) result.exposureTime = formatExposureTime(rat.num, rat.den);
|
|
17221
|
+
}
|
|
17222
|
+
const fEntry = findEntry(sub.entries, 33437);
|
|
17223
|
+
if (fEntry) {
|
|
17224
|
+
const rat = readRational(r, fEntry);
|
|
17225
|
+
if (rat && rat.den !== 0) result.fNumber = round2(rat.num / rat.den);
|
|
17226
|
+
}
|
|
17227
|
+
const isoEntry = findEntry(sub.entries, 34855);
|
|
17228
|
+
if (isoEntry) result.isoSpeed = tu16(r, isoEntry.valueOffsetField);
|
|
17229
|
+
const focalEntry = findEntry(sub.entries, 37386);
|
|
17230
|
+
if (focalEntry) {
|
|
17231
|
+
const rat = readRational(r, focalEntry);
|
|
17232
|
+
if (rat && rat.den !== 0) result.focalLength = round2(rat.num / rat.den);
|
|
17233
|
+
}
|
|
17234
|
+
}
|
|
17235
|
+
}
|
|
17236
|
+
const gpsIfdPtr = findEntry(ifd0.entries, 34853);
|
|
17237
|
+
if (gpsIfdPtr) {
|
|
17238
|
+
const gpsOffset = exifStart + tu32(r, gpsIfdPtr.valueOffsetField);
|
|
17239
|
+
const gps = readIfdEntries(r, gpsOffset);
|
|
17240
|
+
if (gps) {
|
|
17241
|
+
const lat = readGpsCoordinate(r, gps.entries, 2, 1);
|
|
17242
|
+
const lon = readGpsCoordinate(r, gps.entries, 4, 3);
|
|
17243
|
+
if (lat !== null) result.gpsLatitude = lat;
|
|
17244
|
+
if (lon !== null) result.gpsLongitude = lon;
|
|
17245
|
+
}
|
|
17246
|
+
}
|
|
17247
|
+
return result;
|
|
17248
|
+
}
|
|
17065
17249
|
function bytesToLatin1(bytes) {
|
|
17066
17250
|
let out = "";
|
|
17067
17251
|
const CHUNK = 32768;
|
package/dist/index.d.cts
CHANGED
|
@@ -13,7 +13,7 @@ export { c as color } from './color-tPwZCr9H.cjs';
|
|
|
13
13
|
export { c as css } from './css-Cf7AMGM-.cjs';
|
|
14
14
|
export { m as misc } from './misc-CA3N198T.cjs';
|
|
15
15
|
export { a as ai_agent } from './ai_agent-CcpkV2DR.cjs';
|
|
16
|
-
export { m as media } from './media-
|
|
16
|
+
export { m as media } from './media-CHaBS1dI.cjs';
|
|
17
17
|
|
|
18
18
|
declare const version = "0.1.0";
|
|
19
19
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { c as color } from './color-tPwZCr9H.js';
|
|
|
13
13
|
export { c as css } from './css-Cf7AMGM-.js';
|
|
14
14
|
export { m as misc } from './misc-CA3N198T.js';
|
|
15
15
|
export { a as ai_agent } from './ai_agent-CcpkV2DR.js';
|
|
16
|
-
export { m as media } from './media-
|
|
16
|
+
export { m as media } from './media-CHaBS1dI.js';
|
|
17
17
|
|
|
18
18
|
declare const version = "0.1.0";
|
|
19
19
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { media_exports as media } from './chunk-
|
|
1
|
+
export { media_exports as media } from './chunk-FXBB7O5A.js';
|
|
2
2
|
export { units_exports as units } from './chunk-QJE743LY.js';
|
|
3
3
|
export { network_exports as network } from './chunk-V5JKVDBA.js';
|
|
4
4
|
export { api_exports as api } from './chunk-Q3B3YWOA.js';
|
|
@@ -9,7 +9,7 @@ export { misc_exports as misc } from './chunk-GX7H6TAX.js';
|
|
|
9
9
|
export { ai_agent_exports as ai_agent } from './chunk-M35VJETW.js';
|
|
10
10
|
export { json_exports as json } from './chunk-TSAGO3XP.js';
|
|
11
11
|
export { encoding_exports as encoding } from './chunk-IPR7FSX4.js';
|
|
12
|
-
export { hashing_exports as hashing } from './chunk-
|
|
12
|
+
export { hashing_exports as hashing } from './chunk-GEFVMGZR.js';
|
|
13
13
|
export { text_exports as text } from './chunk-YBBYFAOV.js';
|
|
14
14
|
export { data_exports as data } from './chunk-DNCOIO5N.js';
|
|
15
15
|
export { generators_exports as generators } from './chunk-2CJSLYWI.js';
|
|
@@ -16,6 +16,32 @@ interface ImageInfoError {
|
|
|
16
16
|
* GIF, WebP, and BMP.
|
|
17
17
|
*/
|
|
18
18
|
declare function readImageInfo(bytes: Uint8Array): ImageInfo | ImageInfoError;
|
|
19
|
+
interface ExifData {
|
|
20
|
+
make?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
software?: string;
|
|
23
|
+
orientation?: number;
|
|
24
|
+
orientationLabel?: string;
|
|
25
|
+
dateTime?: string;
|
|
26
|
+
dateTimeOriginal?: string;
|
|
27
|
+
exposureTime?: string;
|
|
28
|
+
fNumber?: number;
|
|
29
|
+
isoSpeed?: number;
|
|
30
|
+
focalLength?: number;
|
|
31
|
+
gpsLatitude?: number;
|
|
32
|
+
gpsLongitude?: number;
|
|
33
|
+
tagCount: number;
|
|
34
|
+
}
|
|
35
|
+
interface ExifError {
|
|
36
|
+
error: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read common EXIF tags (make, model, orientation, timestamps, exposure
|
|
40
|
+
* settings, GPS coordinates) directly from a JPEG's APP1 segment. Only
|
|
41
|
+
* JPEG is supported: PNG, WebP, and other formats don't carry EXIF the
|
|
42
|
+
* same way and are rejected with an error.
|
|
43
|
+
*/
|
|
44
|
+
declare function readExifData(bytes: Uint8Array): ExifData | ExifError;
|
|
19
45
|
interface PdfMetadata {
|
|
20
46
|
version: string | null;
|
|
21
47
|
pageCount: number | null;
|
|
@@ -39,15 +65,18 @@ interface PdfMetadataError {
|
|
|
39
65
|
*/
|
|
40
66
|
declare function readPdfMetadata(bytes: Uint8Array): PdfMetadata | PdfMetadataError;
|
|
41
67
|
|
|
68
|
+
type media_ExifData = ExifData;
|
|
69
|
+
type media_ExifError = ExifError;
|
|
42
70
|
type media_ImageFormat = ImageFormat;
|
|
43
71
|
type media_ImageInfo = ImageInfo;
|
|
44
72
|
type media_ImageInfoError = ImageInfoError;
|
|
45
73
|
type media_PdfMetadata = PdfMetadata;
|
|
46
74
|
type media_PdfMetadataError = PdfMetadataError;
|
|
75
|
+
declare const media_readExifData: typeof readExifData;
|
|
47
76
|
declare const media_readImageInfo: typeof readImageInfo;
|
|
48
77
|
declare const media_readPdfMetadata: typeof readPdfMetadata;
|
|
49
78
|
declare namespace media {
|
|
50
|
-
export { type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, media_readImageInfo as readImageInfo, media_readPdfMetadata as readPdfMetadata };
|
|
79
|
+
export { type media_ExifData as ExifData, type media_ExifError as ExifError, type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, media_readExifData as readExifData, media_readImageInfo as readImageInfo, media_readPdfMetadata as readPdfMetadata };
|
|
51
80
|
}
|
|
52
81
|
|
|
53
|
-
export { type ImageFormat as I, type PdfMetadata as P, type
|
|
82
|
+
export { type ExifData as E, type ImageFormat as I, type PdfMetadata as P, type ExifError as a, type ImageInfo as b, type ImageInfoError as c, type PdfMetadataError as d, readImageInfo as e, readPdfMetadata as f, media as m, readExifData as r };
|
|
@@ -16,6 +16,32 @@ interface ImageInfoError {
|
|
|
16
16
|
* GIF, WebP, and BMP.
|
|
17
17
|
*/
|
|
18
18
|
declare function readImageInfo(bytes: Uint8Array): ImageInfo | ImageInfoError;
|
|
19
|
+
interface ExifData {
|
|
20
|
+
make?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
software?: string;
|
|
23
|
+
orientation?: number;
|
|
24
|
+
orientationLabel?: string;
|
|
25
|
+
dateTime?: string;
|
|
26
|
+
dateTimeOriginal?: string;
|
|
27
|
+
exposureTime?: string;
|
|
28
|
+
fNumber?: number;
|
|
29
|
+
isoSpeed?: number;
|
|
30
|
+
focalLength?: number;
|
|
31
|
+
gpsLatitude?: number;
|
|
32
|
+
gpsLongitude?: number;
|
|
33
|
+
tagCount: number;
|
|
34
|
+
}
|
|
35
|
+
interface ExifError {
|
|
36
|
+
error: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read common EXIF tags (make, model, orientation, timestamps, exposure
|
|
40
|
+
* settings, GPS coordinates) directly from a JPEG's APP1 segment. Only
|
|
41
|
+
* JPEG is supported: PNG, WebP, and other formats don't carry EXIF the
|
|
42
|
+
* same way and are rejected with an error.
|
|
43
|
+
*/
|
|
44
|
+
declare function readExifData(bytes: Uint8Array): ExifData | ExifError;
|
|
19
45
|
interface PdfMetadata {
|
|
20
46
|
version: string | null;
|
|
21
47
|
pageCount: number | null;
|
|
@@ -39,15 +65,18 @@ interface PdfMetadataError {
|
|
|
39
65
|
*/
|
|
40
66
|
declare function readPdfMetadata(bytes: Uint8Array): PdfMetadata | PdfMetadataError;
|
|
41
67
|
|
|
68
|
+
type media_ExifData = ExifData;
|
|
69
|
+
type media_ExifError = ExifError;
|
|
42
70
|
type media_ImageFormat = ImageFormat;
|
|
43
71
|
type media_ImageInfo = ImageInfo;
|
|
44
72
|
type media_ImageInfoError = ImageInfoError;
|
|
45
73
|
type media_PdfMetadata = PdfMetadata;
|
|
46
74
|
type media_PdfMetadataError = PdfMetadataError;
|
|
75
|
+
declare const media_readExifData: typeof readExifData;
|
|
47
76
|
declare const media_readImageInfo: typeof readImageInfo;
|
|
48
77
|
declare const media_readPdfMetadata: typeof readPdfMetadata;
|
|
49
78
|
declare namespace media {
|
|
50
|
-
export { type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, media_readImageInfo as readImageInfo, media_readPdfMetadata as readPdfMetadata };
|
|
79
|
+
export { type media_ExifData as ExifData, type media_ExifError as ExifError, type media_ImageFormat as ImageFormat, type media_ImageInfo as ImageInfo, type media_ImageInfoError as ImageInfoError, type media_PdfMetadata as PdfMetadata, type media_PdfMetadataError as PdfMetadataError, media_readExifData as readExifData, media_readImageInfo as readImageInfo, media_readPdfMetadata as readPdfMetadata };
|
|
51
80
|
}
|
|
52
81
|
|
|
53
|
-
export { type ImageFormat as I, type PdfMetadata as P, type
|
|
82
|
+
export { type ExifData as E, type ImageFormat as I, type PdfMetadata as P, type ExifError as a, type ImageInfo as b, type ImageInfoError as c, type PdfMetadataError as d, readImageInfo as e, readPdfMetadata as f, media as m, readExifData as r };
|
package/dist/tools/hashing.cjs
CHANGED
|
@@ -175,13 +175,7 @@ function generateMd5Hash(password) {
|
|
|
175
175
|
return "$apr1$" + md5__default.default(password);
|
|
176
176
|
}
|
|
177
177
|
function generateBcryptHash(password, rounds = 10) {
|
|
178
|
-
|
|
179
|
-
let salt = "";
|
|
180
|
-
for (let i = 0; i < 22; i++) salt += chars[Math.floor(Math.random() * chars.length)];
|
|
181
|
-
const roundStr = rounds.toString().padStart(2, "0");
|
|
182
|
-
let hash = "";
|
|
183
|
-
for (let i = 0; i < 31; i++) hash += chars[Math.floor(Math.random() * chars.length)];
|
|
184
|
-
return `$2b$${roundStr}$${salt}${hash}`;
|
|
178
|
+
return bcrypt__default.default.hashSync(password, rounds);
|
|
185
179
|
}
|
|
186
180
|
function generatePlain(password) {
|
|
187
181
|
return password;
|
|
@@ -190,7 +184,7 @@ function createEntry(username, password, algorithm = "bcrypt") {
|
|
|
190
184
|
let hashedPassword;
|
|
191
185
|
switch (algorithm) {
|
|
192
186
|
case "bcrypt":
|
|
193
|
-
hashedPassword = generateBcryptHash();
|
|
187
|
+
hashedPassword = generateBcryptHash(password);
|
|
194
188
|
break;
|
|
195
189
|
case "sha1":
|
|
196
190
|
hashedPassword = generateSha1Hash(password);
|
package/dist/tools/hashing.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, createEntry, generateBcryptHash, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, hashAll, hashOne, hashPassword, hashWithTiming, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword } from '../chunk-
|
|
1
|
+
export { HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, createEntry, generateBcryptHash, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, hashAll, hashOne, hashPassword, hashWithTiming, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword } from '../chunk-GEFVMGZR.js';
|
|
2
2
|
import '../chunk-MLKGABMK.js';
|
package/dist/tools/media.cjs
CHANGED
|
@@ -126,6 +126,195 @@ function readImageInfo(bytes) {
|
|
|
126
126
|
}
|
|
127
127
|
return { error: "Unrecognized image format. Supported formats: PNG, JPEG, GIF, WebP, BMP" };
|
|
128
128
|
}
|
|
129
|
+
var ORIENTATION_LABELS = {
|
|
130
|
+
1: "Normal",
|
|
131
|
+
2: "Mirrored horizontal",
|
|
132
|
+
3: "Rotated 180\xB0",
|
|
133
|
+
4: "Mirrored vertical",
|
|
134
|
+
5: "Mirrored horizontal, rotated 90\xB0 CCW",
|
|
135
|
+
6: "Rotated 90\xB0 CW",
|
|
136
|
+
7: "Mirrored horizontal, rotated 90\xB0 CW",
|
|
137
|
+
8: "Rotated 90\xB0 CCW"
|
|
138
|
+
};
|
|
139
|
+
function tu16(r, o) {
|
|
140
|
+
return r.view.getUint16(o, r.little);
|
|
141
|
+
}
|
|
142
|
+
function tu32(r, o) {
|
|
143
|
+
return r.view.getUint32(o, r.little);
|
|
144
|
+
}
|
|
145
|
+
function readIfdEntries(r, ifdOffset) {
|
|
146
|
+
if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
|
|
147
|
+
const numEntries = tu16(r, ifdOffset);
|
|
148
|
+
const entries = [];
|
|
149
|
+
for (let i = 0; i < numEntries; i++) {
|
|
150
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
151
|
+
if (entryOffset + 12 > r.bytes.length) break;
|
|
152
|
+
entries.push({
|
|
153
|
+
tag: tu16(r, entryOffset),
|
|
154
|
+
type: tu16(r, entryOffset + 2),
|
|
155
|
+
count: tu32(r, entryOffset + 4),
|
|
156
|
+
valueOffsetField: entryOffset + 8
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { entries };
|
|
160
|
+
}
|
|
161
|
+
function findEntry(entries, tag) {
|
|
162
|
+
return entries.find((e) => e.tag === tag);
|
|
163
|
+
}
|
|
164
|
+
function readAscii(r, entry) {
|
|
165
|
+
const size = entry.count;
|
|
166
|
+
const inline = size <= 4;
|
|
167
|
+
const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
168
|
+
if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
|
|
169
|
+
let out = "";
|
|
170
|
+
for (let i = 0; i < size; i++) {
|
|
171
|
+
const c = r.bytes[dataOffset + i];
|
|
172
|
+
if (c === 0) break;
|
|
173
|
+
out += String.fromCharCode(c);
|
|
174
|
+
}
|
|
175
|
+
const trimmed = out.trim();
|
|
176
|
+
return trimmed || null;
|
|
177
|
+
}
|
|
178
|
+
function readRational(r, entry) {
|
|
179
|
+
const dataOffset = r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
180
|
+
if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
|
|
181
|
+
return { num: tu32(r, dataOffset), den: tu32(r, dataOffset + 4) };
|
|
182
|
+
}
|
|
183
|
+
function readGpsCoordinate(r, entries, valueTag, refTag) {
|
|
184
|
+
const valueEntry = findEntry(entries, valueTag);
|
|
185
|
+
if (!valueEntry) return null;
|
|
186
|
+
const dataOffset = r.tiffStart + tu32(r, valueEntry.valueOffsetField);
|
|
187
|
+
if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
|
|
188
|
+
const deg = tu32(r, dataOffset) / (tu32(r, dataOffset + 4) || 1);
|
|
189
|
+
const min = tu32(r, dataOffset + 8) / (tu32(r, dataOffset + 12) || 1);
|
|
190
|
+
const sec = tu32(r, dataOffset + 16) / (tu32(r, dataOffset + 20) || 1);
|
|
191
|
+
let decimal = deg + min / 60 + sec / 3600;
|
|
192
|
+
const refEntry = findEntry(entries, refTag);
|
|
193
|
+
const ref = refEntry ? readAscii(r, refEntry) : null;
|
|
194
|
+
if (ref === "S" || ref === "W") decimal = -decimal;
|
|
195
|
+
return Math.round(decimal * 1e6) / 1e6;
|
|
196
|
+
}
|
|
197
|
+
function gcd(a, b) {
|
|
198
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
199
|
+
}
|
|
200
|
+
function round2(n) {
|
|
201
|
+
return Math.round(n * 100) / 100;
|
|
202
|
+
}
|
|
203
|
+
function formatExposureTime(num, den) {
|
|
204
|
+
if (num === 0) return "0s";
|
|
205
|
+
if (num >= den) return `${round2(num / den)}s`;
|
|
206
|
+
const divisor = gcd(num, den) || 1;
|
|
207
|
+
const n = num / divisor;
|
|
208
|
+
const d = den / divisor;
|
|
209
|
+
return `1/${Math.round(d / n)}`;
|
|
210
|
+
}
|
|
211
|
+
function readExifData(bytes) {
|
|
212
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
213
|
+
return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
|
|
214
|
+
}
|
|
215
|
+
let offset = 2;
|
|
216
|
+
let exifStart = -1;
|
|
217
|
+
while (offset + 4 <= bytes.length) {
|
|
218
|
+
if (bytes[offset] !== 255) {
|
|
219
|
+
offset++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const marker = bytes[offset + 1];
|
|
223
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
224
|
+
offset += 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (marker === 217 || marker === 218) break;
|
|
228
|
+
const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
229
|
+
if (marker === 225) {
|
|
230
|
+
const idOffset = offset + 4;
|
|
231
|
+
if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
|
|
232
|
+
exifStart = idOffset + 6;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
offset += 2 + segmentLength;
|
|
237
|
+
}
|
|
238
|
+
if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
|
|
239
|
+
if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
|
|
240
|
+
const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
|
|
241
|
+
if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
|
|
242
|
+
const little = byteOrder === "II";
|
|
243
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
244
|
+
const r = { bytes, view, little, tiffStart: exifStart };
|
|
245
|
+
if (tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
|
|
246
|
+
const ifd0Offset = exifStart + tu32(r, exifStart + 4);
|
|
247
|
+
const ifd0 = readIfdEntries(r, ifd0Offset);
|
|
248
|
+
if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
|
|
249
|
+
const result = { tagCount: ifd0.entries.length };
|
|
250
|
+
const makeEntry = findEntry(ifd0.entries, 271);
|
|
251
|
+
if (makeEntry) {
|
|
252
|
+
const v = readAscii(r, makeEntry);
|
|
253
|
+
if (v) result.make = v;
|
|
254
|
+
}
|
|
255
|
+
const modelEntry = findEntry(ifd0.entries, 272);
|
|
256
|
+
if (modelEntry) {
|
|
257
|
+
const v = readAscii(r, modelEntry);
|
|
258
|
+
if (v) result.model = v;
|
|
259
|
+
}
|
|
260
|
+
const softwareEntry = findEntry(ifd0.entries, 305);
|
|
261
|
+
if (softwareEntry) {
|
|
262
|
+
const v = readAscii(r, softwareEntry);
|
|
263
|
+
if (v) result.software = v;
|
|
264
|
+
}
|
|
265
|
+
const dateTimeEntry = findEntry(ifd0.entries, 306);
|
|
266
|
+
if (dateTimeEntry) {
|
|
267
|
+
const v = readAscii(r, dateTimeEntry);
|
|
268
|
+
if (v) result.dateTime = v;
|
|
269
|
+
}
|
|
270
|
+
const orientEntry = findEntry(ifd0.entries, 274);
|
|
271
|
+
if (orientEntry) {
|
|
272
|
+
const v = tu16(r, orientEntry.valueOffsetField);
|
|
273
|
+
result.orientation = v;
|
|
274
|
+
result.orientationLabel = ORIENTATION_LABELS[v] ?? "Unknown";
|
|
275
|
+
}
|
|
276
|
+
const exifIfdPtr = findEntry(ifd0.entries, 34665);
|
|
277
|
+
if (exifIfdPtr) {
|
|
278
|
+
const subOffset = exifStart + tu32(r, exifIfdPtr.valueOffsetField);
|
|
279
|
+
const sub = readIfdEntries(r, subOffset);
|
|
280
|
+
if (sub) {
|
|
281
|
+
const dtoEntry = findEntry(sub.entries, 36867);
|
|
282
|
+
if (dtoEntry) {
|
|
283
|
+
const v = readAscii(r, dtoEntry);
|
|
284
|
+
if (v) result.dateTimeOriginal = v;
|
|
285
|
+
}
|
|
286
|
+
const expEntry = findEntry(sub.entries, 33434);
|
|
287
|
+
if (expEntry) {
|
|
288
|
+
const rat = readRational(r, expEntry);
|
|
289
|
+
if (rat && rat.den !== 0) result.exposureTime = formatExposureTime(rat.num, rat.den);
|
|
290
|
+
}
|
|
291
|
+
const fEntry = findEntry(sub.entries, 33437);
|
|
292
|
+
if (fEntry) {
|
|
293
|
+
const rat = readRational(r, fEntry);
|
|
294
|
+
if (rat && rat.den !== 0) result.fNumber = round2(rat.num / rat.den);
|
|
295
|
+
}
|
|
296
|
+
const isoEntry = findEntry(sub.entries, 34855);
|
|
297
|
+
if (isoEntry) result.isoSpeed = tu16(r, isoEntry.valueOffsetField);
|
|
298
|
+
const focalEntry = findEntry(sub.entries, 37386);
|
|
299
|
+
if (focalEntry) {
|
|
300
|
+
const rat = readRational(r, focalEntry);
|
|
301
|
+
if (rat && rat.den !== 0) result.focalLength = round2(rat.num / rat.den);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const gpsIfdPtr = findEntry(ifd0.entries, 34853);
|
|
306
|
+
if (gpsIfdPtr) {
|
|
307
|
+
const gpsOffset = exifStart + tu32(r, gpsIfdPtr.valueOffsetField);
|
|
308
|
+
const gps = readIfdEntries(r, gpsOffset);
|
|
309
|
+
if (gps) {
|
|
310
|
+
const lat = readGpsCoordinate(r, gps.entries, 2, 1);
|
|
311
|
+
const lon = readGpsCoordinate(r, gps.entries, 4, 3);
|
|
312
|
+
if (lat !== null) result.gpsLatitude = lat;
|
|
313
|
+
if (lon !== null) result.gpsLongitude = lon;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
129
318
|
function bytesToLatin1(bytes) {
|
|
130
319
|
let out = "";
|
|
131
320
|
const CHUNK = 32768;
|
|
@@ -269,5 +458,6 @@ function readPdfMetadata(bytes) {
|
|
|
269
458
|
};
|
|
270
459
|
}
|
|
271
460
|
|
|
461
|
+
exports.readExifData = readExifData;
|
|
272
462
|
exports.readImageInfo = readImageInfo;
|
|
273
463
|
exports.readPdfMetadata = readPdfMetadata;
|
package/dist/tools/media.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { I as ImageFormat,
|
|
1
|
+
export { E as ExifData, a as ExifError, I as ImageFormat, b as ImageInfo, c as ImageInfoError, P as PdfMetadata, d as PdfMetadataError, r as readExifData, e as readImageInfo, f as readPdfMetadata } from '../media-CHaBS1dI.cjs';
|
package/dist/tools/media.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { I as ImageFormat,
|
|
1
|
+
export { E as ExifData, a as ExifError, I as ImageFormat, b as ImageInfo, c as ImageInfoError, P as PdfMetadata, d as PdfMetadataError, r as readExifData, e as readImageInfo, f as readPdfMetadata } from '../media-CHaBS1dI.js';
|
package/dist/tools/media.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { readImageInfo, readPdfMetadata } from '../chunk-
|
|
1
|
+
export { readExifData, readImageInfo, readPdfMetadata } from '../chunk-FXBB7O5A.js';
|
|
2
2
|
import '../chunk-MLKGABMK.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "140+ developer utility tools for Node.js: JSON, encoding, hashing, color, CSS, network and more. Runs locally, no API key required.",
|
|
5
5
|
"author": "Utilix <hello@utilix.tech>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"prepublishOnly": "npm run build && npm test"
|
|
139
139
|
},
|
|
140
140
|
"devDependencies": {
|
|
141
|
-
"@types/bcryptjs": "^
|
|
141
|
+
"@types/bcryptjs": "^3.0.0",
|
|
142
142
|
"@types/blueimp-md5": "^2.18.2",
|
|
143
143
|
"@types/js-yaml": "^4.0.9",
|
|
144
144
|
"@types/node": "^20.0.0",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"vitest": "^2.0.0"
|
|
149
149
|
},
|
|
150
150
|
"dependencies": {
|
|
151
|
-
"bcryptjs": "^
|
|
151
|
+
"bcryptjs": "^3.0.3",
|
|
152
152
|
"blueimp-md5": "^2.19.0",
|
|
153
153
|
"cron-parser": "^4.9.0",
|
|
154
154
|
"date-fns": "^3.6.0",
|