react-shopwave-connect 0.4.4 → 0.4.5
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 +26 -1
- package/dist/core/index.cjs +215 -25
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +102 -5
- package/dist/core/index.d.ts +102 -5
- package/dist/core/index.js +210 -26
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +31 -25
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +31 -25
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +215 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -5
- package/dist/index.d.ts +102 -5
- package/dist/index.js +210 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -662,6 +662,35 @@ async function readImageSize(file) {
|
|
|
662
662
|
bitmap.close?.();
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
|
+
/** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
|
|
666
|
+
async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
|
|
667
|
+
const { sx, sy, sw, sh } = crop;
|
|
668
|
+
if (typeof OffscreenCanvas === "function") {
|
|
669
|
+
const canvas$1 = new OffscreenCanvas(width, height);
|
|
670
|
+
const ctx$1 = canvas$1.getContext("2d");
|
|
671
|
+
if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
|
|
672
|
+
ctx$1.imageSmoothingQuality = "high";
|
|
673
|
+
ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
674
|
+
return canvas$1.convertToBlob({
|
|
675
|
+
type,
|
|
676
|
+
quality
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const canvas = document.createElement("canvas");
|
|
680
|
+
canvas.width = width;
|
|
681
|
+
canvas.height = height;
|
|
682
|
+
const ctx = canvas.getContext("2d");
|
|
683
|
+
if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
|
|
684
|
+
ctx.imageSmoothingQuality = "high";
|
|
685
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
686
|
+
return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
|
|
687
|
+
}
|
|
688
|
+
/** The input's name with the output type's extension. */
|
|
689
|
+
function outputName(file, type) {
|
|
690
|
+
const name = file.name || "image";
|
|
691
|
+
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
692
|
+
return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
693
|
+
}
|
|
665
694
|
/**
|
|
666
695
|
* Returns the image at exactly `width × height`: scaled and centre-cropped
|
|
667
696
|
* (never stretched). Browser only. The result is a `File` named like the
|
|
@@ -670,39 +699,194 @@ async function readImageSize(file) {
|
|
|
670
699
|
async function fitImageToSize(file, width, height, options = {}) {
|
|
671
700
|
requireBrowserImages("fitImageToSize");
|
|
672
701
|
const { type = "image/png", quality, keepIfExact = true } = options;
|
|
673
|
-
const
|
|
674
|
-
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
675
|
-
const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
702
|
+
const outName = outputName(file, type);
|
|
676
703
|
const bitmap = await createImageBitmap(file);
|
|
677
704
|
try {
|
|
678
705
|
if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
|
|
679
|
-
const
|
|
680
|
-
let blob;
|
|
681
|
-
if (typeof OffscreenCanvas === "function") {
|
|
682
|
-
const canvas = new OffscreenCanvas(width, height);
|
|
683
|
-
const ctx = canvas.getContext("2d");
|
|
684
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
685
|
-
ctx.imageSmoothingQuality = "high";
|
|
686
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
687
|
-
blob = await canvas.convertToBlob({
|
|
688
|
-
type,
|
|
689
|
-
quality
|
|
690
|
-
});
|
|
691
|
-
} else {
|
|
692
|
-
const canvas = document.createElement("canvas");
|
|
693
|
-
canvas.width = width;
|
|
694
|
-
canvas.height = height;
|
|
695
|
-
const ctx = canvas.getContext("2d");
|
|
696
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
697
|
-
ctx.imageSmoothingQuality = "high";
|
|
698
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
699
|
-
blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
|
|
700
|
-
}
|
|
706
|
+
const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
|
|
701
707
|
return new File([blob], outName, { type });
|
|
702
708
|
} finally {
|
|
703
709
|
bitmap.close?.();
|
|
704
710
|
}
|
|
705
711
|
}
|
|
712
|
+
/** A rule that can't be met by any image (a mistake in the rule, not the image). */
|
|
713
|
+
var ImageRuleError = class extends Error {
|
|
714
|
+
constructor(message) {
|
|
715
|
+
super(message);
|
|
716
|
+
this.name = "ImageRuleError";
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
/** The image is smaller than the rule's minimum (and `allowUpscale` is off). */
|
|
720
|
+
var ImageTooSmallError = class extends Error {
|
|
721
|
+
/** The smallest size the rule accepts. */
|
|
722
|
+
required;
|
|
723
|
+
/** The largest area of the right shape the image could give. */
|
|
724
|
+
actual;
|
|
725
|
+
constructor(required, actual) {
|
|
726
|
+
super(`The image is too small: it needs to be at least ${required.width} × ${required.height}, and it's ${actual.width} × ${actual.height}`);
|
|
727
|
+
this.name = "ImageTooSmallError";
|
|
728
|
+
this.required = required;
|
|
729
|
+
this.actual = actual;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const isPositive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
733
|
+
/**
|
|
734
|
+
* An aspect ratio as width ÷ height: `"100:77"`, `"100/77"`, `[100, 77]` →
|
|
735
|
+
* 1.2987…; `1.3` → 1.3. Throws {@link ImageRuleError} for anything else.
|
|
736
|
+
*/
|
|
737
|
+
function parseAspectRatio(value) {
|
|
738
|
+
let parts = null;
|
|
739
|
+
if (typeof value === "number") return isPositive(value) ? value : badRatio(value);
|
|
740
|
+
if (Array.isArray(value)) parts = [value[0], value[1]];
|
|
741
|
+
else if (typeof value === "string") {
|
|
742
|
+
const text$1 = value.trim();
|
|
743
|
+
const match = /^(\d+(?:\.\d+)?)\s*[:/x×]\s*(\d+(?:\.\d+)?)$/i.exec(text$1);
|
|
744
|
+
if (match) parts = [Number(match[1]), Number(match[2])];
|
|
745
|
+
else if (/^\d+(?:\.\d+)?$/.test(text$1)) return isPositive(Number(text$1)) ? Number(text$1) : badRatio(value);
|
|
746
|
+
}
|
|
747
|
+
if (parts && isPositive(parts[0]) && isPositive(parts[1])) return parts[0] / parts[1];
|
|
748
|
+
return badRatio(value);
|
|
749
|
+
}
|
|
750
|
+
function badRatio(value) {
|
|
751
|
+
throw new ImageRuleError(`Not an aspect ratio: ${JSON.stringify(value)} (use e.g. "100:77", [100, 77] or 1.3)`);
|
|
752
|
+
}
|
|
753
|
+
/** Throws {@link ImageRuleError} when the rule contradicts itself. */
|
|
754
|
+
function checkImageRule(rule) {
|
|
755
|
+
if (!rule || typeof rule !== "object") throw new ImageRuleError("An image rule must be an object");
|
|
756
|
+
for (const key of [
|
|
757
|
+
"width",
|
|
758
|
+
"height",
|
|
759
|
+
"minWidth",
|
|
760
|
+
"minHeight",
|
|
761
|
+
"maxWidth",
|
|
762
|
+
"maxHeight",
|
|
763
|
+
"maxLongSide"
|
|
764
|
+
]) if (rule[key] != null && !isPositive(rule[key])) throw new ImageRuleError(`${key} must be a positive number`);
|
|
765
|
+
if (rule.width != null || rule.height != null) {
|
|
766
|
+
if (rule.width == null || rule.height == null) throw new ImageRuleError("An exact size needs both width and height");
|
|
767
|
+
if (rule.aspectRatio != null) throw new ImageRuleError("Give an exact size or an aspectRatio, not both (the size sets the ratio)");
|
|
768
|
+
const extra = [
|
|
769
|
+
"minWidth",
|
|
770
|
+
"minHeight",
|
|
771
|
+
"maxWidth",
|
|
772
|
+
"maxHeight",
|
|
773
|
+
"maxLongSide"
|
|
774
|
+
].filter((key) => rule[key] != null);
|
|
775
|
+
if (extra.length > 0) throw new ImageRuleError(`An exact size is already the minimum and the maximum; remove ${extra.join(", ")}`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (rule.aspectRatio != null) parseAspectRatio(rule.aspectRatio);
|
|
779
|
+
if (rule.minWidth != null && rule.maxWidth != null && rule.minWidth > rule.maxWidth) throw new ImageRuleError("minWidth is larger than maxWidth");
|
|
780
|
+
if (rule.minHeight != null && rule.maxHeight != null && rule.minHeight > rule.maxHeight) throw new ImageRuleError("minHeight is larger than maxHeight");
|
|
781
|
+
if (rule.maxLongSide != null && Math.max(rule.minWidth ?? 0, rule.minHeight ?? 0) > rule.maxLongSide) throw new ImageRuleError("The minimum size is larger than maxLongSide");
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Works out what to do to a `source`-sized image to meet `rule`: the centred
|
|
785
|
+
* crop of the right shape (the largest one), then the output size (scaled
|
|
786
|
+
* down to the maximum, never below the minimum). Pure: no browser needed.
|
|
787
|
+
*
|
|
788
|
+
* Throws {@link ImageTooSmallError} when the crop is below the minimum (or
|
|
789
|
+
* the exact size) and `allowUpscale` is off, and {@link ImageRuleError} for a
|
|
790
|
+
* contradictory rule.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* planImageFit({ width: 6000, height: 4000 }, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 })
|
|
794
|
+
* // → crop { sx: 403, sy: 0, sw: 5194, sh: 4000 }, 2000 × 1540
|
|
795
|
+
*/
|
|
796
|
+
function planImageFit(source, rule) {
|
|
797
|
+
checkImageRule(rule);
|
|
798
|
+
const { width: W, height: H } = source ?? {};
|
|
799
|
+
if (!isPositive(W) || !isPositive(H)) throw new Error("planImageFit: the image has no size");
|
|
800
|
+
const exact = rule.width != null && rule.height != null;
|
|
801
|
+
const ratio = exact ? rule.width / rule.height : rule.aspectRatio != null ? parseAspectRatio(rule.aspectRatio) : null;
|
|
802
|
+
const crop = ratio == null ? {
|
|
803
|
+
sx: 0,
|
|
804
|
+
sy: 0,
|
|
805
|
+
sw: W,
|
|
806
|
+
sh: H
|
|
807
|
+
} : coverCrop(W, H, ratio, 1);
|
|
808
|
+
const { sw, sh } = crop;
|
|
809
|
+
const kept = {
|
|
810
|
+
width: sw,
|
|
811
|
+
height: sh
|
|
812
|
+
};
|
|
813
|
+
let width;
|
|
814
|
+
let height;
|
|
815
|
+
if (exact) {
|
|
816
|
+
width = rule.width;
|
|
817
|
+
height = rule.height;
|
|
818
|
+
if ((sw < width || sh < height) && !rule.allowUpscale) throw new ImageTooSmallError({
|
|
819
|
+
width,
|
|
820
|
+
height
|
|
821
|
+
}, kept);
|
|
822
|
+
} else {
|
|
823
|
+
const minW = rule.minWidth ?? 0;
|
|
824
|
+
const minH = rule.minHeight ?? 0;
|
|
825
|
+
if ((sw < minW || sh < minH) && !rule.allowUpscale) throw new ImageTooSmallError(requiredSize(minW, minH, ratio), kept);
|
|
826
|
+
let scale = 1;
|
|
827
|
+
if (rule.maxWidth != null) scale = Math.min(scale, rule.maxWidth / sw);
|
|
828
|
+
if (rule.maxHeight != null) scale = Math.min(scale, rule.maxHeight / sh);
|
|
829
|
+
if (rule.maxLongSide != null) scale = Math.min(scale, rule.maxLongSide / Math.max(sw, sh));
|
|
830
|
+
scale = Math.max(scale, minW / sw, minH / sh);
|
|
831
|
+
width = Math.max(1, Math.round(sw * scale));
|
|
832
|
+
height = Math.max(1, Math.round(sh * scale));
|
|
833
|
+
}
|
|
834
|
+
const unchanged = crop.sx === 0 && crop.sy === 0 && sw === W && sh === H && width === W && height === H;
|
|
835
|
+
return {
|
|
836
|
+
crop,
|
|
837
|
+
width,
|
|
838
|
+
height,
|
|
839
|
+
unchanged
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/** The minimum as a full size: a ratio rule with only one minimum implies the other. */
|
|
843
|
+
function requiredSize(minW, minH, ratio) {
|
|
844
|
+
if (ratio == null) return {
|
|
845
|
+
width: minW,
|
|
846
|
+
height: minH
|
|
847
|
+
};
|
|
848
|
+
return {
|
|
849
|
+
width: Math.max(minW, Math.ceil(minH * ratio)),
|
|
850
|
+
height: Math.max(minH, Math.ceil(minW / ratio))
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const OUTPUT_TYPES = [
|
|
854
|
+
"image/png",
|
|
855
|
+
"image/jpeg",
|
|
856
|
+
"image/webp"
|
|
857
|
+
];
|
|
858
|
+
/**
|
|
859
|
+
* Returns the image meeting `rule` ({@link planImageFit}): centre-cropped to
|
|
860
|
+
* the right shape and scaled in a single draw. An image that already fits is
|
|
861
|
+
* returned untouched (same bytes). Browser only.
|
|
862
|
+
*
|
|
863
|
+
* Throws {@link ImageTooSmallError} (show `required` and `actual` to the
|
|
864
|
+
* person) or {@link ImageRuleError}; an unreadable file rejects with the
|
|
865
|
+
* browser's decoding error.
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* const ready = await prepareImage(file, { aspectRatio: "100:77", minWidth: 600, minHeight: 462, maxLongSide: 2000 });
|
|
869
|
+
* await uploadImage(ready, { kind: "product", prefix: "product" });
|
|
870
|
+
*/
|
|
871
|
+
async function prepareImage(file, rule, options = {}) {
|
|
872
|
+
checkImageRule(rule);
|
|
873
|
+
requireBrowserImages("prepareImage");
|
|
874
|
+
const sourceType = file.type && OUTPUT_TYPES.includes(file.type) ? file.type : "image/png";
|
|
875
|
+
const type = options.type ?? sourceType;
|
|
876
|
+
const quality = options.quality ?? (type === "image/png" ? void 0 : .9);
|
|
877
|
+
const bitmap = await createImageBitmap(file);
|
|
878
|
+
try {
|
|
879
|
+
const plan = planImageFit({
|
|
880
|
+
width: bitmap.width,
|
|
881
|
+
height: bitmap.height
|
|
882
|
+
}, rule);
|
|
883
|
+
if (plan.unchanged && type === file.type) return file instanceof File ? file : new File([file], outputName(file, type), { type });
|
|
884
|
+
const blob = await drawCrop(bitmap, plan.crop, plan.width, plan.height, type, quality, "prepareImage");
|
|
885
|
+
return new File([blob], outputName(file, type), { type });
|
|
886
|
+
} finally {
|
|
887
|
+
bitmap.close?.();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
706
890
|
|
|
707
891
|
//#endregion
|
|
708
892
|
//#region src/core/upload.ts
|
|
@@ -1997,6 +2181,8 @@ function useBasketReport(options) {
|
|
|
1997
2181
|
//#endregion
|
|
1998
2182
|
exports.DEFAULT_BASKET_FILTERS = DEFAULT_BASKET_FILTERS;
|
|
1999
2183
|
exports.DEFAULT_PAGINATION = DEFAULT_PAGINATION;
|
|
2184
|
+
exports.ImageRuleError = ImageRuleError;
|
|
2185
|
+
exports.ImageTooSmallError = ImageTooSmallError;
|
|
2000
2186
|
exports.MERCHANT_COLOUR_KEYS = MERCHANT_COLOUR_KEYS;
|
|
2001
2187
|
exports.MERCHANT_FIELD_MAX_LENGTH = MERCHANT_FIELD_MAX_LENGTH;
|
|
2002
2188
|
exports.MERCHANT_IMAGE_SLOTS = MERCHANT_IMAGE_SLOTS;
|
|
@@ -2022,6 +2208,7 @@ exports.assertNoApiErrors = assertNoApiErrors;
|
|
|
2022
2208
|
exports.authorizationFor = authorizationFor;
|
|
2023
2209
|
exports.buildBasketReportQuery = buildBasketReportQuery;
|
|
2024
2210
|
exports.buildConsumerMap = buildConsumerMap;
|
|
2211
|
+
exports.checkImageRule = checkImageRule;
|
|
2025
2212
|
exports.collectConsumerIds = collectConsumerIds;
|
|
2026
2213
|
exports.collectProductIds = collectProductIds;
|
|
2027
2214
|
exports.combineBasketRows = combineBasketRows;
|
|
@@ -2073,7 +2260,10 @@ exports.merchantLinksFromForm = merchantLinksFromForm;
|
|
|
2073
2260
|
exports.merchantLinksToForm = merchantLinksToForm;
|
|
2074
2261
|
exports.normalizeMerchantLinks = normalizeMerchantLinks;
|
|
2075
2262
|
exports.normalizeTagList = normalizeTagList;
|
|
2263
|
+
exports.parseAspectRatio = parseAspectRatio;
|
|
2076
2264
|
exports.parseBasketReportData = parseBasketReportData;
|
|
2265
|
+
exports.planImageFit = planImageFit;
|
|
2266
|
+
exports.prepareImage = prepareImage;
|
|
2077
2267
|
exports.productImageRole = productImageRole;
|
|
2078
2268
|
exports.readImageSize = readImageSize;
|
|
2079
2269
|
exports.rolesById = rolesById;
|