@utilix-tech/sdk 0.10.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
+ ## 0.12.0
6
+ - Added ICO Favicon Inspector (`readIcoInfo`) and Barcode Checksum Validator (`validateBarcode`).
7
+
8
+ ## 0.11.0
9
+ - Added IBAN Validator / Formatter (`validateIban`) and Loan / Mortgage Calculator (`calculateLoan`).
10
+ - (npm publish for this version was retried after fixing a CI pin: `npm install -g npm@latest` started pulling npm 12, which requires Node >=22 and broke on this job's pinned Node 20 — now pinned to the npm 11 line.)
11
+
5
12
  ## 0.10.0
6
13
  - Added robots.txt Validator and Credit Card Number Validator.
7
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **531 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **535 developer utility functions for Node.js: runs locally, no API key required.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@utilix-tech/sdk?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@utilix-tech/sdk)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@utilix-tech/sdk?style=flat-square)](https://www.npmjs.com/package/@utilix-tech/sdk)
@@ -242,7 +242,7 @@ getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
242
242
  ### `/units`: Conversions
243
243
 
244
244
  ```ts
245
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber } from "@utilix-tech/sdk/units";
245
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode } from "@utilix-tech/sdk/units";
246
246
 
247
247
  // File sizes
248
248
  convertBytes(1073741824, "GB"); // 1
@@ -264,6 +264,18 @@ formatValue(1234567.89, { locale: "en-US", currency: "USD" });
264
264
  // Credit card Luhn checksum + network detection
265
265
  validateCardNumber("4111 1111 1111 1111");
266
266
  // { valid: true, network: "Visa", length: 16, formatted: "4111 1111 1111 1111" }
267
+
268
+ // IBAN mod-97 checksum + formatting
269
+ validateIban("DE89 3704 0044 0532 0130 00");
270
+ // { valid: true, countryCode: "DE", checksumValid: true, formatted: "DE89 3704 0044 0532 0130 00", ... }
271
+
272
+ // Loan / mortgage amortization
273
+ calculateLoan(200000, 6.5, 360);
274
+ // { monthlyPayment: 1264.14, totalInterest: 255085.82, termMonths: 360, schedule: [...] }
275
+
276
+ // EAN-13 / EAN-8 / UPC-A barcode checksum validation
277
+ validateBarcode("4006381333931");
278
+ // { valid: true, format: "EAN-13", digits: "4006381333931", checkDigit: 1, expectedCheckDigit: 1 }
267
279
  ```
268
280
 
269
281
  ---
@@ -436,10 +448,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
436
448
 
437
449
  ---
438
450
 
439
- ### `/media`: Image Header Parsing, EXIF, PDF, WAV & ID3 Metadata
451
+ ### `/media`: Image Header Parsing, EXIF, PDF, WAV, ID3 & ICO Metadata
440
452
 
441
453
  ```ts
442
- import { readImageInfo, readExifData, readPdfMetadata, readWavInfo, readId3Tags } from "@utilix-tech/sdk/media";
454
+ import { readImageInfo, readExifData, readPdfMetadata, readWavInfo, readId3Tags, readIcoInfo } from "@utilix-tech/sdk/media";
443
455
 
444
456
  // Reads format, dimensions, bit depth, and alpha channel straight from
445
457
  // file bytes: no decoding, no canvas, no image library.
@@ -471,9 +483,15 @@ readWavInfo(new Uint8Array(wavBytes));
471
483
  const mp3Bytes = await fs.promises.readFile("track.mp3");
472
484
  readId3Tags(new Uint8Array(mp3Bytes));
473
485
  // { version: "ID3v2.3.0", title: "Track Name", artist: "Artist Name", genre: "Rock", ... }
486
+
487
+ // Reads how many images a .ico file embeds and each one's dimensions, color
488
+ // depth, and size directly from its ICONDIR/ICONDIRENTRY header table.
489
+ const icoBytes = await fs.promises.readFile("favicon.ico");
490
+ readIcoInfo(new Uint8Array(icoBytes));
491
+ // { imageCount: 3, images: [{ width: 16, height: 16, bitCount: 32, bytesInRes: 1128, ... }, ...] }
474
492
  ```
475
493
 
476
- `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded).
494
+ `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files.
477
495
 
478
496
  ---
479
497
 
@@ -488,7 +506,7 @@ readId3Tags(new Uint8Array(mp3Bytes));
488
506
  | `@utilix-tech/sdk/data` | CSV, YAML, TOML, XML, INI, NDJSON parse and convert |
489
507
  | `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, fake data |
490
508
  | `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
491
- | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format |
509
+ | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode validation, loan calculator |
492
510
  | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing |
493
511
  | `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
494
512
  | `@utilix-tech/sdk/code` | SQL format/minify, HTML format/minify, regex tester, GraphQL format |
@@ -496,7 +514,7 @@ readId3Tags(new Uint8Array(mp3Bytes));
496
514
  | `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
497
515
  | `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
498
516
  | `@utilix-tech/sdk/ai_agent` | Token estimate/trim, chunk text, extract URLs/JSON/keywords, sanitize HTML, flatten/merge JSON, dedupe lines, validate schema, PII/secret/injection detect |
499
- | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; WAV audio info |
517
+ | `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; WAV audio info; ID3 tags; ICO favicon inspection |
500
518
 
501
519
  ---
502
520
 
@@ -16,6 +16,7 @@ __export(units_exports, {
16
16
  bumpVersion: () => bumpVersion,
17
17
  calcAspectRatio: () => calcAspectRatio,
18
18
  calcSubnet: () => calcSubnet,
19
+ calculateLoan: () => calculateLoan,
19
20
  compareSemver: () => compareSemver,
20
21
  convert: () => convert,
21
22
  convertBytes: () => convertBytes,
@@ -60,7 +61,9 @@ __export(units_exports, {
60
61
  sortVersions: () => sortVersions,
61
62
  splitSubnet: () => splitSubnet,
62
63
  symbolicToPermissions: () => symbolicToPermissions,
64
+ validateBarcode: () => validateBarcode,
63
65
  validateCardNumber: () => validateCardNumber,
66
+ validateIban: () => validateIban,
64
67
  viewportToPx: () => viewportToPx
65
68
  });
66
69
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -953,5 +956,189 @@ function validateCardNumber(input) {
953
956
  const formatted = formatCardNumber(digits, network);
954
957
  return { valid, network, length: digits.length, formatted };
955
958
  }
959
+ var IBAN_COUNTRY_LENGTHS = {
960
+ AD: 24,
961
+ AE: 23,
962
+ AL: 28,
963
+ AT: 20,
964
+ AZ: 28,
965
+ BA: 20,
966
+ BE: 16,
967
+ BG: 22,
968
+ BH: 22,
969
+ BR: 29,
970
+ BY: 28,
971
+ CH: 21,
972
+ CR: 22,
973
+ CY: 28,
974
+ CZ: 24,
975
+ DE: 22,
976
+ DK: 18,
977
+ DO: 28,
978
+ EE: 20,
979
+ EG: 29,
980
+ ES: 24,
981
+ FI: 18,
982
+ FO: 18,
983
+ FR: 27,
984
+ GB: 22,
985
+ GE: 22,
986
+ GI: 23,
987
+ GL: 18,
988
+ GR: 27,
989
+ GT: 28,
990
+ HR: 21,
991
+ HU: 28,
992
+ IE: 22,
993
+ IL: 23,
994
+ IQ: 23,
995
+ IS: 26,
996
+ IT: 27,
997
+ JO: 30,
998
+ KW: 30,
999
+ KZ: 20,
1000
+ LB: 28,
1001
+ LC: 32,
1002
+ LI: 21,
1003
+ LT: 20,
1004
+ LU: 20,
1005
+ LV: 21,
1006
+ LY: 25,
1007
+ MC: 27,
1008
+ MD: 24,
1009
+ ME: 22,
1010
+ MK: 19,
1011
+ MR: 27,
1012
+ MT: 31,
1013
+ MU: 30,
1014
+ NL: 18,
1015
+ NO: 15,
1016
+ PK: 24,
1017
+ PL: 28,
1018
+ PS: 29,
1019
+ PT: 25,
1020
+ QA: 29,
1021
+ RO: 24,
1022
+ RS: 22,
1023
+ SA: 24,
1024
+ SC: 31,
1025
+ SE: 24,
1026
+ SI: 19,
1027
+ SK: 24,
1028
+ SM: 27,
1029
+ ST: 25,
1030
+ SV: 28,
1031
+ TL: 23,
1032
+ TN: 24,
1033
+ TR: 26,
1034
+ UA: 29,
1035
+ VA: 22,
1036
+ VG: 24,
1037
+ XK: 20
1038
+ };
1039
+ function ibanMod97(numeric) {
1040
+ let remainder = 0;
1041
+ for (let i = 0; i < numeric.length; i += 7) {
1042
+ remainder = parseInt(String(remainder) + numeric.substr(i, 7), 10) % 97;
1043
+ }
1044
+ return remainder;
1045
+ }
1046
+ function formatIban(cleaned) {
1047
+ return cleaned.match(/.{1,4}/g)?.join(" ") ?? cleaned;
1048
+ }
1049
+ function validateIban(input) {
1050
+ if (!input || !input.trim()) return { error: "Input is empty" };
1051
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
1052
+ if (!/^[A-Z0-9]+$/.test(cleaned)) return { error: "IBAN must contain only letters and digits" };
1053
+ if (cleaned.length < 4 || cleaned.length > 34) {
1054
+ return { error: `IBAN length (${cleaned.length}) is outside the valid range of 4-34 characters` };
1055
+ }
1056
+ if (!/^[A-Z]{2}\d{2}/.test(cleaned)) {
1057
+ return { error: "IBAN must start with a 2-letter country code followed by 2 check digits" };
1058
+ }
1059
+ const countryCode = cleaned.slice(0, 2);
1060
+ const checkDigits = cleaned.slice(2, 4);
1061
+ const bban = cleaned.slice(4);
1062
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
1063
+ const numeric = rearranged.split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
1064
+ const checksumValid = ibanMod97(numeric) === 1;
1065
+ const expectedLength = IBAN_COUNTRY_LENGTHS[countryCode] ?? null;
1066
+ const lengthValid = expectedLength === null ? null : cleaned.length === expectedLength;
1067
+ const valid = checksumValid && lengthValid !== false;
1068
+ return {
1069
+ valid,
1070
+ formatted: formatIban(cleaned),
1071
+ countryCode,
1072
+ checkDigits,
1073
+ bban,
1074
+ length: cleaned.length,
1075
+ expectedLength,
1076
+ checksumValid,
1077
+ lengthValid
1078
+ };
1079
+ }
1080
+ function round2(n) {
1081
+ return Math.round(n * 100) / 100;
1082
+ }
1083
+ function calculateLoan(principal, annualRatePercent, termMonths) {
1084
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
1085
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
1086
+ return { error: "Annual interest rate must be zero or a positive number" };
1087
+ }
1088
+ if (!Number.isInteger(termMonths) || termMonths <= 0) {
1089
+ return { error: "Term must be a positive whole number of months" };
1090
+ }
1091
+ const monthlyRate = annualRatePercent / 100 / 12;
1092
+ let monthlyPayment;
1093
+ if (monthlyRate === 0) {
1094
+ monthlyPayment = principal / termMonths;
1095
+ } else {
1096
+ const factor = Math.pow(1 + monthlyRate, termMonths);
1097
+ monthlyPayment = principal * monthlyRate * factor / (factor - 1);
1098
+ }
1099
+ const schedule = [];
1100
+ let balance = round2(principal);
1101
+ let totalPayment = 0;
1102
+ for (let month = 1; month <= termMonths; month++) {
1103
+ const interest = monthlyRate === 0 ? 0 : round2(balance * monthlyRate);
1104
+ let principalPortion = round2(monthlyPayment) - interest;
1105
+ if (month === termMonths || principalPortion > balance) principalPortion = balance;
1106
+ const payment = round2(principalPortion + interest);
1107
+ balance = round2(balance - principalPortion);
1108
+ totalPayment += payment;
1109
+ schedule.push({ month, payment, principal: round2(principalPortion), interest, balance: Math.max(balance, 0) });
1110
+ }
1111
+ return {
1112
+ monthlyPayment: round2(monthlyPayment),
1113
+ totalPayment: round2(totalPayment),
1114
+ totalInterest: round2(totalPayment - principal),
1115
+ termMonths,
1116
+ schedule
1117
+ };
1118
+ }
1119
+ var BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
1120
+ function computeBarcodeCheckDigit(dataDigits) {
1121
+ let sum = 0;
1122
+ const n = dataDigits.length;
1123
+ for (let i = 0; i < n; i++) {
1124
+ const distanceFromEnd = n - 1 - i;
1125
+ const weight = distanceFromEnd % 2 === 0 ? 3 : 1;
1126
+ sum += dataDigits[i] * weight;
1127
+ }
1128
+ return (10 - sum % 10) % 10;
1129
+ }
1130
+ function validateBarcode(input) {
1131
+ if (!input || !input.trim()) return { error: "Input is empty" };
1132
+ const digits = input.replace(/[\s-]/g, "");
1133
+ if (!/^\d+$/.test(digits)) return { error: "Barcode must contain only digits (spaces and hyphens are ignored)" };
1134
+ const format = BARCODE_FORMAT_BY_LENGTH[digits.length];
1135
+ if (!format) {
1136
+ return { error: `Unsupported barcode length (${digits.length} digits); expected 8 (EAN-8), 12 (UPC-A), or 13 (EAN-13)` };
1137
+ }
1138
+ const dataDigits = digits.slice(0, -1).split("").map(Number);
1139
+ const checkDigit = Number(digits[digits.length - 1]);
1140
+ const expectedCheckDigit = computeBarcodeCheckDigit(dataDigits);
1141
+ return { valid: checkDigit === expectedCheckDigit, format, digits, checkDigit, expectedCheckDigit };
1142
+ }
956
1143
 
957
- export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_VIEWPORTS, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, compareSemver, convert, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateCardNumber, viewportToPx };
1144
+ export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_VIEWPORTS, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateLoan, compareSemver, convert, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, viewportToPx };
@@ -4,6 +4,7 @@ import { __export } from './chunk-MLKGABMK.js';
4
4
  var media_exports = {};
5
5
  __export(media_exports, {
6
6
  readExifData: () => readExifData,
7
+ readIcoInfo: () => readIcoInfo,
7
8
  readId3Tags: () => readId3Tags,
8
9
  readImageInfo: () => readImageInfo,
9
10
  readPdfMetadata: () => readPdfMetadata,
@@ -730,5 +731,38 @@ function readId3Tags(bytes) {
730
731
  }
731
732
  return { error: "No ID3v1 or ID3v2.3/2.4 tags found in this file" };
732
733
  }
734
+ function readU16LE(bytes, offset) {
735
+ return bytes[offset] | bytes[offset + 1] << 8;
736
+ }
737
+ function readU32LE(bytes, offset) {
738
+ return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
739
+ }
740
+ function readIcoInfo(bytes) {
741
+ if (bytes.length < 6) return { error: "File too small to be a valid ICO file" };
742
+ if (readU16LE(bytes, 0) !== 0) return { error: "Not a valid ICO file (reserved header field must be 0)" };
743
+ const type = readU16LE(bytes, 2);
744
+ if (type === 2) return { error: "This is a .cur cursor file (type 2), not a .ico icon file" };
745
+ if (type !== 1) return { error: `Unrecognized ICONDIR type (${type}); expected 1 for an icon file` };
746
+ const count = readU16LE(bytes, 4);
747
+ if (count === 0) return { error: "ICO file declares zero embedded images" };
748
+ const images = [];
749
+ for (let i = 0; i < count; i++) {
750
+ const offset = 6 + i * 16;
751
+ if (offset + 16 > bytes.length) return { error: `Truncated ICONDIRENTRY for image ${i}` };
752
+ const widthRaw = bytes[offset];
753
+ const heightRaw = bytes[offset + 1];
754
+ const colorCountRaw = bytes[offset + 2];
755
+ images.push({
756
+ width: widthRaw === 0 ? 256 : widthRaw,
757
+ height: heightRaw === 0 ? 256 : heightRaw,
758
+ colorCount: colorCountRaw === 0 ? null : colorCountRaw,
759
+ planes: readU16LE(bytes, offset + 4),
760
+ bitCount: readU16LE(bytes, offset + 6),
761
+ bytesInRes: readU32LE(bytes, offset + 8),
762
+ imageOffset: readU32LE(bytes, offset + 12)
763
+ });
764
+ }
765
+ return { imageCount: count, images };
766
+ }
733
767
 
734
- export { media_exports, readExifData, readId3Tags, readImageInfo, readPdfMetadata, readWavInfo };
768
+ export { media_exports, readExifData, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readWavInfo };
package/dist/index.cjs CHANGED
@@ -6621,6 +6621,7 @@ __export(units_exports, {
6621
6621
  bumpVersion: () => bumpVersion,
6622
6622
  calcAspectRatio: () => calcAspectRatio,
6623
6623
  calcSubnet: () => calcSubnet,
6624
+ calculateLoan: () => calculateLoan,
6624
6625
  compareSemver: () => compareSemver,
6625
6626
  convert: () => convert,
6626
6627
  convertBytes: () => convertBytes,
@@ -6665,7 +6666,9 @@ __export(units_exports, {
6665
6666
  sortVersions: () => sortVersions,
6666
6667
  splitSubnet: () => splitSubnet,
6667
6668
  symbolicToPermissions: () => symbolicToPermissions,
6669
+ validateBarcode: () => validateBarcode,
6668
6670
  validateCardNumber: () => validateCardNumber,
6671
+ validateIban: () => validateIban,
6669
6672
  viewportToPx: () => viewportToPx
6670
6673
  });
6671
6674
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -7558,6 +7561,190 @@ function validateCardNumber(input) {
7558
7561
  const formatted = formatCardNumber(digits, network);
7559
7562
  return { valid, network, length: digits.length, formatted };
7560
7563
  }
7564
+ var IBAN_COUNTRY_LENGTHS = {
7565
+ AD: 24,
7566
+ AE: 23,
7567
+ AL: 28,
7568
+ AT: 20,
7569
+ AZ: 28,
7570
+ BA: 20,
7571
+ BE: 16,
7572
+ BG: 22,
7573
+ BH: 22,
7574
+ BR: 29,
7575
+ BY: 28,
7576
+ CH: 21,
7577
+ CR: 22,
7578
+ CY: 28,
7579
+ CZ: 24,
7580
+ DE: 22,
7581
+ DK: 18,
7582
+ DO: 28,
7583
+ EE: 20,
7584
+ EG: 29,
7585
+ ES: 24,
7586
+ FI: 18,
7587
+ FO: 18,
7588
+ FR: 27,
7589
+ GB: 22,
7590
+ GE: 22,
7591
+ GI: 23,
7592
+ GL: 18,
7593
+ GR: 27,
7594
+ GT: 28,
7595
+ HR: 21,
7596
+ HU: 28,
7597
+ IE: 22,
7598
+ IL: 23,
7599
+ IQ: 23,
7600
+ IS: 26,
7601
+ IT: 27,
7602
+ JO: 30,
7603
+ KW: 30,
7604
+ KZ: 20,
7605
+ LB: 28,
7606
+ LC: 32,
7607
+ LI: 21,
7608
+ LT: 20,
7609
+ LU: 20,
7610
+ LV: 21,
7611
+ LY: 25,
7612
+ MC: 27,
7613
+ MD: 24,
7614
+ ME: 22,
7615
+ MK: 19,
7616
+ MR: 27,
7617
+ MT: 31,
7618
+ MU: 30,
7619
+ NL: 18,
7620
+ NO: 15,
7621
+ PK: 24,
7622
+ PL: 28,
7623
+ PS: 29,
7624
+ PT: 25,
7625
+ QA: 29,
7626
+ RO: 24,
7627
+ RS: 22,
7628
+ SA: 24,
7629
+ SC: 31,
7630
+ SE: 24,
7631
+ SI: 19,
7632
+ SK: 24,
7633
+ SM: 27,
7634
+ ST: 25,
7635
+ SV: 28,
7636
+ TL: 23,
7637
+ TN: 24,
7638
+ TR: 26,
7639
+ UA: 29,
7640
+ VA: 22,
7641
+ VG: 24,
7642
+ XK: 20
7643
+ };
7644
+ function ibanMod97(numeric) {
7645
+ let remainder = 0;
7646
+ for (let i = 0; i < numeric.length; i += 7) {
7647
+ remainder = parseInt(String(remainder) + numeric.substr(i, 7), 10) % 97;
7648
+ }
7649
+ return remainder;
7650
+ }
7651
+ function formatIban(cleaned) {
7652
+ return cleaned.match(/.{1,4}/g)?.join(" ") ?? cleaned;
7653
+ }
7654
+ function validateIban(input) {
7655
+ if (!input || !input.trim()) return { error: "Input is empty" };
7656
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
7657
+ if (!/^[A-Z0-9]+$/.test(cleaned)) return { error: "IBAN must contain only letters and digits" };
7658
+ if (cleaned.length < 4 || cleaned.length > 34) {
7659
+ return { error: `IBAN length (${cleaned.length}) is outside the valid range of 4-34 characters` };
7660
+ }
7661
+ if (!/^[A-Z]{2}\d{2}/.test(cleaned)) {
7662
+ return { error: "IBAN must start with a 2-letter country code followed by 2 check digits" };
7663
+ }
7664
+ const countryCode = cleaned.slice(0, 2);
7665
+ const checkDigits = cleaned.slice(2, 4);
7666
+ const bban = cleaned.slice(4);
7667
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
7668
+ const numeric = rearranged.split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
7669
+ const checksumValid = ibanMod97(numeric) === 1;
7670
+ const expectedLength = IBAN_COUNTRY_LENGTHS[countryCode] ?? null;
7671
+ const lengthValid = expectedLength === null ? null : cleaned.length === expectedLength;
7672
+ const valid = checksumValid && lengthValid !== false;
7673
+ return {
7674
+ valid,
7675
+ formatted: formatIban(cleaned),
7676
+ countryCode,
7677
+ checkDigits,
7678
+ bban,
7679
+ length: cleaned.length,
7680
+ expectedLength,
7681
+ checksumValid,
7682
+ lengthValid
7683
+ };
7684
+ }
7685
+ function round2(n) {
7686
+ return Math.round(n * 100) / 100;
7687
+ }
7688
+ function calculateLoan(principal, annualRatePercent, termMonths) {
7689
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
7690
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
7691
+ return { error: "Annual interest rate must be zero or a positive number" };
7692
+ }
7693
+ if (!Number.isInteger(termMonths) || termMonths <= 0) {
7694
+ return { error: "Term must be a positive whole number of months" };
7695
+ }
7696
+ const monthlyRate = annualRatePercent / 100 / 12;
7697
+ let monthlyPayment;
7698
+ if (monthlyRate === 0) {
7699
+ monthlyPayment = principal / termMonths;
7700
+ } else {
7701
+ const factor = Math.pow(1 + monthlyRate, termMonths);
7702
+ monthlyPayment = principal * monthlyRate * factor / (factor - 1);
7703
+ }
7704
+ const schedule = [];
7705
+ let balance = round2(principal);
7706
+ let totalPayment = 0;
7707
+ for (let month = 1; month <= termMonths; month++) {
7708
+ const interest = monthlyRate === 0 ? 0 : round2(balance * monthlyRate);
7709
+ let principalPortion = round2(monthlyPayment) - interest;
7710
+ if (month === termMonths || principalPortion > balance) principalPortion = balance;
7711
+ const payment = round2(principalPortion + interest);
7712
+ balance = round2(balance - principalPortion);
7713
+ totalPayment += payment;
7714
+ schedule.push({ month, payment, principal: round2(principalPortion), interest, balance: Math.max(balance, 0) });
7715
+ }
7716
+ return {
7717
+ monthlyPayment: round2(monthlyPayment),
7718
+ totalPayment: round2(totalPayment),
7719
+ totalInterest: round2(totalPayment - principal),
7720
+ termMonths,
7721
+ schedule
7722
+ };
7723
+ }
7724
+ var BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
7725
+ function computeBarcodeCheckDigit(dataDigits) {
7726
+ let sum = 0;
7727
+ const n = dataDigits.length;
7728
+ for (let i = 0; i < n; i++) {
7729
+ const distanceFromEnd = n - 1 - i;
7730
+ const weight = distanceFromEnd % 2 === 0 ? 3 : 1;
7731
+ sum += dataDigits[i] * weight;
7732
+ }
7733
+ return (10 - sum % 10) % 10;
7734
+ }
7735
+ function validateBarcode(input) {
7736
+ if (!input || !input.trim()) return { error: "Input is empty" };
7737
+ const digits = input.replace(/[\s-]/g, "");
7738
+ if (!/^\d+$/.test(digits)) return { error: "Barcode must contain only digits (spaces and hyphens are ignored)" };
7739
+ const format = BARCODE_FORMAT_BY_LENGTH[digits.length];
7740
+ if (!format) {
7741
+ return { error: `Unsupported barcode length (${digits.length} digits); expected 8 (EAN-8), 12 (UPC-A), or 13 (EAN-13)` };
7742
+ }
7743
+ const dataDigits = digits.slice(0, -1).split("").map(Number);
7744
+ const checkDigit = Number(digits[digits.length - 1]);
7745
+ const expectedCheckDigit = computeBarcodeCheckDigit(dataDigits);
7746
+ return { valid: checkDigit === expectedCheckDigit, format, digits, checkDigit, expectedCheckDigit };
7747
+ }
7561
7748
 
7562
7749
  // src/tools/network.ts
7563
7750
  var network_exports = {};
@@ -17251,6 +17438,7 @@ function summarizeForLlm(text, maxTokens = 200, strategy = "extractive") {
17251
17438
  var media_exports = {};
17252
17439
  __export(media_exports, {
17253
17440
  readExifData: () => readExifData,
17441
+ readIcoInfo: () => readIcoInfo,
17254
17442
  readId3Tags: () => readId3Tags,
17255
17443
  readImageInfo: () => readImageInfo,
17256
17444
  readPdfMetadata: () => readPdfMetadata,
@@ -17452,12 +17640,12 @@ function readGpsCoordinate(r, entries, valueTag, refTag) {
17452
17640
  function gcd2(a, b) {
17453
17641
  return b === 0 ? a : gcd2(b, a % b);
17454
17642
  }
17455
- function round2(n) {
17643
+ function round22(n) {
17456
17644
  return Math.round(n * 100) / 100;
17457
17645
  }
17458
17646
  function formatExposureTime(num, den) {
17459
17647
  if (num === 0) return "0s";
17460
- if (num >= den) return `${round2(num / den)}s`;
17648
+ if (num >= den) return `${round22(num / den)}s`;
17461
17649
  const divisor = gcd2(num, den) || 1;
17462
17650
  const n = num / divisor;
17463
17651
  const d = den / divisor;
@@ -17546,14 +17734,14 @@ function readExifData(bytes) {
17546
17734
  const fEntry = findEntry(sub.entries, 33437);
17547
17735
  if (fEntry) {
17548
17736
  const rat = readRational(r, fEntry);
17549
- if (rat && rat.den !== 0) result.fNumber = round2(rat.num / rat.den);
17737
+ if (rat && rat.den !== 0) result.fNumber = round22(rat.num / rat.den);
17550
17738
  }
17551
17739
  const isoEntry = findEntry(sub.entries, 34855);
17552
17740
  if (isoEntry) result.isoSpeed = tu16(r, isoEntry.valueOffsetField);
17553
17741
  const focalEntry = findEntry(sub.entries, 37386);
17554
17742
  if (focalEntry) {
17555
17743
  const rat = readRational(r, focalEntry);
17556
- if (rat && rat.den !== 0) result.focalLength = round2(rat.num / rat.den);
17744
+ if (rat && rat.den !== 0) result.focalLength = round22(rat.num / rat.den);
17557
17745
  }
17558
17746
  }
17559
17747
  }
@@ -17977,6 +18165,39 @@ function readId3Tags(bytes) {
17977
18165
  }
17978
18166
  return { error: "No ID3v1 or ID3v2.3/2.4 tags found in this file" };
17979
18167
  }
18168
+ function readU16LE(bytes, offset) {
18169
+ return bytes[offset] | bytes[offset + 1] << 8;
18170
+ }
18171
+ function readU32LE(bytes, offset) {
18172
+ return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
18173
+ }
18174
+ function readIcoInfo(bytes) {
18175
+ if (bytes.length < 6) return { error: "File too small to be a valid ICO file" };
18176
+ if (readU16LE(bytes, 0) !== 0) return { error: "Not a valid ICO file (reserved header field must be 0)" };
18177
+ const type = readU16LE(bytes, 2);
18178
+ if (type === 2) return { error: "This is a .cur cursor file (type 2), not a .ico icon file" };
18179
+ if (type !== 1) return { error: `Unrecognized ICONDIR type (${type}); expected 1 for an icon file` };
18180
+ const count = readU16LE(bytes, 4);
18181
+ if (count === 0) return { error: "ICO file declares zero embedded images" };
18182
+ const images = [];
18183
+ for (let i = 0; i < count; i++) {
18184
+ const offset = 6 + i * 16;
18185
+ if (offset + 16 > bytes.length) return { error: `Truncated ICONDIRENTRY for image ${i}` };
18186
+ const widthRaw = bytes[offset];
18187
+ const heightRaw = bytes[offset + 1];
18188
+ const colorCountRaw = bytes[offset + 2];
18189
+ images.push({
18190
+ width: widthRaw === 0 ? 256 : widthRaw,
18191
+ height: heightRaw === 0 ? 256 : heightRaw,
18192
+ colorCount: colorCountRaw === 0 ? null : colorCountRaw,
18193
+ planes: readU16LE(bytes, offset + 4),
18194
+ bitCount: readU16LE(bytes, offset + 6),
18195
+ bytesInRes: readU32LE(bytes, offset + 8),
18196
+ imageOffset: readU32LE(bytes, offset + 12)
18197
+ });
18198
+ }
18199
+ return { imageCount: count, images };
18200
+ }
17980
18201
 
17981
18202
  // src/index.ts
17982
18203
  var version = "0.1.0";
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ export { t as text } from './text-Bbx-tZ43.cjs';
5
5
  export { d as data } from './data-D8XTI7Du.cjs';
6
6
  export { g as generators } from './generators-BGtRGpJZ.cjs';
7
7
  export { t as time } from './time-DbT8fjaF.cjs';
8
- export { u as units } from './units-DRT6W-vu.cjs';
8
+ export { u as units } from './units-Dt80-S2j.cjs';
9
9
  export { n as network } from './network-DaNoZ5ER.cjs';
10
10
  export { a as api } from './api-Xl7OzIgq.cjs';
11
11
  export { c as code } from './code-BUqyaofO.cjs';
@@ -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-C3ZvGyKc.cjs';
16
+ export { m as media } from './media-9HEpTPGA.cjs';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { t as text } from './text-Bbx-tZ43.js';
5
5
  export { d as data } from './data-D8XTI7Du.js';
6
6
  export { g as generators } from './generators-BGtRGpJZ.js';
7
7
  export { t as time } from './time-DbT8fjaF.js';
8
- export { u as units } from './units-DRT6W-vu.js';
8
+ export { u as units } from './units-Dt80-S2j.js';
9
9
  export { n as network } from './network-DaNoZ5ER.js';
10
10
  export { a as api } from './api-Xl7OzIgq.js';
11
11
  export { c as code } from './code-BUqyaofO.js';
@@ -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-C3ZvGyKc.js';
16
+ export { m as media } from './media-9HEpTPGA.js';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { media_exports as media } from './chunk-GUUYEIU6.js';
2
- export { units_exports as units } from './chunk-YVFM5WYI.js';
1
+ export { media_exports as media } from './chunk-XMKJYVKB.js';
2
+ export { units_exports as units } from './chunk-CJGHHS5M.js';
3
3
  export { network_exports as network } from './chunk-OB56VNKJ.js';
4
4
  export { api_exports as api } from './chunk-Q3B3YWOA.js';
5
5
  export { code_exports as code } from './chunk-V5S6OJ4A.js';