@trackunit/shared-utils 0.0.88 → 0.0.89

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/index.cjs.js CHANGED
@@ -531,6 +531,128 @@ const toID = (uuid) => Number(uuid.replace(/-/g, "").replace(/^0+/, ""));
531
531
  */
532
532
  const toIDs = (ids) => { var _a; return (_a = ids === null || ids === void 0 ? void 0 : ids.filter(id => !isNaN(toID(id))).map(id => toID(id))) !== null && _a !== void 0 ? _a : []; };
533
533
 
534
+ /**
535
+ * Converts an HTMLImageElement to a PNG image.
536
+ *
537
+ * @param options - image options
538
+ * @param {HTMLImageElement} options.image - The image element to convert.
539
+ * @param {ImageDimensions} options.dimensions - The dimensions of the image.
540
+ * @param {number} options.maxHeight - The maximum height of the converted PNG image.
541
+ * @param {number} options.maxWidth - The maximum width of the converted PNG image.
542
+ * @param {number} options.idealArea - The ideal area of the converted PNG image.
543
+ * @returns The converted PNG image as a data URL.
544
+ * @throws Error if unable to get the canvas context.
545
+ */
546
+ const toPNG = ({ image, dimensions, maxHeight, maxWidth, idealArea, }) => {
547
+ var _a, _b;
548
+ let width = (_a = dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) !== null && _a !== void 0 ? _a : image.width;
549
+ let height = (_b = dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) !== null && _b !== void 0 ? _b : image.height;
550
+ const scale = calculateImageScaleRatio(width, height, maxWidth, maxHeight, idealArea);
551
+ width = Math.round(width * scale);
552
+ height = Math.round(height * scale);
553
+ const canvas = document.createElement("canvas");
554
+ canvas.width = width;
555
+ canvas.height = height;
556
+ const canvasCtx = canvas.getContext("2d");
557
+ if (!canvasCtx) {
558
+ throw new Error("Unable to get canvas context");
559
+ }
560
+ canvasCtx.drawImage(image, 0, 0, width, height);
561
+ return canvas.toDataURL("image/png");
562
+ };
563
+ /**
564
+ * Calculates the scale ratio for an image to fit inside given bounds
565
+ * Result is based on the idealArea more info: https://nicksherman.com/size-by-area/
566
+ *
567
+ * @param {number} width The width of the image
568
+ * @param {number} height The height of the image
569
+ * @param {number} maxWidth The maxWidth of the image
570
+ * @param {number} maxHeight The maxHeight of the image
571
+ * @param {number} idealArea The ideal area for the image to cover (Read more above)
572
+ */
573
+ const calculateImageScaleRatio = (width, height, maxWidth, maxHeight, idealArea) => {
574
+ // Get the max width based on idealArea
575
+ const maxImageWidth = Math.round(width * Math.sqrt(idealArea / (width * height)));
576
+ const ratio = maxImageWidth / width;
577
+ // If the area scaling is largen than the ares, scale to fix
578
+ if (width * ratio > maxWidth || height * ratio > maxHeight) {
579
+ const scaleX = maxWidth / width;
580
+ const scaleY = maxHeight / height;
581
+ const scale = scaleX < scaleY ? scaleX : scaleY; // Use the lowest scale to make it fit and scale uniformly
582
+ return scale;
583
+ }
584
+ return maxImageWidth / width;
585
+ };
586
+ /**
587
+ * Preloads a data url as an image.
588
+ *
589
+ * @param {string} dataUrl - The data url to preload.
590
+ * @returns {Promise<HTMLImageElement>} A promise that resolves to the preloaded image.
591
+ */
592
+ const preload = (dataUrl) => {
593
+ return new Promise((resolve, reject) => {
594
+ const image = new Image();
595
+ image.src = dataUrl;
596
+ image.onload = () => resolve(image);
597
+ image.onerror = () => reject();
598
+ });
599
+ };
600
+ /**
601
+ * Extracts the type of an image from a data URL
602
+ *
603
+ * @param {string} dataURL the data URL to extract the type from.
604
+ * @returns {"jpg" | "png" | "svg"} the type of the encoded image.
605
+ * @throws
606
+ */
607
+ const getMimeTypeFromDataURL = (dataURL) => {
608
+ const mimeToTypeMap = {
609
+ "image/svg+xml": "svg",
610
+ "image/jpeg": "jpeg",
611
+ "image/jpg": "jpeg",
612
+ "image/png": "png",
613
+ };
614
+ const matches = dataURL.match(/^data:(.*?);base64,/);
615
+ if (!matches || !matches[1]) {
616
+ throw new Error("Invalid file");
617
+ }
618
+ const type = mimeToTypeMap[matches[1]];
619
+ if (!type) {
620
+ throw new Error("File is not a supported image");
621
+ }
622
+ return type;
623
+ };
624
+ /**
625
+ * Fetches and image from a URL and converts it to a base64 encoded data url
626
+ *
627
+ * @param {string} url - url pointing to an image resource
628
+ * @returns {Promise<string>} base64 encoded data url.
629
+ * @throws
630
+ */
631
+ const fetchImageAsBase64 = async (url) => {
632
+ // Fetch the image from the URL
633
+ const response = await fetch(url);
634
+ if (!response.ok) {
635
+ throw new Error(`Failed to fetch image. Status: ${response.status}`);
636
+ }
637
+ // Convert the response to a Blob
638
+ const blob = await response.blob();
639
+ // Convert the Blob to a Base64 string
640
+ return new Promise((resolve, reject) => {
641
+ const reader = new FileReader();
642
+ reader.onloadend = () => {
643
+ if (typeof reader.result === "string") {
644
+ const base64String = reader.result;
645
+ resolve(base64String);
646
+ }
647
+ reject(new Error("Failed to convert Blob to Base64."));
648
+ };
649
+ reader.onerror = () => {
650
+ reject(new Error("Failed to convert Blob to Base64."));
651
+ };
652
+ reader.readAsDataURL(blob);
653
+ });
654
+ };
655
+
534
656
  /**
535
657
  * Deletes all undefined keys from an object.
536
658
  *
@@ -982,6 +1104,137 @@ const stripHiddenCharacters = (input) => {
982
1104
  return input.replace(hiddenRegex, "");
983
1105
  };
984
1106
 
1107
+ /**
1108
+ * Convert the given value to HEX
1109
+ * If the value is not rgb, returns back the same value
1110
+ */
1111
+ const rgb2hex = (rgb) => {
1112
+ var _a;
1113
+ const hexColor = (_a = rgb
1114
+ .match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1).map(n => parseInt(n, 10).toString(16).padStart(2, "0")).join("");
1115
+ return hexColor ? `#${hexColor}` : rgb;
1116
+ };
1117
+ /**
1118
+ * Returns all svg related colors from a CSSStyleDeclaration
1119
+ */
1120
+ const colorsFromStyleDeclaration = (styles) => {
1121
+ return [styles.fill, styles.stroke, styles.stopColor].filter(color => color);
1122
+ };
1123
+ /**
1124
+ * Loads the dimensions of an SVG string.
1125
+ *
1126
+ * @param base64EncodedSVG The SVG string to load dimensions from.
1127
+ * @returns A promise that resolves to an object containing the width and height of the SVG.
1128
+ * @throws {Error} If the SVG string is invalid or the dimensions cannot be determined.
1129
+ */
1130
+ const loadSVGDimensions = (base64EncodedSVG) => {
1131
+ return new Promise((resolve, reject) => {
1132
+ // Create a temporary SVG element
1133
+ const svgAsString = atob(base64EncodedSVG.replace("data:image/svg+xml;base64,", ""));
1134
+ const parser = new DOMParser();
1135
+ const svgDoc = parser.parseFromString(svgAsString, "image/svg+xml");
1136
+ const svgElement = svgDoc.documentElement;
1137
+ // Check if parsing was successful
1138
+ if (svgElement.nodeName === "parsererror") {
1139
+ reject(new Error("Invalid SVG string"));
1140
+ return;
1141
+ }
1142
+ // Get dimensions
1143
+ const width = svgElement.width.baseVal.value;
1144
+ const height = svgElement.height.baseVal.value;
1145
+ // If width and height are not set explicitly, try to get from viewBox
1146
+ if (!width || !height) {
1147
+ const viewBox = svgElement.viewBox.baseVal;
1148
+ if (viewBox.width && viewBox.height) {
1149
+ resolve({
1150
+ width: viewBox.width,
1151
+ height: viewBox.height,
1152
+ });
1153
+ }
1154
+ else {
1155
+ reject(new Error("Unable to determine SVG dimensions"));
1156
+ }
1157
+ }
1158
+ else {
1159
+ resolve({
1160
+ width,
1161
+ height,
1162
+ });
1163
+ }
1164
+ });
1165
+ };
1166
+ /**
1167
+ * Returns an array of all colors used in an svg
1168
+ */
1169
+ const getAllColors = (base64EncodedSVG) => {
1170
+ const svgAsString = atob(base64EncodedSVG.replace("data:image/svg+xml;base64,", ""));
1171
+ const parser = new DOMParser();
1172
+ const svgImage = parser.parseFromString(svgAsString, "image/svg+xml");
1173
+ const colors = [];
1174
+ svgImage.querySelectorAll("[style]").forEach(element => {
1175
+ // Not all Elements have the style prop, this seems like a safe way to check and assert
1176
+ if (element instanceof HTMLElement) {
1177
+ const elementStyles = element.style;
1178
+ colors.push(...colorsFromStyleDeclaration(elementStyles));
1179
+ }
1180
+ });
1181
+ svgImage.querySelectorAll("style").forEach(element => {
1182
+ var _a;
1183
+ const cssRules = (_a = element.sheet) === null || _a === void 0 ? void 0 : _a.cssRules;
1184
+ if (cssRules === null || cssRules === void 0 ? void 0 : cssRules.length) {
1185
+ for (let i = 0; i < cssRules.length; i++) {
1186
+ // Checking if the style attribute exists
1187
+ const cssRule = cssRules[i]; // Replace type casting with type assertion
1188
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1189
+ if (cssRule.style) {
1190
+ const cssRuleStyles = cssRule.style;
1191
+ colors.push(...colorsFromStyleDeclaration(cssRuleStyles));
1192
+ }
1193
+ }
1194
+ }
1195
+ });
1196
+ // Get fills
1197
+ svgImage.querySelectorAll("[fill]").forEach(element => {
1198
+ var _a;
1199
+ const color = (_a = element.attributes.getNamedItem("fill")) === null || _a === void 0 ? void 0 : _a.value;
1200
+ if (color) {
1201
+ colors.push(color);
1202
+ }
1203
+ });
1204
+ // Get stokes
1205
+ svgImage.querySelectorAll("[stroke]").forEach(element => {
1206
+ var _a;
1207
+ const color = (_a = element.attributes.getNamedItem("stroke")) === null || _a === void 0 ? void 0 : _a.value;
1208
+ if (color) {
1209
+ colors.push(color);
1210
+ }
1211
+ });
1212
+ // Get stop-color (for gradients)
1213
+ svgImage.querySelectorAll("[stop-color]").forEach(element => {
1214
+ var _a;
1215
+ const color = (_a = element.attributes.getNamedItem("stop-color")) === null || _a === void 0 ? void 0 : _a.value;
1216
+ if (color) {
1217
+ colors.push(color);
1218
+ }
1219
+ });
1220
+ return [...new Set(colors.map(color => rgb2hex(color)))];
1221
+ };
1222
+ /**
1223
+ * Converts a Base64 encoded SVG to a Scaled Base64 PNG.
1224
+ *
1225
+ * @param {string} base64EncodedSVG The Base64 encoded svg string to convert
1226
+ * @param {number} maxWidth The max width(px) of the output PNG
1227
+ * @param {number} maxHeight The max height(px) of the output PNG
1228
+ * @returns {Promise<string>} A Base64 encoded PNG, scaled to the given parameters
1229
+ */
1230
+ const svgToPNG = async ({ base64EncodedSVG, maxWidth, maxHeight, idealArea, }) => {
1231
+ const [image, dimensions] = await Promise.all([
1232
+ preload(base64EncodedSVG),
1233
+ loadSVGDimensions(base64EncodedSVG),
1234
+ ]);
1235
+ return toPNG({ image, dimensions, maxHeight, maxWidth, idealArea });
1236
+ };
1237
+
985
1238
  /* eslint-disable local-rules/prefer-custom-object-keys */
986
1239
  /* eslint-disable local-rules/prefer-custom-object-from-entries */
987
1240
  /**
@@ -1065,7 +1318,9 @@ exports.alphabeticallySort = alphabeticallySort;
1065
1318
  exports.arrayLengthCompare = arrayLengthCompare;
1066
1319
  exports.arrayNotEmpty = arrayNotEmpty;
1067
1320
  exports.booleanCompare = booleanCompare;
1321
+ exports.calculateImageScaleRatio = calculateImageScaleRatio;
1068
1322
  exports.capitalize = capitalize;
1323
+ exports.colorsFromStyleDeclaration = colorsFromStyleDeclaration;
1069
1324
  exports.convertBlobToBase64 = convertBlobToBase64;
1070
1325
  exports.convertMetersToYards = convertMetersToYards;
1071
1326
  exports.convertYardsToMeters = convertYardsToMeters;
@@ -1077,14 +1332,17 @@ exports.enumFromValue = enumFromValue;
1077
1332
  exports.enumFromValueTypesafe = enumFromValueTypesafe;
1078
1333
  exports.enumOrUndefinedFromValue = enumOrUndefinedFromValue;
1079
1334
  exports.exhaustiveCheck = exhaustiveCheck;
1335
+ exports.fetchImageAsBase64 = fetchImageAsBase64;
1080
1336
  exports.filterByMultiple = filterByMultiple;
1081
1337
  exports.formatAddress = formatAddress;
1082
1338
  exports.formatCoordinates = formatCoordinates;
1083
1339
  exports.fuzzySearch = fuzzySearch;
1340
+ exports.getAllColors = getAllColors;
1084
1341
  exports.getDifferenceBetweenDates = getDifferenceBetweenDates;
1085
1342
  exports.getEndOfDay = getEndOfDay;
1086
1343
  exports.getFirstLevelObjectPropertyDifferences = getFirstLevelObjectPropertyDifferences;
1087
1344
  exports.getISOStringFromDate = getISOStringFromDate;
1345
+ exports.getMimeTypeFromDataURL = getMimeTypeFromDataURL;
1088
1346
  exports.getResizedDimensions = getResizedDimensions;
1089
1347
  exports.getStartOfDay = getStartOfDay;
1090
1348
  exports.groupBy = groupBy;
@@ -1095,6 +1353,7 @@ exports.isArrayEqual = isArrayEqual;
1095
1353
  exports.isSorted = isSorted;
1096
1354
  exports.isUUID = isUUID;
1097
1355
  exports.isValidImage = isValidImage;
1356
+ exports.loadSVGDimensions = loadSVGDimensions;
1098
1357
  exports.nonNullable = nonNullable;
1099
1358
  exports.numberCompare = numberCompare;
1100
1359
  exports.numberCompareUnknownAfterHighest = numberCompareUnknownAfterHighest;
@@ -1104,17 +1363,21 @@ exports.objectFromEntries = objectFromEntries;
1104
1363
  exports.objectKeys = objectKeys;
1105
1364
  exports.objectValues = objectValues;
1106
1365
  exports.pick = pick;
1366
+ exports.preload = preload;
1107
1367
  exports.removeLeftPadding = removeLeftPadding;
1108
1368
  exports.resizeBlob = resizeBlob;
1109
1369
  exports.resizeImage = resizeImage;
1370
+ exports.rgb2hex = rgb2hex;
1110
1371
  exports.size = size;
1111
1372
  exports.stringCompare = stringCompare;
1112
1373
  exports.stringCompareFromKey = stringCompareFromKey;
1113
1374
  exports.stringNaturalCompare = stringNaturalCompare;
1114
1375
  exports.stripHiddenCharacters = stripHiddenCharacters;
1376
+ exports.svgToPNG = svgToPNG;
1115
1377
  exports.titleCase = titleCase;
1116
1378
  exports.toID = toID;
1117
1379
  exports.toIDs = toIDs;
1380
+ exports.toPNG = toPNG;
1118
1381
  exports.toUUID = toUUID;
1119
1382
  exports.trimIds = trimIds;
1120
1383
  exports.trimPath = trimPath;
package/index.esm.js CHANGED
@@ -529,6 +529,128 @@ const toID = (uuid) => Number(uuid.replace(/-/g, "").replace(/^0+/, ""));
529
529
  */
530
530
  const toIDs = (ids) => { var _a; return (_a = ids === null || ids === void 0 ? void 0 : ids.filter(id => !isNaN(toID(id))).map(id => toID(id))) !== null && _a !== void 0 ? _a : []; };
531
531
 
532
+ /**
533
+ * Converts an HTMLImageElement to a PNG image.
534
+ *
535
+ * @param options - image options
536
+ * @param {HTMLImageElement} options.image - The image element to convert.
537
+ * @param {ImageDimensions} options.dimensions - The dimensions of the image.
538
+ * @param {number} options.maxHeight - The maximum height of the converted PNG image.
539
+ * @param {number} options.maxWidth - The maximum width of the converted PNG image.
540
+ * @param {number} options.idealArea - The ideal area of the converted PNG image.
541
+ * @returns The converted PNG image as a data URL.
542
+ * @throws Error if unable to get the canvas context.
543
+ */
544
+ const toPNG = ({ image, dimensions, maxHeight, maxWidth, idealArea, }) => {
545
+ var _a, _b;
546
+ let width = (_a = dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) !== null && _a !== void 0 ? _a : image.width;
547
+ let height = (_b = dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) !== null && _b !== void 0 ? _b : image.height;
548
+ const scale = calculateImageScaleRatio(width, height, maxWidth, maxHeight, idealArea);
549
+ width = Math.round(width * scale);
550
+ height = Math.round(height * scale);
551
+ const canvas = document.createElement("canvas");
552
+ canvas.width = width;
553
+ canvas.height = height;
554
+ const canvasCtx = canvas.getContext("2d");
555
+ if (!canvasCtx) {
556
+ throw new Error("Unable to get canvas context");
557
+ }
558
+ canvasCtx.drawImage(image, 0, 0, width, height);
559
+ return canvas.toDataURL("image/png");
560
+ };
561
+ /**
562
+ * Calculates the scale ratio for an image to fit inside given bounds
563
+ * Result is based on the idealArea more info: https://nicksherman.com/size-by-area/
564
+ *
565
+ * @param {number} width The width of the image
566
+ * @param {number} height The height of the image
567
+ * @param {number} maxWidth The maxWidth of the image
568
+ * @param {number} maxHeight The maxHeight of the image
569
+ * @param {number} idealArea The ideal area for the image to cover (Read more above)
570
+ */
571
+ const calculateImageScaleRatio = (width, height, maxWidth, maxHeight, idealArea) => {
572
+ // Get the max width based on idealArea
573
+ const maxImageWidth = Math.round(width * Math.sqrt(idealArea / (width * height)));
574
+ const ratio = maxImageWidth / width;
575
+ // If the area scaling is largen than the ares, scale to fix
576
+ if (width * ratio > maxWidth || height * ratio > maxHeight) {
577
+ const scaleX = maxWidth / width;
578
+ const scaleY = maxHeight / height;
579
+ const scale = scaleX < scaleY ? scaleX : scaleY; // Use the lowest scale to make it fit and scale uniformly
580
+ return scale;
581
+ }
582
+ return maxImageWidth / width;
583
+ };
584
+ /**
585
+ * Preloads a data url as an image.
586
+ *
587
+ * @param {string} dataUrl - The data url to preload.
588
+ * @returns {Promise<HTMLImageElement>} A promise that resolves to the preloaded image.
589
+ */
590
+ const preload = (dataUrl) => {
591
+ return new Promise((resolve, reject) => {
592
+ const image = new Image();
593
+ image.src = dataUrl;
594
+ image.onload = () => resolve(image);
595
+ image.onerror = () => reject();
596
+ });
597
+ };
598
+ /**
599
+ * Extracts the type of an image from a data URL
600
+ *
601
+ * @param {string} dataURL the data URL to extract the type from.
602
+ * @returns {"jpg" | "png" | "svg"} the type of the encoded image.
603
+ * @throws
604
+ */
605
+ const getMimeTypeFromDataURL = (dataURL) => {
606
+ const mimeToTypeMap = {
607
+ "image/svg+xml": "svg",
608
+ "image/jpeg": "jpeg",
609
+ "image/jpg": "jpeg",
610
+ "image/png": "png",
611
+ };
612
+ const matches = dataURL.match(/^data:(.*?);base64,/);
613
+ if (!matches || !matches[1]) {
614
+ throw new Error("Invalid file");
615
+ }
616
+ const type = mimeToTypeMap[matches[1]];
617
+ if (!type) {
618
+ throw new Error("File is not a supported image");
619
+ }
620
+ return type;
621
+ };
622
+ /**
623
+ * Fetches and image from a URL and converts it to a base64 encoded data url
624
+ *
625
+ * @param {string} url - url pointing to an image resource
626
+ * @returns {Promise<string>} base64 encoded data url.
627
+ * @throws
628
+ */
629
+ const fetchImageAsBase64 = async (url) => {
630
+ // Fetch the image from the URL
631
+ const response = await fetch(url);
632
+ if (!response.ok) {
633
+ throw new Error(`Failed to fetch image. Status: ${response.status}`);
634
+ }
635
+ // Convert the response to a Blob
636
+ const blob = await response.blob();
637
+ // Convert the Blob to a Base64 string
638
+ return new Promise((resolve, reject) => {
639
+ const reader = new FileReader();
640
+ reader.onloadend = () => {
641
+ if (typeof reader.result === "string") {
642
+ const base64String = reader.result;
643
+ resolve(base64String);
644
+ }
645
+ reject(new Error("Failed to convert Blob to Base64."));
646
+ };
647
+ reader.onerror = () => {
648
+ reject(new Error("Failed to convert Blob to Base64."));
649
+ };
650
+ reader.readAsDataURL(blob);
651
+ });
652
+ };
653
+
532
654
  /**
533
655
  * Deletes all undefined keys from an object.
534
656
  *
@@ -980,6 +1102,137 @@ const stripHiddenCharacters = (input) => {
980
1102
  return input.replace(hiddenRegex, "");
981
1103
  };
982
1104
 
1105
+ /**
1106
+ * Convert the given value to HEX
1107
+ * If the value is not rgb, returns back the same value
1108
+ */
1109
+ const rgb2hex = (rgb) => {
1110
+ var _a;
1111
+ const hexColor = (_a = rgb
1112
+ .match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1).map(n => parseInt(n, 10).toString(16).padStart(2, "0")).join("");
1113
+ return hexColor ? `#${hexColor}` : rgb;
1114
+ };
1115
+ /**
1116
+ * Returns all svg related colors from a CSSStyleDeclaration
1117
+ */
1118
+ const colorsFromStyleDeclaration = (styles) => {
1119
+ return [styles.fill, styles.stroke, styles.stopColor].filter(color => color);
1120
+ };
1121
+ /**
1122
+ * Loads the dimensions of an SVG string.
1123
+ *
1124
+ * @param base64EncodedSVG The SVG string to load dimensions from.
1125
+ * @returns A promise that resolves to an object containing the width and height of the SVG.
1126
+ * @throws {Error} If the SVG string is invalid or the dimensions cannot be determined.
1127
+ */
1128
+ const loadSVGDimensions = (base64EncodedSVG) => {
1129
+ return new Promise((resolve, reject) => {
1130
+ // Create a temporary SVG element
1131
+ const svgAsString = atob(base64EncodedSVG.replace("data:image/svg+xml;base64,", ""));
1132
+ const parser = new DOMParser();
1133
+ const svgDoc = parser.parseFromString(svgAsString, "image/svg+xml");
1134
+ const svgElement = svgDoc.documentElement;
1135
+ // Check if parsing was successful
1136
+ if (svgElement.nodeName === "parsererror") {
1137
+ reject(new Error("Invalid SVG string"));
1138
+ return;
1139
+ }
1140
+ // Get dimensions
1141
+ const width = svgElement.width.baseVal.value;
1142
+ const height = svgElement.height.baseVal.value;
1143
+ // If width and height are not set explicitly, try to get from viewBox
1144
+ if (!width || !height) {
1145
+ const viewBox = svgElement.viewBox.baseVal;
1146
+ if (viewBox.width && viewBox.height) {
1147
+ resolve({
1148
+ width: viewBox.width,
1149
+ height: viewBox.height,
1150
+ });
1151
+ }
1152
+ else {
1153
+ reject(new Error("Unable to determine SVG dimensions"));
1154
+ }
1155
+ }
1156
+ else {
1157
+ resolve({
1158
+ width,
1159
+ height,
1160
+ });
1161
+ }
1162
+ });
1163
+ };
1164
+ /**
1165
+ * Returns an array of all colors used in an svg
1166
+ */
1167
+ const getAllColors = (base64EncodedSVG) => {
1168
+ const svgAsString = atob(base64EncodedSVG.replace("data:image/svg+xml;base64,", ""));
1169
+ const parser = new DOMParser();
1170
+ const svgImage = parser.parseFromString(svgAsString, "image/svg+xml");
1171
+ const colors = [];
1172
+ svgImage.querySelectorAll("[style]").forEach(element => {
1173
+ // Not all Elements have the style prop, this seems like a safe way to check and assert
1174
+ if (element instanceof HTMLElement) {
1175
+ const elementStyles = element.style;
1176
+ colors.push(...colorsFromStyleDeclaration(elementStyles));
1177
+ }
1178
+ });
1179
+ svgImage.querySelectorAll("style").forEach(element => {
1180
+ var _a;
1181
+ const cssRules = (_a = element.sheet) === null || _a === void 0 ? void 0 : _a.cssRules;
1182
+ if (cssRules === null || cssRules === void 0 ? void 0 : cssRules.length) {
1183
+ for (let i = 0; i < cssRules.length; i++) {
1184
+ // Checking if the style attribute exists
1185
+ const cssRule = cssRules[i]; // Replace type casting with type assertion
1186
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1187
+ if (cssRule.style) {
1188
+ const cssRuleStyles = cssRule.style;
1189
+ colors.push(...colorsFromStyleDeclaration(cssRuleStyles));
1190
+ }
1191
+ }
1192
+ }
1193
+ });
1194
+ // Get fills
1195
+ svgImage.querySelectorAll("[fill]").forEach(element => {
1196
+ var _a;
1197
+ const color = (_a = element.attributes.getNamedItem("fill")) === null || _a === void 0 ? void 0 : _a.value;
1198
+ if (color) {
1199
+ colors.push(color);
1200
+ }
1201
+ });
1202
+ // Get stokes
1203
+ svgImage.querySelectorAll("[stroke]").forEach(element => {
1204
+ var _a;
1205
+ const color = (_a = element.attributes.getNamedItem("stroke")) === null || _a === void 0 ? void 0 : _a.value;
1206
+ if (color) {
1207
+ colors.push(color);
1208
+ }
1209
+ });
1210
+ // Get stop-color (for gradients)
1211
+ svgImage.querySelectorAll("[stop-color]").forEach(element => {
1212
+ var _a;
1213
+ const color = (_a = element.attributes.getNamedItem("stop-color")) === null || _a === void 0 ? void 0 : _a.value;
1214
+ if (color) {
1215
+ colors.push(color);
1216
+ }
1217
+ });
1218
+ return [...new Set(colors.map(color => rgb2hex(color)))];
1219
+ };
1220
+ /**
1221
+ * Converts a Base64 encoded SVG to a Scaled Base64 PNG.
1222
+ *
1223
+ * @param {string} base64EncodedSVG The Base64 encoded svg string to convert
1224
+ * @param {number} maxWidth The max width(px) of the output PNG
1225
+ * @param {number} maxHeight The max height(px) of the output PNG
1226
+ * @returns {Promise<string>} A Base64 encoded PNG, scaled to the given parameters
1227
+ */
1228
+ const svgToPNG = async ({ base64EncodedSVG, maxWidth, maxHeight, idealArea, }) => {
1229
+ const [image, dimensions] = await Promise.all([
1230
+ preload(base64EncodedSVG),
1231
+ loadSVGDimensions(base64EncodedSVG),
1232
+ ]);
1233
+ return toPNG({ image, dimensions, maxHeight, maxWidth, idealArea });
1234
+ };
1235
+
983
1236
  /* eslint-disable local-rules/prefer-custom-object-keys */
984
1237
  /* eslint-disable local-rules/prefer-custom-object-from-entries */
985
1238
  /**
@@ -1057,4 +1310,4 @@ const uuidv4 = () => {
1057
1310
  */
1058
1311
  const uuidv5 = (name, namespace) => v5(name, namespace);
1059
1312
 
1060
- export { DateTimeFormat, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, capitalize, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, getDifferenceBetweenDates, getEndOfDay, getFirstLevelObjectPropertyDifferences, getISOStringFromDate, getResizedDimensions, getStartOfDay, groupBy, groupTinyDataToOthers, hourIntervals, intersection, isArrayEqual, isSorted, isUUID, isValidImage, nonNullable, numberCompare, numberCompareUnknownAfterHighest, objNotEmpty, objectEntries, objectFromEntries, objectKeys, objectValues, pick, removeLeftPadding, resizeBlob, resizeImage, size, stringCompare, stringCompareFromKey, stringNaturalCompare, stripHiddenCharacters, titleCase, toID, toIDs, toUUID, trimIds, trimPath, truthy, unionArraysByKey, uuidv3, uuidv4, uuidv5 };
1313
+ export { DateTimeFormat, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, calculateImageScaleRatio, capitalize, colorsFromStyleDeclaration, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, fetchImageAsBase64, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, getAllColors, getDifferenceBetweenDates, getEndOfDay, getFirstLevelObjectPropertyDifferences, getISOStringFromDate, getMimeTypeFromDataURL, getResizedDimensions, getStartOfDay, groupBy, groupTinyDataToOthers, hourIntervals, intersection, isArrayEqual, isSorted, isUUID, isValidImage, loadSVGDimensions, nonNullable, numberCompare, numberCompareUnknownAfterHighest, objNotEmpty, objectEntries, objectFromEntries, objectKeys, objectValues, pick, preload, removeLeftPadding, resizeBlob, resizeImage, rgb2hex, size, stringCompare, stringCompareFromKey, stringNaturalCompare, stripHiddenCharacters, svgToPNG, titleCase, toID, toIDs, toPNG, toUUID, trimIds, trimPath, truthy, unionArraysByKey, uuidv3, uuidv4, uuidv5 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/shared-utils",
3
- "version": "0.0.88",
3
+ "version": "0.0.89",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -0,0 +1,59 @@
1
+ export interface ImageDimensions {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /**
6
+ * Converts an HTMLImageElement to a PNG image.
7
+ *
8
+ * @param options - image options
9
+ * @param {HTMLImageElement} options.image - The image element to convert.
10
+ * @param {ImageDimensions} options.dimensions - The dimensions of the image.
11
+ * @param {number} options.maxHeight - The maximum height of the converted PNG image.
12
+ * @param {number} options.maxWidth - The maximum width of the converted PNG image.
13
+ * @param {number} options.idealArea - The ideal area of the converted PNG image.
14
+ * @returns The converted PNG image as a data URL.
15
+ * @throws Error if unable to get the canvas context.
16
+ */
17
+ export declare const toPNG: ({ image, dimensions, maxHeight, maxWidth, idealArea, }: {
18
+ image: HTMLImageElement;
19
+ dimensions?: ImageDimensions;
20
+ maxHeight: number;
21
+ maxWidth: number;
22
+ idealArea: number;
23
+ }) => string;
24
+ /**
25
+ * Calculates the scale ratio for an image to fit inside given bounds
26
+ * Result is based on the idealArea more info: https://nicksherman.com/size-by-area/
27
+ *
28
+ * @param {number} width The width of the image
29
+ * @param {number} height The height of the image
30
+ * @param {number} maxWidth The maxWidth of the image
31
+ * @param {number} maxHeight The maxHeight of the image
32
+ * @param {number} idealArea The ideal area for the image to cover (Read more above)
33
+ */
34
+ export declare const calculateImageScaleRatio: (width: number, height: number, maxWidth: number, maxHeight: number, idealArea: number) => number;
35
+ /**
36
+ * Preloads a data url as an image.
37
+ *
38
+ * @param {string} dataUrl - The data url to preload.
39
+ * @returns {Promise<HTMLImageElement>} A promise that resolves to the preloaded image.
40
+ */
41
+ export declare const preload: (dataUrl: string) => Promise<HTMLImageElement>;
42
+ type ImageType = "jpeg" | "png" | "svg";
43
+ /**
44
+ * Extracts the type of an image from a data URL
45
+ *
46
+ * @param {string} dataURL the data URL to extract the type from.
47
+ * @returns {"jpg" | "png" | "svg"} the type of the encoded image.
48
+ * @throws
49
+ */
50
+ export declare const getMimeTypeFromDataURL: (dataURL: string) => ImageType;
51
+ /**
52
+ * Fetches and image from a URL and converts it to a base64 encoded data url
53
+ *
54
+ * @param {string} url - url pointing to an image resource
55
+ * @returns {Promise<string>} base64 encoded data url.
56
+ * @throws
57
+ */
58
+ export declare const fetchImageAsBase64: (url: string) => Promise<string>;
59
+ export {};
package/src/index.d.ts CHANGED
@@ -12,12 +12,14 @@ export * from "./filter";
12
12
  export * from "./groupBy/groupBy";
13
13
  export * from "./GroupingUtility";
14
14
  export * from "./idUtils";
15
+ export * from "./imageTools";
15
16
  export * from "./Maybe";
16
17
  export * from "./objectUtils";
17
18
  export * from "./pathUtils";
18
19
  export * from "./pictureUtils/pictureUtils";
19
20
  export * from "./sorting/sorting";
20
21
  export * from "./stringUtils";
22
+ export * from "./svgTools";
21
23
  export * from "./translationUtils";
22
24
  export * from "./typeUtils";
23
25
  export * from "./UnitOfMeasurementConverter";
@@ -0,0 +1,36 @@
1
+ import { type ImageDimensions } from "./imageTools";
2
+ /**
3
+ * Convert the given value to HEX
4
+ * If the value is not rgb, returns back the same value
5
+ */
6
+ export declare const rgb2hex: (rgb: string) => string;
7
+ /**
8
+ * Returns all svg related colors from a CSSStyleDeclaration
9
+ */
10
+ export declare const colorsFromStyleDeclaration: (styles: CSSStyleDeclaration) => string[];
11
+ /**
12
+ * Loads the dimensions of an SVG string.
13
+ *
14
+ * @param base64EncodedSVG The SVG string to load dimensions from.
15
+ * @returns A promise that resolves to an object containing the width and height of the SVG.
16
+ * @throws {Error} If the SVG string is invalid or the dimensions cannot be determined.
17
+ */
18
+ export declare const loadSVGDimensions: (base64EncodedSVG: string) => Promise<ImageDimensions>;
19
+ /**
20
+ * Returns an array of all colors used in an svg
21
+ */
22
+ export declare const getAllColors: (base64EncodedSVG: string) => string[];
23
+ /**
24
+ * Converts a Base64 encoded SVG to a Scaled Base64 PNG.
25
+ *
26
+ * @param {string} base64EncodedSVG The Base64 encoded svg string to convert
27
+ * @param {number} maxWidth The max width(px) of the output PNG
28
+ * @param {number} maxHeight The max height(px) of the output PNG
29
+ * @returns {Promise<string>} A Base64 encoded PNG, scaled to the given parameters
30
+ */
31
+ export declare const svgToPNG: ({ base64EncodedSVG, maxWidth, maxHeight, idealArea, }: {
32
+ base64EncodedSVG: string;
33
+ maxWidth: number;
34
+ maxHeight: number;
35
+ idealArea: number;
36
+ }) => Promise<string>;