PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | build/scripts/upload-media/index.js +1467 -143 23.2.2 → trunk View file →
@@ -1,4 +1,5 @@
1 +(function() {
1 2 "use strict";
2 3 var wp;
3 4 (wp ||= {}).uploadMedia = (() => {
4 5 var __create = Object.create;
@@ -44,8 +45,15 @@
44 45 module.exports = window.wp.data;
45 46 }
46 47 });
47 48
49 + // package-external:@wordpress/i18n
50 + var require_i18n = __commonJS({
51 + "package-external:@wordpress/i18n"(exports, module) {
52 + module.exports = window.wp.i18n;
53 + }
54 + });
55 +
48 56 // package-external:@wordpress/url
49 57 var require_url = __commonJS({
50 58 "package-external:@wordpress/url"(exports, module) {
51 59 module.exports = window.wp.url;
@@ -51,15 +59,8 @@
51 59 module.exports = window.wp.url;
52 60 }
53 61 });
54 62
55 - // package-external:@wordpress/i18n
56 - var require_i18n = __commonJS({
57 - "package-external:@wordpress/i18n"(exports, module) {
58 - module.exports = window.wp.i18n;
59 - }
60 - });
61 -
62 63 // package-external:@wordpress/blob
63 64 var require_blob = __commonJS({
64 65 "package-external:@wordpress/blob"(exports, module) {
65 66 module.exports = window.wp.blob;
@@ -96,14 +97,19 @@
96 97
97 98 // packages/upload-media/build-module/index.mjs
98 99 var index_exports = {};
99 100 __export(index_exports, {
101 + ErrorCode: () => ErrorCode,
100 102 MediaUploadProvider: () => provider_default,
101 103 UploadError: () => UploadError,
102 104 clearFeatureDetectionCache: () => clearFeatureDetectionCache,
103 105 detectClientSideMediaSupport: () => detectClientSideMediaSupport,
106 + getErrorMessage: () => getErrorMessage,
107 + getHeicConversionAdvice: () => getHeicConversionAdvice,
108 + getHeicUnsupportedMessage: () => getHeicUnsupportedMessage,
104 109 isClientSideMediaSupported: () => isClientSideMediaSupported,
105 110 isHeicCanvasSupported: () => isHeicCanvasSupported,
111 + isHeicFile: () => isHeicFile,
106 112 store: () => store
107 113 });
108 114
109 115 // packages/upload-media/build-module/store/index.mjs
@@ -116,8 +122,9 @@
116 122 Type2["Prepare"] = "PREPARE_ITEM";
117 123 Type2["Cancel"] = "CANCEL_ITEM";
118 124 Type2["Remove"] = "REMOVE_ITEM";
119 125 Type2["RetryItem"] = "RETRY_ITEM";
126 + Type2["ScheduleRetry"] = "SCHEDULE_RETRY";
120 127 Type2["PauseItem"] = "PAUSE_ITEM";
121 128 Type2["ResumeItem"] = "RESUME_ITEM";
122 129 Type2["PauseQueue"] = "PAUSE_QUEUE";
123 130 Type2["ResumeQueue"] = "RESUME_QUEUE";
@@ -134,8 +141,9 @@
134 141 var ItemStatus = /* @__PURE__ */ ((ItemStatus2) => {
135 142 ItemStatus2["Queued"] = "QUEUED";
136 143 ItemStatus2["Processing"] = "PROCESSING";
137 144 ItemStatus2["Paused"] = "PAUSED";
145 + ItemStatus2["PendingRetry"] = "PENDING_RETRY";
138 146 ItemStatus2["Uploaded"] = "UPLOADED";
139 147 ItemStatus2["Error"] = "ERROR";
140 148 return ItemStatus2;
141 149 })(ItemStatus || {});
@@ -144,10 +152,12 @@
144 152 OperationType2["Upload"] = "UPLOAD";
145 153 OperationType2["ResizeCrop"] = "RESIZE_CROP";
146 154 OperationType2["Rotate"] = "ROTATE";
147 155 OperationType2["TranscodeImage"] = "TRANSCODE_IMAGE";
156 + OperationType2["TranscodeGif"] = "TRANSCODE_GIF";
148 157 OperationType2["ThumbnailGeneration"] = "THUMBNAIL_GENERATION";
149 158 OperationType2["Finalize"] = "FINALIZE";
159 + OperationType2["DetectUltraHdr"] = "DETECT_ULTRAHDR";
150 160 return OperationType2;
151 161 })(OperationType || {});
152 162
153 163 // packages/upload-media/build-module/store/constants.mjs
@@ -153,8 +163,15 @@
153 163 // packages/upload-media/build-module/store/constants.mjs
154 164 var STORE_NAME = "core/upload-media";
155 165 var DEFAULT_MAX_CONCURRENT_UPLOADS = 5;
156 166 var DEFAULT_MAX_CONCURRENT_IMAGE_PROCESSING = 2;
167 + var DEFAULT_RETRY_SETTINGS = {
168 + maxRetryAttempts: 3,
169 + initialRetryDelayMs: 1e3,
170 + maxRetryDelayMs: 3e4,
171 + backoffMultiplier: 2,
172 + retryJitter: 0.1
173 + };
157 174 var CLIENT_SIDE_SUPPORTED_MIME_TYPES = [
158 175 "image/jpeg",
159 176 "image/png",
160 177 "image/gif",
@@ -175,10 +192,12 @@
175 192 blobUrls: {},
176 193 settings: {
177 194 mediaUpload: noop,
178 195 maxConcurrentUploads: DEFAULT_MAX_CONCURRENT_UPLOADS,
179 - maxConcurrentImageProcessing: DEFAULT_MAX_CONCURRENT_IMAGE_PROCESSING
180 - }
196 + maxConcurrentImageProcessing: DEFAULT_MAX_CONCURRENT_IMAGE_PROCESSING,
197 + retry: { ...DEFAULT_RETRY_SETTINGS }
198 + },
199 + failureCount: 0
181 200 };
182 201 function reducer(state = DEFAULT_STATE, action = { type: Type.Unknown }) {
183 202 switch (action.type) {
184 203 case Type.PauseQueue: {
@@ -217,11 +236,15 @@
217 236 return {
218 237 ...state,
219 238 queue: [...state.queue, action.item]
220 239 };
221 - case Type.Cancel:
240 + case Type.Cancel: {
241 + const cancelled = state.queue.find(
242 + (item) => item.id === action.id
243 + );
222 244 return {
223 245 ...state,
246 + failureCount: cancelled && !cancelled.parentId ? state.failureCount + 1 : state.failureCount,
224 247 queue: state.queue.map(
225 248 (item) => item.id === action.id ? {
226 249 ...item,
227 250 error: action.error
@@ -227,8 +250,9 @@
227 250 error: action.error
228 251 } : item
229 252 )
230 253 };
254 + }
231 255 case Type.RetryItem:
232 256 return {
233 257 ...state,
234 258 queue: state.queue.map(
@@ -235,12 +259,37 @@
235 259 (item) => item.id === action.id ? {
236 260 ...item,
237 261 status: ItemStatus.Processing,
238 262 error: void 0,
239 - retryCount: (item.retryCount ?? 0) + 1
263 + retryCount: (item.retryCount ?? 0) + 1,
264 + abortController: new AbortController(),
265 + /*
266 + * The failed operation is still recorded on
267 + * the item: nothing finishes it when it
268 + * fails, and while the item waits out the
269 + * backoff that is what keeps it out of the
270 + * concurrency pools. Clear it now that the
271 + * item is about to run again, so processItem
272 + * does not mistake it for an operation still
273 + * in flight and skip the retry.
274 + */
275 + currentOperation: void 0
240 276 } : item
241 277 )
242 278 };
279 + case Type.ScheduleRetry:
280 + return {
281 + ...state,
282 + queue: state.queue.map(
283 + (item) => item.id === action.id ? {
284 + ...item,
285 + status: ItemStatus.PendingRetry,
286 + error: action.error,
287 + retryCount: action.retryCount,
288 + nextRetryTimestamp: action.nextRetryTimestamp
289 + } : item
290 + )
291 + };
243 292 case Type.Remove:
244 293 return {
245 294 ...state,
246 295 queue: state.queue.filter((item) => item.id !== action.id)
@@ -385,15 +434,18 @@
385 434 var private_selectors_exports = {};
386 435 __export(private_selectors_exports, {
387 436 getActiveImageProcessingCount: () => getActiveImageProcessingCount,
388 437 getActiveUploadCount: () => getActiveUploadCount,
438 + getActiveVideoProcessingCount: () => getActiveVideoProcessingCount,
389 439 getAllItems: () => getAllItems,
390 440 getBlobUrls: () => getBlobUrls,
391 441 getFailedItems: () => getFailedItems,
442 + getFailureCount: () => getFailureCount,
392 443 getItem: () => getItem,
393 444 getItemProgress: () => getItemProgress,
394 445 getPendingImageProcessing: () => getPendingImageProcessing,
395 446 getPendingUploads: () => getPendingUploads,
447 + getPendingVideoProcessing: () => getPendingVideoProcessing,
396 448 hasPendingItemsByParentId: () => hasPendingItemsByParentId,
397 449 isBatchUploaded: () => isBatchUploaded,
398 450 isPaused: () => isPaused
399 451 });
@@ -399,8 +451,11 @@
399 451 });
400 452 function getAllItems(state) {
401 453 return state.queue;
402 454 }
455 + function getFailureCount(state) {
456 + return state.failureCount;
457 + }
403 458 function getItem(state, id) {
404 459 return state.queue.find((item) => item.id === id);
405 460 }
406 461 function isBatchUploaded(state, batchId) {
@@ -430,8 +485,13 @@
430 485 return state.queue.filter(
431 486 (item) => item.currentOperation === OperationType.ResizeCrop || item.currentOperation === OperationType.Rotate
432 487 ).length;
433 488 }
489 + function getActiveVideoProcessingCount(state) {
490 + return state.queue.filter(
491 + (item) => item.currentOperation === OperationType.TranscodeGif
492 + ).length;
493 + }
434 494 function getPendingImageProcessing(state) {
435 495 return state.queue.filter((item) => {
436 496 const nextOperation = Array.isArray(item.operations?.[0]) ? item.operations[0][0] : item.operations?.[0];
437 497 return (nextOperation === OperationType.ResizeCrop || nextOperation === OperationType.Rotate) && item.currentOperation !== OperationType.ResizeCrop && item.currentOperation !== OperationType.Rotate;
@@ -436,8 +496,14 @@
436 496 const nextOperation = Array.isArray(item.operations?.[0]) ? item.operations[0][0] : item.operations?.[0];
437 497 return (nextOperation === OperationType.ResizeCrop || nextOperation === OperationType.Rotate) && item.currentOperation !== OperationType.ResizeCrop && item.currentOperation !== OperationType.Rotate;
438 498 });
439 499 }
500 + function getPendingVideoProcessing(state) {
501 + return state.queue.filter((item) => {
502 + const nextOperation = Array.isArray(item.operations?.[0]) ? item.operations[0][0] : item.operations?.[0];
503 + return nextOperation === OperationType.TranscodeGif && item.currentOperation !== OperationType.TranscodeGif;
504 + });
505 + }
440 506 function getFailedItems(state) {
441 507 return state.queue.filter((item) => item.error !== void 0);
442 508 }
443 509 function hasPendingItemsByParentId(state, parentId) {
@@ -452,9 +518,11 @@
452 518 var actions_exports = {};
453 519 __export(actions_exports, {
454 520 addItems: () => addItems,
455 521 cancelItem: () => cancelItem,
456 - retryItem: () => retryItem
522 + executeRetry: () => executeRetry,
523 + retryItem: () => retryItem,
524 + scheduleRetry: () => scheduleRetry
457 525 });
458 526
459 527 // node_modules/uuid/dist/stringify.js
460 528 var byteToHex = [];
@@ -499,8 +567,50 @@
499 567 return unsafeStringify(rnds);
500 568 }
501 569 var v4_default = v4;
502 570
571 + // packages/upload-media/build-module/store/actions.mjs
572 + var import_i18n5 = __toESM(require_i18n(), 1);
573 +
574 + // packages/upload-media/build-module/store/utils/retry.mjs
575 + function calculateRetryDelay(options) {
576 + const { attempt, initialDelay, maxDelay, multiplier, jitter } = options;
577 + const exponentialDelay = initialDelay * Math.pow(multiplier, attempt - 1);
578 + const cappedDelay = Math.min(exponentialDelay, maxDelay);
579 + const jitterFactor = 1 + (Math.random() * 2 - 1) * jitter;
580 + return Math.floor(cappedDelay * jitterFactor);
581 + }
582 + var RETRYABLE_MESSAGE_PATTERNS = [
583 + /network/i,
584 + /timeout/i,
585 + /ECONNRESET/i,
586 + /fetch failed/i,
587 + /connection/i,
588 + /socket/i,
589 + /ETIMEDOUT/i,
590 + /ENOTFOUND/i,
591 + /Could not get a valid response/i,
592 + /Failed to fetch/i,
593 + /Load failed/i
594 + ];
595 + function shouldRetryError(error, retryCount, maxRetries) {
596 + if (retryCount >= maxRetries) {
597 + return false;
598 + }
599 + const message = typeof error === "string" ? error : error?.message || "";
600 + return RETRYABLE_MESSAGE_PATTERNS.some(
601 + (pattern) => pattern.test(message)
602 + );
603 + }
604 + var retryTimers = /* @__PURE__ */ new Map();
605 + function clearRetryTimer(id) {
606 + const pendingTimer = retryTimers.get(id);
607 + if (pendingTimer !== void 0) {
608 + clearTimeout(pendingTimer);
609 + retryTimers.delete(id);
610 + }
611 + }
612 +
503 613 // packages/upload-media/build-module/image-file.mjs
504 614 var ImageFile = class extends File {
505 615 width = 0;
506 616 height = 0;
@@ -551,8 +661,72 @@
551 661 }
552 662 function getFileBasename(name) {
553 663 return name.includes(".") ? name.split(".").slice(0, -1).join(".") : name;
554 664 }
665 + function isAnimatedGif(buffer) {
666 + const view = new Uint8Array(buffer);
667 + if (view.length < 4 || view[0] !== 71 || view[1] !== 73 || view[2] !== 70 || view[3] !== 56) {
668 + return false;
669 + }
670 + let frameCount = 0;
671 + for (let i = 0; i < view.length - 2; i++) {
672 + if (view[i] === 0 && view[i + 1] === 33 && view[i + 2] === 249) {
673 + frameCount++;
674 + if (frameCount > 1) {
675 + return true;
676 + }
677 + }
678 + }
679 + return false;
680 + }
681 + var FILE_TYPE_BOX_BYTES = 64;
682 + var HEIC_BRANDS = [
683 + "heic",
684 + "heix",
685 + "heim",
686 + "heis",
687 + "hevc",
688 + "hevx",
689 + "hevm",
690 + "hevs",
691 + "mif1",
692 + "msf1"
693 + ];
694 + var AVIF_BRANDS = ["avif", "avis"];
695 + function isHeicBuffer(buffer) {
696 + const view = new Uint8Array(buffer);
697 + const readBrand = (offset) => String.fromCharCode(...view.subarray(offset, offset + 4));
698 + if (view.length < 12 || readBrand(4) !== "ftyp") {
699 + return false;
700 + }
701 + const boxEnd = Math.min(
702 + view.length,
703 + new DataView(buffer).getUint32(0)
704 + );
705 + const brands = [readBrand(8)];
706 + for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
707 + brands.push(readBrand(offset));
708 + }
709 + if (brands.some((brand) => AVIF_BRANDS.includes(brand))) {
710 + return false;
711 + }
712 + return brands.some((brand) => HEIC_BRANDS.includes(brand));
713 + }
714 + async function isHeicFile(file) {
715 + if (HEIC_MIME_TYPES.includes(file.type)) {
716 + return true;
717 + }
718 + if (file.type && !file.type.startsWith("image/")) {
719 + return false;
720 + }
721 + try {
722 + return isHeicBuffer(
723 + await file.slice(0, FILE_TYPE_BOX_BYTES).arrayBuffer()
724 + );
725 + } catch {
726 + return false;
727 + }
728 + }
555 729
556 730 // packages/upload-media/build-module/store/utils/index.mjs
557 731 var vipsModulePromise;
558 732 var vipsModule;
@@ -566,9 +740,9 @@
566 740 );
567 741 }
568 742 return vipsModulePromise;
569 743 }
570 - async function vipsConvertImageFormat(id, file, type, quality, interlaced) {
744 + async function vipsConvertImageFormat(id, file, type, options = {}) {
571 745 const { vipsConvertImageFormat: convertImageFormat } = await loadVipsModule();
572 746 const buffer = await convertImageFormat(
573 747 id,
574 748 await file.arrayBuffer(),
@@ -573,10 +747,9 @@
573 747 id,
574 748 await file.arrayBuffer(),
575 749 file.type,
576 750 type,
577 - quality,
578 - interlaced
751 + options
579 752 );
580 753 const ext = type.split("/")[1];
581 754 const fileName = `${getFileBasename(file.name)}.${ext}`;
582 755 return new File([new Blob([buffer])], fileName, {
@@ -590,21 +763,32 @@
590 763 throw new Error(`Failed to fetch image: ${response.status}`);
591 764 }
592 765 return hasTransparency(await response.arrayBuffer());
593 766 }
594 - async function vipsResizeImage(id, file, resize, smartCrop, addSuffix, signal, scaledSuffix, quality) {
767 + async function vipsGetUltraHdrInfo(buffer) {
768 + const { vipsGetUltraHdrInfo: getUltraHdrInfo } = await loadVipsModule();
769 + return getUltraHdrInfo(buffer);
770 + }
771 + async function vipsResizeImage(id, file, resize, options = {}) {
772 + const {
773 + smartCrop = false,
774 + addSuffix = false,
775 + signal,
776 + scaledSuffix,
777 + quality,
778 + stripMeta,
779 + maxBitdepth
780 + } = options;
595 781 if (signal?.aborted) {
596 782 throw new Error("Operation aborted");
597 783 }
598 784 const { vipsResizeImage: resizeImage } = await loadVipsModule();
599 - const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(
600 - id,
601 - await file.arrayBuffer(),
602 - file.type,
603 - resize,
785 + const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(id, await file.arrayBuffer(), file.type, resize, {
604 786 smartCrop,
605 - quality
606 - );
787 + quality,
788 + stripMeta,
789 + maxBitdepth
790 + });
607 791 let fileName = file.name;
608 792 const wasResized = originalWidth > width || originalHeight > height;
609 793 if (wasResized) {
610 794 const basename = getFileBasename(file.name);
@@ -671,13 +855,172 @@
671 855 if (vipsModule) {
672 856 vipsModule.terminateVipsWorker();
673 857 }
674 858 }
859 + var completedVipsOperations = 0;
860 + var MAX_VIPS_OPS_BEFORE_RECYCLE = 50;
861 + function maybeRecycleVipsWorker(activeImageProcessingCount) {
862 + completedVipsOperations++;
863 + if (completedVipsOperations >= MAX_VIPS_OPS_BEFORE_RECYCLE && activeImageProcessingCount === 0) {
864 + terminateVipsWorker();
865 + completedVipsOperations = 0;
866 + }
867 + }
675 868
676 - // packages/upload-media/build-module/validate-mime-type.mjs
677 - var import_i18n2 = __toESM(require_i18n(), 1);
869 + // packages/upload-media/build-module/store/utils/video-conversion.mjs
870 + var UNSUPPORTED_ERROR_PREFIX = "Unsupported";
871 + var SIZE_LIMIT_ERROR_PREFIX = `${UNSUPPORTED_ERROR_PREFIX}: GIF exceeds maximum conversion size`;
872 + var CONVERSION_TIMEOUT_ERROR_PREFIX = "GIF to video conversion timed out";
873 + var DEFAULT_CONVERSION_TIMEOUT = 3e4;
874 + function isUnsupportedConversionError(error) {
875 + return error instanceof Error && error.message.startsWith(UNSUPPORTED_ERROR_PREFIX);
876 + }
877 + function isSizeLimitConversionError(error) {
878 + return error instanceof Error && error.message.startsWith(SIZE_LIMIT_ERROR_PREFIX);
879 + }
880 + function isConversionTimeoutError(error) {
881 + return error instanceof Error && error.message.startsWith(CONVERSION_TIMEOUT_ERROR_PREFIX);
882 + }
883 + var videoConversionModulePromise;
884 + var videoConversionModule;
885 + function loadVideoConversionModule() {
886 + if (!videoConversionModulePromise) {
887 + videoConversionModulePromise = import("@wordpress/video-conversion/worker").then((mod) => {
888 + videoConversionModule = mod;
889 + return mod;
890 + }).catch((error) => {
891 + videoConversionModulePromise = void 0;
892 + throw error;
893 + });
894 + }
895 + return videoConversionModulePromise;
896 + }
897 + async function convertGifToVideo(id, file, outputMimeType, {
898 + maxDimensions,
899 + timeout = DEFAULT_CONVERSION_TIMEOUT,
900 + maxTotalPixels
901 + } = {}) {
902 + const mod = await loadVideoConversionModule();
903 + const conversion = mod.convertGifToVideo(
904 + id,
905 + file,
906 + outputMimeType,
907 + maxDimensions,
908 + maxTotalPixels
909 + );
910 + let buffer;
911 + if (timeout > 0) {
912 + let timer;
913 + try {
914 + buffer = await Promise.race([
915 + conversion,
916 + new Promise((_, reject) => {
917 + timer = setTimeout(() => {
918 + reject(
919 + new Error(
920 + `${CONVERSION_TIMEOUT_ERROR_PREFIX} after ${timeout}ms`
921 + )
922 + );
923 + }, timeout);
924 + })
925 + ]);
926 + } catch (error) {
927 + if (isConversionTimeoutError(error)) {
928 + conversion.catch(() => {
929 + });
930 + mod.cancelGifToVideoOperations(id).catch(() => {
931 + });
932 + }
933 + throw error;
934 + } finally {
935 + clearTimeout(timer);
936 + }
937 + } else {
938 + buffer = await conversion;
939 + }
940 + const ext = outputMimeType === "video/webm" ? "webm" : "mp4";
941 + const fileName = `${getFileBasename(file.name)}.${ext}`;
942 + return new File(
943 + [new Blob([buffer], { type: outputMimeType })],
944 + fileName,
945 + { type: outputMimeType }
946 + );
947 + }
948 + async function cancelGifToVideoOperations(id) {
949 + const mod = videoConversionModule ?? (videoConversionModulePromise ? await videoConversionModulePromise.catch(() => void 0) : void 0);
950 + if (!mod) {
951 + return false;
952 + }
953 + return mod.cancelGifToVideoOperations(id);
954 + }
955 + function terminateVideoConversionWorker() {
956 + if (videoConversionModule) {
957 + videoConversionModule.terminateVideoConversionWorker();
958 + return;
959 + }
960 + if (videoConversionModulePromise) {
961 + void videoConversionModulePromise.then((mod) => mod.terminateVideoConversionWorker()).catch(() => {
962 + });
963 + }
964 + }
678 965
966 + // packages/upload-media/build-module/store/utils/debug-logger.mjs
967 + function isDebugEnabled() {
968 + return true;
969 + }
970 + function debug(message, ...args) {
971 + if (!isDebugEnabled()) {
972 + return;
973 + }
974 + console.debug(`[upload-media] ${message}`, ...args);
975 + }
976 + function measure(options) {
977 + if (!isDebugEnabled()) {
978 + return;
979 + }
980 + const {
981 + measureName,
982 + startTime,
983 + endTime = performance.now(),
984 + tooltipText,
985 + properties
986 + } = options;
987 + const detail = {
988 + devtools: {
989 + dataType: "track-entry",
990 + track: "Upload Media",
991 + tooltipText,
992 + properties: properties?.map(([key, value]) => ({
993 + key,
994 + value
995 + }))
996 + }
997 + };
998 + try {
999 + performance.measure(measureName, {
1000 + start: startTime,
1001 + end: endTime,
1002 + detail
1003 + });
1004 + } catch {
1005 + }
1006 + }
1007 +
679 1008 // packages/upload-media/build-module/upload-error.mjs
1009 + var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
1010 + ErrorCode2["EMPTY_FILE"] = "EMPTY_FILE";
1011 + ErrorCode2["SIZE_ABOVE_LIMIT"] = "SIZE_ABOVE_LIMIT";
1012 + ErrorCode2["MIME_TYPE_NOT_SUPPORTED"] = "MIME_TYPE_NOT_SUPPORTED";
1013 + ErrorCode2["MIME_TYPE_NOT_ALLOWED_FOR_USER"] = "MIME_TYPE_NOT_ALLOWED_FOR_USER";
1014 + ErrorCode2["HEIC_DECODE_ERROR"] = "HEIC_DECODE_ERROR";
1015 + ErrorCode2["IMAGE_TRANSCODING_ERROR"] = "IMAGE_TRANSCODING_ERROR";
1016 + ErrorCode2["IMAGE_ROTATION_ERROR"] = "IMAGE_ROTATION_ERROR";
1017 + ErrorCode2["MEDIA_TRANSCODING_ERROR"] = "MEDIA_TRANSCODING_ERROR";
1018 + ErrorCode2["GIF_TRANSCODING_ERROR"] = "GIF_TRANSCODING_ERROR";
1019 + ErrorCode2["MEDIA_FINALIZE_ERROR"] = "MEDIA_FINALIZE_ERROR";
1020 + ErrorCode2["GENERAL"] = "GENERAL";
1021 + return ErrorCode2;
1022 + })(ErrorCode || {});
680 1023 var UploadError = class extends Error {
681 1024 code;
682 1025 file;
683 1026 constructor({ code, message, file, cause }) {
@@ -688,8 +1031,9 @@
688 1031 }
689 1032 };
690 1033
691 1034 // packages/upload-media/build-module/validate-mime-type.mjs
1035 + var import_i18n2 = __toESM(require_i18n(), 1);
692 1036 function validateMimeType(file, allowedTypes) {
693 1037 if (!allowedTypes) {
694 1038 return;
695 1039 }
@@ -700,9 +1044,9 @@
700 1044 return file.type.startsWith(`${allowedType}/`);
701 1045 });
702 1046 if (file.type && !isAllowedType) {
703 1047 throw new UploadError({
704 - code: "MIME_TYPE_NOT_SUPPORTED",
1048 + code: ErrorCode.MIME_TYPE_NOT_SUPPORTED,
705 1049 message: (0, import_i18n2.sprintf)(
706 1050 // translators: %s: file name.
707 1051 (0, import_i18n2.__)("%s: Sorry, this file type is not supported here."),
708 1052 file.name
@@ -744,9 +1088,9 @@
744 1088 file.type
745 1089 );
746 1090 if (file.type && !isAllowedMimeTypeForUser) {
747 1091 throw new UploadError({
748 - code: "MIME_TYPE_NOT_ALLOWED_FOR_USER",
1092 + code: ErrorCode.MIME_TYPE_NOT_ALLOWED_FOR_USER,
749 1093 message: (0, import_i18n3.sprintf)(
750 1094 // translators: %s: file name.
751 1095 (0, import_i18n3.__)(
752 1096 "%s: Sorry, you are not allowed to upload this file type."
@@ -762,9 +1106,9 @@
762 1106 var import_i18n4 = __toESM(require_i18n(), 1);
763 1107 function validateFileSize(file, maxUploadFileSize) {
764 1108 if (file.size <= 0) {
765 1109 throw new UploadError({
766 - code: "EMPTY_FILE",
1110 + code: ErrorCode.EMPTY_FILE,
767 1111 message: (0, import_i18n4.sprintf)(
768 1112 // translators: %s: file name.
769 1113 (0, import_i18n4.__)("%s: This file is empty."),
770 1114 file.name
@@ -773,9 +1117,9 @@
773 1117 });
774 1118 }
775 1119 if (maxUploadFileSize && file.size > maxUploadFileSize) {
776 1120 throw new UploadError({
777 - code: "SIZE_ABOVE_LIMIT",
1121 + code: ErrorCode.SIZE_ABOVE_LIMIT,
778 1122 message: (0, import_i18n4.sprintf)(
779 1123 // translators: %s: file name.
780 1124 (0, import_i18n4.__)(
781 1125 "%s: This file exceeds the maximum upload size for this site."
@@ -836,17 +1180,38 @@
836 1180 const item = select2.getItem(id);
837 1181 if (!item) {
838 1182 return;
839 1183 }
1184 + clearRetryTimer(id);
1185 + if (!silent && error && !item.parentId && !item.attachment?.id) {
1186 + const settings = select2.getSettings();
1187 + const retrySettings = settings.retry;
1188 + if (retrySettings) {
1189 + const retryCount = item.retryCount ?? 0;
1190 + const maxRetries = retrySettings.maxRetryAttempts;
1191 + if (shouldRetryError(error, retryCount, maxRetries)) {
1192 + dispatch.scheduleRetry(id, error);
1193 + return;
1194 + }
1195 + }
1196 + }
840 1197 item.abortController?.abort();
841 - await vipsCancelOperations(id);
1198 + vipsCancelOperations(id).catch(() => {
1199 + });
1200 + cancelGifToVideoOperations(id).catch(() => {
1201 + });
842 1202 if (!silent) {
843 1203 const { onError } = item;
844 1204 onError?.(error ?? new Error("Upload cancelled"));
845 - if (!onError && error) {
1205 + if (!onError && error && !item.parentId) {
846 1206 console.error("Upload cancelled", error);
847 1207 }
1208 + } else {
1209 + debug(
1210 + `Item cancelled: ${item.file.name} (item ${id}): ${error instanceof Error ? error.message : error}`
1211 + );
848 1212 }
1213 + const { currentOperation, parentId, batchId } = item;
849 1214 dispatch({
850 1215 type: Type.Cancel,
851 1216 id,
852 1217 error
@@ -852,9 +1217,59 @@
852 1217 error
853 1218 });
854 1219 dispatch.removeItem(id);
855 1220 dispatch.revokeBlobUrls(id);
856 - if (item.batchId && select2.isBatchUploaded(item.batchId)) {
1221 + if (currentOperation === OperationType.ResizeCrop || currentOperation === OperationType.Rotate) {
1222 + for (const pending of select2.getPendingImageProcessing()) {
1223 + dispatch.processItem(pending.id);
1224 + }
1225 + }
1226 + if (currentOperation === OperationType.Upload) {
1227 + for (const pending of select2.getPendingUploads()) {
1228 + dispatch.processItem(pending.id);
1229 + }
1230 + }
1231 + if (currentOperation === OperationType.TranscodeGif) {
1232 + for (const pending of select2.getPendingVideoProcessing()) {
1233 + dispatch.processItem(pending.id);
1234 + }
1235 + }
1236 + if (currentOperation === OperationType.ResizeCrop || currentOperation === OperationType.Rotate || currentOperation === OperationType.TranscodeImage) {
1237 + maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
1238 + }
1239 + if (parentId) {
1240 + const parentItem = select2.getItem(parentId);
1241 + if (parentItem) {
1242 + const isOptionalCompanion = item.additionalData?.image_size === "animated_video" || item.additionalData?.image_size === "animated_video_poster";
1243 + if (select2.hasPendingItemsByParentId(parentId)) {
1244 + if (parentItem.operations && parentItem.operations.length > 0) {
1245 + dispatch.processItem(parentId);
1246 + }
1247 + } else if (parentItem.subSizes && parentItem.subSizes.length > 0 || isOptionalCompanion) {
1248 + if (parentItem.operations && parentItem.operations.length > 0) {
1249 + dispatch.processItem(parentId);
1250 + }
1251 + } else {
1252 + const parentAttachmentId = parentItem.attachment?.id;
1253 + const { mediaDelete } = select2.getSettings();
1254 + if (parentAttachmentId && mediaDelete) {
1255 + mediaDelete(parentAttachmentId).catch(() => {
1256 + });
1257 + }
1258 + await dispatch.cancelItem(
1259 + parentId,
1260 + new UploadError({
1261 + code: error instanceof UploadError && error.code || ErrorCode.GENERAL,
1262 + message: error?.message || (0, import_i18n5.__)("The image could not be uploaded."),
1263 + file: parentItem.file,
1264 + cause: error instanceof Error ? error : void 0
1265 + })
1266 + );
1267 + }
1268 + }
1269 + }
1270 + if (batchId && select2.isBatchUploaded(batchId)) {
1271 + debug(`Batch completed: ${batchId}`);
857 1272 item.onBatchSuccess?.();
858 1273 }
859 1274 };
860 1275 }
@@ -873,8 +1288,57 @@
873 1288 });
874 1289 dispatch.processItem(id);
875 1290 };
876 1291 }
1292 + function scheduleRetry(id, error) {
1293 + return async ({ select: select2, dispatch }) => {
1294 + const item = select2.getItem(id);
1295 + if (!item) {
1296 + return;
1297 + }
1298 + const settings = select2.getSettings();
1299 + const retrySettings = settings.retry;
1300 + if (!retrySettings) {
1301 + return;
1302 + }
1303 + const currentRetryCount = item.retryCount ?? 0;
1304 + const delay = calculateRetryDelay({
1305 + attempt: currentRetryCount + 1,
1306 + initialDelay: retrySettings.initialRetryDelayMs,
1307 + maxDelay: retrySettings.maxRetryDelayMs,
1308 + multiplier: retrySettings.backoffMultiplier,
1309 + jitter: retrySettings.retryJitter
1310 + });
1311 + const timerId = setTimeout(() => {
1312 + retryTimers.delete(id);
1313 + dispatch.executeRetry(id);
1314 + }, delay);
1315 + retryTimers.set(id, timerId);
1316 + dispatch({
1317 + type: Type.ScheduleRetry,
1318 + id,
1319 + error,
1320 + retryCount: currentRetryCount,
1321 + nextRetryTimestamp: Date.now() + delay
1322 + });
1323 + };
1324 + }
1325 + function executeRetry(id) {
1326 + return async ({ select: select2, dispatch }) => {
1327 + const item = select2.getItem(id);
1328 + if (!item || item.status !== ItemStatus.PendingRetry) {
1329 + return;
1330 + }
1331 + if (select2.isPaused()) {
1332 + return;
1333 + }
1334 + dispatch({
1335 + type: Type.RetryItem,
1336 + id
1337 + });
1338 + dispatch.processItem(id);
1339 + };
1340 + }
877 1341
878 1342 // packages/upload-media/build-module/store/private-actions.mjs
879 1343 var private_actions_exports = {};
880 1344 __export(private_actions_exports, {
@@ -879,8 +1343,9 @@
879 1343 var private_actions_exports = {};
880 1344 __export(private_actions_exports, {
881 1345 addItem: () => addItem,
882 1346 addSideloadItem: () => addSideloadItem,
1347 + detectUltraHdr: () => detectUltraHdr,
883 1348 finalizeItem: () => finalizeItem,
884 1349 finishOperation: () => finishOperation,
885 1350 generateThumbnails: () => generateThumbnails,
886 1351 getTranscodeImageOperation: () => getTranscodeImageOperation,
@@ -893,8 +1358,9 @@
893 1358 resumeQueue: () => resumeQueue,
894 1359 revokeBlobUrls: () => revokeBlobUrls,
895 1360 rotateItem: () => rotateItem,
896 1361 sideloadItem: () => sideloadItem,
1362 + transcodeGifItem: () => transcodeGifItem,
897 1363 transcodeImageItem: () => transcodeImageItem,
898 1364 updateItemProgress: () => updateItemProgress,
899 1365 updateSettings: () => updateSettings,
900 1366 uploadItem: () => uploadItem
@@ -899,8 +1365,9 @@
899 1365 updateSettings: () => updateSettings,
900 1366 uploadItem: () => uploadItem
901 1367 });
902 1368 var import_blob = __toESM(require_blob(), 1);
1369 + var import_i18n7 = __toESM(require_i18n(), 1);
903 1370
904 1371 // packages/upload-media/build-module/heic-parser.mjs
905 1372 var Reader = class {
906 1373 view;
@@ -1171,8 +1638,15 @@
1171 1638 return `hvc1.${spacePrefix}${profileIdc}.${compatHex}.${tierChar}${levelIdc}${constraintStr}`;
1172 1639 }
1173 1640 function readItemData(buffer, loc, idatOffset) {
1174 1641 const baseOffset = loc.constructionMethod === 1 ? idatOffset : 0;
1642 + for (const ext of loc.extents) {
1643 + if (baseOffset + ext.offset + ext.length > buffer.byteLength) {
1644 + throw new Error(
1645 + "HEIC item data extends past the end of the file"
1646 + );
1647 + }
1648 + }
1175 1649 if (loc.extents.length === 1) {
1176 1650 const ext = loc.extents[0];
1177 1651 const start = baseOffset + ext.offset;
1178 1652 return new Uint8Array(buffer.slice(start, start + ext.length));
@@ -1307,9 +1781,10 @@
1307 1781 tileWidth: width,
1308 1782 tileHeight: height,
1309 1783 outputWidth: width,
1310 1784 outputHeight: height,
1311 - rotation
1785 + rotation,
1786 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1312 1787 };
1313 1788 }
1314 1789 function parseGridImage(r, buffer, gridItemId, locations, allAssoc, properties, irefBox, idatOffset) {
1315 1790 const gridLoc = locations.get(gridItemId);
@@ -1400,13 +1875,269 @@
1400 1875 tileWidth,
1401 1876 tileHeight,
1402 1877 outputWidth,
1403 1878 outputHeight,
1404 - rotation
1879 + rotation,
1880 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1405 1881 };
1406 1882 }
1883 + function readTiffOrientation(payload) {
1884 + if (payload.length < 8) {
1885 + return 1;
1886 + }
1887 + const view = new DataView(
1888 + payload.buffer,
1889 + payload.byteOffset,
1890 + payload.byteLength
1891 + );
1892 + let tiffStart = 0;
1893 + const firstWord = view.getUint16(0);
1894 + if (firstWord !== 18761 && firstWord !== 19789) {
1895 + tiffStart = view.getUint32(0) + 4;
1896 + }
1897 + if (tiffStart + 8 > payload.length) {
1898 + return 1;
1899 + }
1900 + const byteOrder = view.getUint16(tiffStart);
1901 + let little;
1902 + if (byteOrder === 18761) {
1903 + little = true;
1904 + } else if (byteOrder === 19789) {
1905 + little = false;
1906 + } else {
1907 + return 1;
1908 + }
1909 + const ifd0 = tiffStart + view.getUint32(tiffStart + 4, little);
1910 + if (ifd0 + 2 > payload.length) {
1911 + return 1;
1912 + }
1913 + const entryCount = view.getUint16(ifd0, little);
1914 + for (let i = 0; i < entryCount; i++) {
1915 + const entry = ifd0 + 2 + i * 12;
1916 + if (entry + 12 > payload.length) {
1917 + break;
1918 + }
1919 + if (view.getUint16(entry, little) === 274) {
1920 + const value = view.getUint16(entry + 8, little);
1921 + return value >= 1 && value <= 8 ? value : 1;
1922 + }
1923 + }
1924 + return 1;
1925 + }
1926 + function findMeta(r) {
1927 + const metaBox = findBox(r, 0, r.view.byteLength, "meta");
1928 + if (!metaBox) {
1929 + return null;
1930 + }
1931 + const start = metaBox.offset + metaBox.headerSize + 4;
1932 + const end = metaBox.offset + metaBox.size;
1933 + return { box: metaBox, children: findBoxes(r, start, end) };
1934 + }
1935 + function parseExifOrientation(buffer) {
1936 + try {
1937 + const r = new Reader(buffer);
1938 + const meta = findMeta(r);
1939 + if (!meta) {
1940 + return 1;
1941 + }
1942 + const iinfBox = meta.children.find((b) => b.type === "iinf");
1943 + const ilocBox = meta.children.find((b) => b.type === "iloc");
1944 + if (!iinfBox || !ilocBox) {
1945 + return 1;
1946 + }
1947 + const itemTypes = parseIinf(r, iinfBox);
1948 + let exifItemId;
1949 + for (const [id, type] of itemTypes) {
1950 + if (type === "Exif") {
1951 + exifItemId = id;
1952 + break;
1953 + }
1954 + }
1955 + if (exifItemId === void 0) {
1956 + return 1;
1957 + }
1958 + const loc = parseIloc(r, ilocBox).get(exifItemId);
1959 + if (!loc || loc.extents.length === 0) {
1960 + return 1;
1961 + }
1962 + const idatBox = meta.children.find((b) => b.type === "idat");
1963 + const idatOffset = idatBox ? idatBox.offset + idatBox.headerSize : 0;
1964 + return readTiffOrientation(readItemData(buffer, loc, idatOffset));
1965 + } catch {
1966 + return 1;
1967 + }
1968 + }
1969 + function hasNativeTransform(r) {
1970 + const meta = findMeta(r);
1971 + if (!meta) {
1972 + return false;
1973 + }
1974 + const iprp = meta.children.find((b) => b.type === "iprp");
1975 + if (!iprp) {
1976 + return false;
1977 + }
1978 + const ipco = findBox(
1979 + r,
1980 + iprp.offset + iprp.headerSize,
1981 + iprp.offset + iprp.size,
1982 + "ipco"
1983 + );
1984 + if (!ipco) {
1985 + return false;
1986 + }
1987 + const start = ipco.offset + ipco.headerSize;
1988 + const end = ipco.offset + ipco.size;
1989 + return Boolean(
1990 + findBox(r, start, end, "irot") || findBox(r, start, end, "imir")
1991 + );
1992 + }
1993 + function getUnappliedExifOrientation(buffer) {
1994 + try {
1995 + if (hasNativeTransform(new Reader(buffer))) {
1996 + return 1;
1997 + }
1998 + } catch {
1999 + return 1;
2000 + }
2001 + return parseExifOrientation(buffer);
2002 + }
1407 2003
2004 + // packages/upload-media/build-module/heic-support.mjs
2005 + var import_i18n6 = __toESM(require_i18n(), 1);
2006 + function detectBrowserFamily() {
2007 + if (typeof navigator === "undefined") {
2008 + return null;
2009 + }
2010 + const brands = navigator.userAgentData?.brands;
2011 + if (brands?.length) {
2012 + if (brands.some(({ brand }) => /Chromium/i.test(brand))) {
2013 + return "chromium";
2014 + }
2015 + }
2016 + const userAgent = navigator.userAgent || "";
2017 + if (/Firefox|FxiOS/i.test(userAgent)) {
2018 + return "firefox";
2019 + }
2020 + if (/Chrome|Chromium|CriOS|Edg|OPR/i.test(userAgent)) {
2021 + return "chromium";
2022 + }
2023 + if (/Safari/i.test(userAgent)) {
2024 + return "safari";
2025 + }
2026 + return null;
2027 + }
2028 + function detectPlatform() {
2029 + if (typeof navigator === "undefined") {
2030 + return "other";
2031 + }
2032 + const platform = navigator.userAgentData?.platform || navigator.userAgent || "";
2033 + if (/iPhone|iPad|iPod|iOS/i.test(platform)) {
2034 + return "other";
2035 + }
2036 + if (/macOS|Mac OS X|Macintosh/i.test(platform)) {
2037 + return "macos";
2038 + }
2039 + if (/Windows/i.test(platform)) {
2040 + return "windows";
2041 + }
2042 + if (/Android|CrOS|Chrome OS/i.test(platform)) {
2043 + return "other";
2044 + }
2045 + if (/Linux|X11/i.test(platform)) {
2046 + return "linux";
2047 + }
2048 + return "other";
2049 + }
2050 + function getBrowserName() {
2051 + if (detectBrowserFamily() === "chromium") {
2052 + const brands = navigator.userAgentData?.brands ?? [];
2053 + const userAgent = navigator.userAgent || "";
2054 + if (brands.some(({ brand }) => /Edge/i.test(brand)) || /Edg\//i.test(userAgent)) {
2055 + return "Edge";
2056 + }
2057 + if (brands.some(({ brand }) => /Opera/i.test(brand)) || /OPR\//i.test(userAgent)) {
2058 + return "Opera";
2059 + }
2060 + return "Chrome";
2061 + }
2062 + switch (detectBrowserFamily()) {
2063 + case "firefox":
2064 + return "Firefox";
2065 + case "safari":
2066 + return "Safari";
2067 + default:
2068 + return null;
2069 + }
2070 + }
2071 + function getHeicConversionAdvice() {
2072 + const platform = detectPlatform();
2073 + const family = detectBrowserFamily();
2074 + if (platform === "macos") {
2075 + if (family === "chromium") {
2076 + return (0, import_i18n6.__)(
2077 + "Safari usually can, or you can upload a JPEG instead."
2078 + );
2079 + }
2080 + if (family !== "safari") {
2081 + return (0, import_i18n6.__)(
2082 + "Safari or Chrome usually can, or you can upload a JPEG instead."
2083 + );
2084 + }
2085 + }
2086 + if (platform === "windows" && family === "firefox") {
2087 + return (0, import_i18n6.__)("Chrome or Edge might, or you can upload a JPEG instead.");
2088 + }
2089 + return (0, import_i18n6.__)("You can upload a JPEG instead.");
2090 + }
2091 + function getHeicUnsupportedMessage() {
2092 + const platform = detectPlatform();
2093 + const family = detectBrowserFamily();
2094 + const browserName = getBrowserName();
2095 + let cause;
2096 + if (platform === "linux") {
2097 + cause = (0, import_i18n6.__)(
2098 + "HEIC photos can't be decoded on Linux, so unfortunately we couldn't convert this one."
2099 + );
2100 + } else if (family === "firefox") {
2101 + cause = (0, import_i18n6.__)(
2102 + "Firefox can't read HEIC photos, so we couldn't convert this one."
2103 + );
2104 + } else if (browserName && platform === "windows") {
2105 + cause = (0, import_i18n6.sprintf)(
2106 + /* translators: %s: browser name, e.g. "Chrome". */
2107 + (0, import_i18n6.__)(
2108 + "%s couldn't decode HEIC on this PC, so we couldn't convert this one."
2109 + ),
2110 + browserName
2111 + );
2112 + } else if (browserName && platform === "macos") {
2113 + cause = (0, import_i18n6.sprintf)(
2114 + /* translators: %s: browser name, e.g. "Safari". */
2115 + (0, import_i18n6.__)(
2116 + "%s couldn't decode HEIC on this Mac, so we couldn't convert this one."
2117 + ),
2118 + browserName
2119 + );
2120 + } else if (browserName) {
2121 + cause = (0, import_i18n6.sprintf)(
2122 + /* translators: %s: browser name, e.g. "Chrome". */
2123 + (0, import_i18n6.__)(
2124 + "%s couldn't decode HEIC on this device, so we couldn't convert this one."
2125 + ),
2126 + browserName
2127 + );
2128 + } else {
2129 + cause = (0, import_i18n6.__)(
2130 + "This browser can't read HEIC photos, so we couldn't convert this one."
2131 + );
2132 + }
2133 + return `${cause} ${getHeicConversionAdvice()}`;
2134 + }
2135 +
1408 2136 // packages/upload-media/build-module/canvas-utils.mjs
2137 + var IMAGE_DECODER_TIMEOUT = 15e3;
2138 + var HeicUnsupportedError = class extends Error {
2139 + };
1409 2140 async function canvasConvertToJpeg(file, quality = 0.82) {
1410 2141 const baseName = getFileBasename(file.name);
1411 2142 try {
1412 2143 const bitmap = await createImageBitmap(file);
@@ -1435,10 +2166,36 @@
1435 2166 const decoder = new ImageDecoder({
1436 2167 type: file.type,
1437 2168 data: file.stream()
1438 2169 });
2170 + let videoFrame;
2171 + let timeoutId;
2172 + let timedOut = false;
1439 2173 try {
1440 - const { image: videoFrame } = await decoder.decode();
2174 + const decoded = await Promise.race([
2175 + decoder.decode(),
2176 + new Promise((_resolve, reject) => {
2177 + timeoutId = setTimeout(() => {
2178 + timedOut = true;
2179 + decoder.close();
2180 + reject(
2181 + new Error("ImageDecoder decode timed out")
2182 + );
2183 + }, IMAGE_DECODER_TIMEOUT);
2184 + })
2185 + ]);
2186 + videoFrame = decoded.image;
2187 + } catch (error) {
2188 + if (!timedOut) {
2189 + throw error;
2190 + }
2191 + } finally {
2192 + clearTimeout(timeoutId);
2193 + if (!timedOut) {
2194 + decoder.close();
2195 + }
2196 + }
2197 + if (videoFrame) {
1441 2198 try {
1442 2199 const canvas = new OffscreenCanvas(
1443 2200 videoFrame.displayWidth,
1444 2201 videoFrame.displayHeight
@@ -1457,57 +2214,62 @@
1457 2214 });
1458 2215 } finally {
1459 2216 videoFrame.close();
1460 2217 }
1461 - } finally {
1462 - decoder.close();
1463 2218 }
1464 2219 }
1465 2220 }
2221 + let heicData;
2222 + try {
2223 + heicData = parseHeic(await file.arrayBuffer());
2224 + } catch (error) {
2225 + throw new Error("The HEIC file could not be parsed", {
2226 + cause: error
2227 + });
2228 + }
1466 2229 if (typeof VideoDecoder !== "undefined") {
2230 + let supported = false;
1467 2231 try {
1468 - const heicData = parseHeic(await file.arrayBuffer());
1469 2232 const support = await VideoDecoder.isConfigSupported({
1470 2233 codec: heicData.codecString
1471 2234 });
1472 - if (support.supported) {
1473 - const canvas = new OffscreenCanvas(
1474 - heicData.outputWidth,
1475 - heicData.outputHeight
2235 + supported = support.supported === true;
2236 + } catch {
2237 + }
2238 + if (supported) {
2239 + const canvas = new OffscreenCanvas(
2240 + heicData.outputWidth,
2241 + heicData.outputHeight
2242 + );
2243 + const ctx = canvas.getContext("2d");
2244 + if (!ctx) {
2245 + throw new Error("Could not get canvas 2d context");
2246 + }
2247 + for (const tile of heicData.tiles) {
2248 + const frame = await decodeHevcFrame(
2249 + heicData.codecString,
2250 + heicData.description,
2251 + heicData.tileWidth,
2252 + heicData.tileHeight,
2253 + tile.data
1476 2254 );
1477 - const ctx = canvas.getContext("2d");
1478 - if (!ctx) {
1479 - throw new Error("Could not get canvas 2d context");
2255 + try {
2256 + ctx.drawImage(frame, tile.x, tile.y);
2257 + } finally {
2258 + frame.close();
1480 2259 }
1481 - for (const tile of heicData.tiles) {
1482 - const frame = await decodeHevcFrame(
1483 - heicData.codecString,
1484 - heicData.description,
1485 - heicData.tileWidth,
1486 - heicData.tileHeight,
1487 - tile.data
1488 - );
1489 - try {
1490 - ctx.drawImage(frame, tile.x, tile.y);
1491 - } finally {
1492 - frame.close();
1493 - }
1494 - }
1495 - const outputCanvas = applyRotation(canvas, heicData.rotation);
1496 - const jpegBlob = await outputCanvas.convertToBlob({
1497 - type: "image/jpeg",
1498 - quality
1499 - });
1500 - return new File([jpegBlob], `${baseName}.jpg`, {
1501 - type: "image/jpeg"
1502 - });
1503 2260 }
1504 - } catch {
2261 + const outputCanvas = heicData.rotation !== 0 ? applyRotation(canvas, heicData.rotation) : applyExifOrientation(canvas, heicData.exifOrientation);
2262 + const jpegBlob = await outputCanvas.convertToBlob({
2263 + type: "image/jpeg",
2264 + quality
2265 + });
2266 + return new File([jpegBlob], `${baseName}.jpg`, {
2267 + type: "image/jpeg"
2268 + });
1505 2269 }
1506 2270 }
1507 - throw new Error(
1508 - "This browser cannot decode HEIC images. Please use Safari or convert to JPEG before uploading."
1509 - );
2271 + throw new HeicUnsupportedError(getHeicUnsupportedMessage());
1510 2272 }
1511 2273 function applyRotation(source, rotation) {
1512 2274 if (rotation === 0) {
1513 2275 return source;
@@ -1524,8 +2286,45 @@
1524 2286 ctx.rotate(-rotation * Math.PI / 180);
1525 2287 ctx.drawImage(source, -source.width / 2, -source.height / 2);
1526 2288 return rotated;
1527 2289 }
2290 + function applyExifOrientation(source, orientation) {
2291 + if (orientation <= 1 || orientation > 8) {
2292 + return source;
2293 + }
2294 + const { width: sw, height: sh } = source;
2295 + const swap = orientation >= 5;
2296 + const out = new OffscreenCanvas(swap ? sh : sw, swap ? sw : sh);
2297 + const ctx = out.getContext("2d");
2298 + if (!ctx) {
2299 + return source;
2300 + }
2301 + switch (orientation) {
2302 + case 2:
2303 + ctx.transform(-1, 0, 0, 1, sw, 0);
2304 + break;
2305 + case 3:
2306 + ctx.transform(-1, 0, 0, -1, sw, sh);
2307 + break;
2308 + case 4:
2309 + ctx.transform(1, 0, 0, -1, 0, sh);
2310 + break;
2311 + case 5:
2312 + ctx.transform(0, 1, 1, 0, 0, 0);
2313 + break;
2314 + case 6:
2315 + ctx.transform(0, 1, -1, 0, sh, 0);
2316 + break;
2317 + case 7:
2318 + ctx.transform(0, -1, -1, 0, sh, sw);
2319 + break;
2320 + case 8:
2321 + ctx.transform(0, -1, 1, 0, 0, sw);
2322 + break;
2323 + }
2324 + ctx.drawImage(source, 0, 0);
2325 + return out;
2326 + }
1528 2327 function decodeHevcFrame(codec, description, width, height, data) {
1529 2328 return new Promise((resolve, reject) => {
1530 2329 const decoder = new VideoDecoder({
1531 2330 output: (frame) => {
@@ -1652,9 +2451,94 @@
1652 2451 }
1653 2452 function clearFeatureDetectionCache() {
1654 2453 cachedResult = null;
1655 2454 }
2455 + var BYTES_PER_PIXEL = 4;
2456 + var INTERLACED_MEMORY_BUDGET = 0.5 * 1024 * 1024 * 1024;
2457 + var BASELINE_MEMORY_BUDGET = 0.9 * 1024 * 1024 * 1024;
2458 + function exceedsClientProcessingMemory(dimensions) {
2459 + const { width, height, interlaced } = dimensions;
2460 + const estimatedBytes = width * height * BYTES_PER_PIXEL;
2461 + const budget = interlaced ? INTERLACED_MEMORY_BUDGET : BASELINE_MEMORY_BUDGET;
2462 + return estimatedBytes > budget;
2463 + }
1656 2464
2465 + // packages/upload-media/build-module/get-image-dimensions.mjs
2466 + var MAX_HEADER_BYTES = 512 * 1024;
2467 + function parseJpeg(view) {
2468 + let offset = 2;
2469 + while (offset < view.byteLength) {
2470 + if (view.getUint8(offset) !== 255) {
2471 + return null;
2472 + }
2473 + while (offset < view.byteLength && view.getUint8(offset) === 255) {
2474 + offset++;
2475 + }
2476 + if (offset >= view.byteLength) {
2477 + return null;
2478 + }
2479 + const marker = view.getUint8(offset);
2480 + offset++;
2481 + if (marker === 1 || marker >= 208 && marker <= 217) {
2482 + continue;
2483 + }
2484 + if (marker === 218) {
2485 + return null;
2486 + }
2487 + if (offset + 2 > view.byteLength) {
2488 + return null;
2489 + }
2490 + const segmentLength = view.getUint16(offset);
2491 + if (segmentLength < 2) {
2492 + return null;
2493 + }
2494 + const isStartOfFrame = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
2495 + if (isStartOfFrame) {
2496 + if (offset + 7 > view.byteLength) {
2497 + return null;
2498 + }
2499 + const height = view.getUint16(offset + 3);
2500 + const width = view.getUint16(offset + 5);
2501 + const interlaced = marker === 194 || marker === 198 || marker === 202 || marker === 206;
2502 + return { width, height, interlaced };
2503 + }
2504 + offset += segmentLength;
2505 + }
2506 + return null;
2507 + }
2508 + function parsePng(view) {
2509 + if (view.byteLength < 29) {
2510 + return null;
2511 + }
2512 + const isIhdr = view.getUint8(12) === 73 && // I
2513 + view.getUint8(13) === 72 && // H
2514 + view.getUint8(14) === 68 && // D
2515 + view.getUint8(15) === 82;
2516 + if (!isIhdr) {
2517 + return null;
2518 + }
2519 + const width = view.getUint32(16);
2520 + const height = view.getUint32(20);
2521 + const interlaceMethod = view.getUint8(28);
2522 + return { width, height, interlaced: interlaceMethod !== 0 };
2523 + }
2524 + async function getImageDimensions(file) {
2525 + try {
2526 + const headerBytes = Math.min(file.size, MAX_HEADER_BYTES);
2527 + const buffer = await file.slice(0, headerBytes).arrayBuffer();
2528 + const view = new DataView(buffer);
2529 + if (view.byteLength >= 3 && view.getUint16(0) === 65496) {
2530 + return parseJpeg(view);
2531 + }
2532 + if (view.byteLength >= 8 && view.getUint32(0) === 2303741511 && view.getUint32(4) === 218765834) {
2533 + return parsePng(view);
2534 + }
2535 + return null;
2536 + } catch {
2537 + return null;
2538 + }
2539 + }
2540 +
1657 2541 // packages/upload-media/build-module/stub-file.mjs
1658 2542 var StubFile = class extends File {
1659 2543 constructor(fileName = "stub-file") {
1660 2544 super([], fileName);
@@ -1662,8 +2546,9 @@
1662 2546 };
1663 2547
1664 2548 // packages/upload-media/build-module/store/private-actions.mjs
1665 2549 var DEFAULT_OUTPUT_QUALITY = 0.82;
2550 + var ultraHdrItems = /* @__PURE__ */ new Set();
1666 2551 function addItem({
1667 2552 file: fileOrBlob,
1668 2553 batchId,
1669 2554 onChange,
@@ -1754,8 +2639,11 @@
1754 2639 const item = select2.getItem(id);
1755 2640 if (!item) {
1756 2641 return;
1757 2642 }
2643 + if (item.currentOperation) {
2644 + return;
2645 + }
1758 2646 const {
1759 2647 attachment,
1760 2648 onChange,
1761 2649 onSuccess,
@@ -1778,8 +2666,14 @@
1778 2666 if (activeCount >= settings.maxConcurrentImageProcessing) {
1779 2667 return;
1780 2668 }
1781 2669 }
2670 + if (operation === OperationType.TranscodeGif) {
2671 + const activeCount = select2.getActiveVideoProcessingCount();
2672 + if (activeCount >= 1) {
2673 + return;
2674 + }
2675 + }
1782 2676 if (attachment) {
1783 2677 const isHeicUrl = attachment.url && /\.hei[cf]$/i.test(attachment.url);
1784 2678 if (!isHeicUrl) {
1785 2679 onChange?.([attachment]);
@@ -1823,8 +2717,11 @@
1823 2717 type: Type.OperationStart,
1824 2718 id,
1825 2719 operation
1826 2720 });
2721 + debug(
2722 + `Starting operation ${operation} for ${item.file.name} (item ${item.id})`
2723 + );
1827 2724 switch (operation) {
1828 2725 case OperationType.Prepare:
1829 2726 dispatch.prepareItem(item.id);
1830 2727 break;
@@ -1845,8 +2742,14 @@
1845 2742 item.id,
1846 2743 operationArgs
1847 2744 );
1848 2745 break;
2746 + case OperationType.TranscodeGif:
2747 + dispatch.transcodeGifItem(
2748 + item.id,
2749 + operationArgs
2750 + );
2751 + break;
1849 2752 case OperationType.Upload:
1850 2753 if (item.parentId) {
1851 2754 dispatch.sideloadItem(id);
1852 2755 } else {
@@ -1858,8 +2761,11 @@
1858 2761 break;
1859 2762 case OperationType.Finalize:
1860 2763 dispatch.finalizeItem(id);
1861 2764 break;
2765 + case OperationType.DetectUltraHdr:
2766 + dispatch.detectUltraHdr(id);
2767 + break;
1862 2768 }
1863 2769 };
1864 2770 }
1865 2771 function pauseQueue() {
@@ -1872,9 +2778,13 @@
1872 2778 dispatch({
1873 2779 type: Type.ResumeQueue
1874 2780 });
1875 2781 for (const item of select2.getAllItems()) {
1876 - dispatch.processItem(item.id);
2782 + if (item.status === ItemStatus.PendingRetry) {
2783 + dispatch.executeRetry(item.id);
2784 + } else {
2785 + dispatch.processItem(item.id);
2786 + }
1877 2787 }
1878 2788 };
1879 2789 }
1880 2790 function pauseItem(id) {
@@ -1890,8 +2800,10 @@
1890 2800 const item = select2.getItem(id);
1891 2801 if (!item) {
1892 2802 return;
1893 2803 }
2804 + ultraHdrItems.delete(id);
2805 + clearRetryTimer(id);
1894 2806 dispatch({
1895 2807 type: Type.Remove,
1896 2808 id
1897 2809 });
@@ -1896,8 +2808,9 @@
1896 2808 id
1897 2809 });
1898 2810 if (select2.getAllItems().length === 0) {
1899 2811 terminateVipsWorker();
2812 + terminateVideoConversionWorker();
1900 2813 }
1901 2814 };
1902 2815 }
1903 2816 function finishOperation(id, updates) {
@@ -1921,8 +2834,17 @@
1921 2834 for (const pendingItem of pendingItems) {
1922 2835 dispatch.processItem(pendingItem.id);
1923 2836 }
1924 2837 }
2838 + if (previousOperation === OperationType.TranscodeGif) {
2839 + const pendingItems = select2.getPendingVideoProcessing();
2840 + for (const pendingItem of pendingItems) {
2841 + dispatch.processItem(pendingItem.id);
2842 + }
2843 + }
2844 + if (previousOperation === OperationType.ResizeCrop || previousOperation === OperationType.Rotate || previousOperation === OperationType.TranscodeImage) {
2845 + maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
2846 + }
1925 2847 };
1926 2848 }
1927 2849 var VALID_IMAGE_FORMATS = ["jpeg", "webp", "avif", "png", "gif"];
1928 2850 function isValidImageFormat(format) {
@@ -1927,9 +2849,9 @@
1927 2849 var VALID_IMAGE_FORMATS = ["jpeg", "webp", "avif", "png", "gif"];
1928 2850 function isValidImageFormat(format) {
1929 2851 return VALID_IMAGE_FORMATS.includes(format);
1930 2852 }
1931 - async function getTranscodeImageOperation(file, outputMimeType, interlaced = false) {
2853 + async function getTranscodeImageOperation(file, outputMimeType, interlaced = false, quality = DEFAULT_OUTPUT_QUALITY) {
1932 2854 if (file.type === "image/png" && outputMimeType === "image/jpeg") {
1933 2855 const blobUrl = (0, import_blob.createBlobURL)(file);
1934 2856 try {
1935 2857 const hasAlpha = await vipsHasTransparency(blobUrl);
@@ -1949,9 +2871,9 @@
1949 2871 return [
1950 2872 OperationType.TranscodeImage,
1951 2873 {
1952 2874 outputFormat: formatPart,
1953 - outputQuality: DEFAULT_OUTPUT_QUALITY,
2875 + outputQuality: quality,
1954 2876 interlaced
1955 2877 }
1956 2878 ];
1957 2879 }
@@ -1963,28 +2885,63 @@
1963 2885 }
1964 2886 const { file } = item;
1965 2887 const operations = [];
1966 2888 const settings = select2.getSettings();
2889 + if (file.type === "image/gif" && settings.gifConvert !== false && typeof ImageDecoder !== "undefined" && typeof VideoEncoder !== "undefined") {
2890 + let isAnimated = false;
2891 + try {
2892 + isAnimated = isAnimatedGif(await file.arrayBuffer());
2893 + } catch {
2894 + isAnimated = false;
2895 + }
2896 + if (isAnimated) {
2897 + let hasTransparency = false;
2898 + const blobUrl = (0, import_blob.createBlobURL)(file);
2899 + try {
2900 + hasTransparency = await vipsHasTransparency(blobUrl);
2901 + } catch {
2902 + hasTransparency = true;
2903 + } finally {
2904 + (0, import_blob.revokeBlobURL)(blobUrl);
2905 + }
2906 + if (!hasTransparency) {
2907 + operations.push(
2908 + OperationType.Upload,
2909 + OperationType.ThumbnailGeneration,
2910 + OperationType.Finalize
2911 + );
2912 + dispatch({
2913 + type: Type.AddOperations,
2914 + id,
2915 + operations
2916 + });
2917 + dispatch.finishOperation(id, {
2918 + animatedGifFile: item.file
2919 + });
2920 + return;
2921 + }
2922 + }
2923 + }
1967 2924 let heicJpeg = null;
1968 - const isImage = file.type.startsWith("image/");
1969 - const isVipsSupported = CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(
1970 - file.type
1971 - );
1972 - const isHeic = HEIC_MIME_TYPES.includes(file.type);
2925 + const isHeic = await isHeicFile(file);
2926 + const isMisnamedHeic = isHeic && !HEIC_MIME_TYPES.includes(file.type);
2927 + const isImage = file.type.startsWith("image/") || isHeic;
2928 + const isVipsSupported = !isHeic && CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(file.type);
2929 + const heicFile = isMisnamedHeic ? new File([file], `${getFileBasename(file.name)}.heic`, {
2930 + type: HEIC_MIME_TYPES[0],
2931 + lastModified: file.lastModified
2932 + }) : file;
2933 + let tooLargeForClient = false;
1973 2934 if (isImage && isVipsSupported) {
1974 - const { bigImageSizeThreshold } = settings;
1975 - if (bigImageSizeThreshold) {
1976 - operations.push([
1977 - OperationType.ResizeCrop,
1978 - {
1979 - resize: {
1980 - width: bigImageSizeThreshold,
1981 - height: bigImageSizeThreshold
1982 - },
1983 - isThresholdResize: true
1984 - }
1985 - ]);
2935 + const dimensions = await getImageDimensions(file);
2936 + if (dimensions && exceedsClientProcessingMemory(dimensions)) {
2937 + tooLargeForClient = true;
1986 2938 }
2939 + }
2940 + if (file.type === "image/jpeg" && !isHeic && !tooLargeForClient) {
2941 + operations.push(OperationType.DetectUltraHdr);
2942 + }
2943 + if (isImage && isVipsSupported && !tooLargeForClient) {
1987 2944 operations.push(
1988 2945 OperationType.Upload,
1989 2946 OperationType.ThumbnailGeneration,
1990 2947 OperationType.Finalize
@@ -1991,18 +2948,22 @@
1991 2948 );
1992 2949 } else if (isImage && isHeic) {
1993 2950 try {
1994 2951 heicJpeg = await canvasConvertToJpeg(
1995 - file,
2952 + heicFile,
1996 2953 settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY
1997 2954 );
1998 - } catch {
2955 + } catch (error) {
2956 + const unsupported = error instanceof HeicUnsupportedError;
1999 2957 dispatch.cancelItem(
2000 2958 id,
2001 2959 new UploadError({
2002 - code: "HEIC_DECODE_ERROR",
2003 - message: "This browser cannot decode HEIC images and the server does not support them either. Please convert to JPEG before uploading.",
2004 - file
2960 + code: unsupported ? ErrorCode.HEIC_DECODE_ERROR : ErrorCode.IMAGE_TRANSCODING_ERROR,
2961 + message: unsupported ? getHeicUnsupportedMessage() : (0, import_i18n7.__)(
2962 + "This HEIC image could not be converted. Try converting it to JPEG before uploading."
2963 + ),
2964 + file,
2965 + cause: error instanceof Error ? error : void 0
2005 2966 })
2006 2967 );
2007 2968 return;
2008 2969 }
@@ -2018,15 +2979,15 @@
2018 2979 type: Type.AddOperations,
2019 2980 id,
2020 2981 operations
2021 2982 });
2022 - let updates = {};
2983 + let updates;
2023 2984 if (isHeic && heicJpeg) {
2024 2985 const vipsAvailable = isClientSideMediaSupported();
2025 2986 updates = {
2026 2987 file: heicJpeg,
2027 2988 sourceFile: heicJpeg,
2028 - originalHeicFile: item.file,
2989 + originalHeicFile: heicFile,
2029 2990 additionalData: {
2030 2991 ...item.additionalData,
2031 2992 generate_sub_sizes: !vipsAvailable,
2032 2993 convert_format: true
@@ -2031,9 +2992,9 @@
2031 2992 generate_sub_sizes: !vipsAvailable,
2032 2993 convert_format: true
2033 2994 }
2034 2995 };
2035 - } else if (!isVipsSupported || !isImage) {
2996 + } else if (!isVipsSupported || !isImage || tooLargeForClient) {
2036 2997 updates = {
2037 2998 additionalData: {
2038 2999 ...item.additionalData,
2039 3000 generate_sub_sizes: true,
@@ -2039,12 +3000,37 @@
2039 3000 generate_sub_sizes: true,
2040 3001 convert_format: true
2041 3002 }
2042 3003 };
3004 + } else {
3005 + updates = {
3006 + additionalData: {
3007 + ...item.additionalData,
3008 + generate_sub_sizes: false
3009 + }
3010 + };
2043 3011 }
2044 3012 dispatch.finishOperation(id, updates);
2045 3013 };
2046 3014 }
3015 + function detectUltraHdr(id) {
3016 + return async ({ select: select2, dispatch }) => {
3017 + const item = select2.getItem(id);
3018 + if (!item) {
3019 + return;
3020 + }
3021 + let info;
3022 + try {
3023 + const buffer = await item.file.arrayBuffer();
3024 + info = await vipsGetUltraHdrInfo(buffer);
3025 + } catch {
3026 + }
3027 + if (info) {
3028 + ultraHdrItems.add(id);
3029 + }
3030 + dispatch.finishOperation(id, {});
3031 + };
3032 + }
2047 3033 function uploadItem(id) {
2048 3034 return async ({ select: select2, dispatch }) => {
2049 3035 const item = select2.getItem(id);
2050 3036 if (!item) {
@@ -2049,23 +3035,42 @@
2049 3035 const item = select2.getItem(id);
2050 3036 if (!item) {
2051 3037 return;
2052 3038 }
3039 + const startTime = performance.now();
3040 + let finished = false;
3041 + const finishUpload = (attachment) => {
3042 + if (finished) {
3043 + return;
3044 + }
3045 + finished = true;
3046 + measure({
3047 + measureName: `Upload ${item.file.name}`,
3048 + startTime,
3049 + tooltipText: item.file.name,
3050 + properties: [
3051 + ["Item ID", item.id],
3052 + ["File name", item.file.name]
3053 + ]
3054 + });
3055 + dispatch.finishOperation(id, { attachment });
3056 + };
2053 3057 select2.getSettings().mediaUpload({
2054 3058 filesList: [item.file],
2055 3059 additionalData: item.additionalData,
2056 3060 signal: item.abortController?.signal,
3061 + // The queue's own items drive upload progress UI and save
3062 + // locking; without this, consumers that track uploads themselves
3063 + // (e.g. the editor's progress snackbar) would count this file a
3064 + // second time.
3065 + isTransportOnly: true,
2057 3066 onFileChange: ([attachment]) => {
2058 3067 if (attachment && !(0, import_blob.isBlobURL)(attachment.url)) {
2059 - dispatch.finishOperation(id, {
2060 - attachment
2061 - });
3068 + finishUpload(attachment);
2062 3069 }
2063 3070 },
2064 3071 onSuccess: ([attachment]) => {
2065 - dispatch.finishOperation(id, {
2066 - attachment
2067 - });
3072 + finishUpload(attachment);
2068 3073 },
2069 3074 onError: (error) => {
2070 3075 dispatch.cancelItem(id, error);
2071 3076 }
@@ -2083,8 +3088,9 @@
2083 3088 if (!mediaSideload) {
2084 3089 dispatch.finishOperation(id, {});
2085 3090 return;
2086 3091 }
3092 + const startTime = performance.now();
2087 3093 mediaSideload({
2088 3094 file: item.file,
2089 3095 attachmentId: post,
2090 3096 additionalData,
@@ -2089,8 +3095,17 @@
2089 3095 attachmentId: post,
2090 3096 additionalData,
2091 3097 signal: item.abortController?.signal,
2092 3098 onSuccess: (subSize) => {
3099 + measure({
3100 + measureName: `Sideload ${item.file.name}`,
3101 + startTime,
3102 + tooltipText: item.file.name,
3103 + properties: [
3104 + ["Item ID", item.id],
3105 + ["File name", item.file.name]
3106 + ]
3107 + });
2093 3108 if (item.parentId) {
2094 3109 dispatch({
2095 3110 type: Type.AccumulateSubSize,
2096 3111 id: item.parentId,
@@ -2116,21 +3131,36 @@
2116 3131 file: item.file
2117 3132 });
2118 3133 return;
2119 3134 }
3135 + const startTime = performance.now();
2120 3136 const addSuffix = Boolean(item.parentId);
2121 3137 const scaledSuffix = Boolean(args.isThresholdResize);
3138 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2122 3139 try {
2123 3140 const file = await vipsResizeImage(
2124 3141 item.id,
2125 3142 item.file,
2126 3143 args.resize,
2127 - false,
2128 - // smartCrop
2129 - addSuffix,
2130 - item.abortController?.signal,
2131 - scaledSuffix
3144 + {
3145 + smartCrop: false,
3146 + addSuffix,
3147 + signal: item.abortController?.signal,
3148 + scaledSuffix,
3149 + quality: args.quality,
3150 + stripMeta: imageStripMeta,
3151 + maxBitdepth: imageMaxBitDepth
3152 + }
2132 3153 );
3154 + measure({
3155 + measureName: `ResizeCrop ${item.file.name}`,
3156 + startTime,
3157 + tooltipText: item.file.name,
3158 + properties: [
3159 + ["Item ID", item.id],
3160 + ["File name", item.file.name]
3161 + ]
3162 + });
2133 3163 const blobUrl = (0, import_blob.createBlobURL)(file);
2134 3164 dispatch({
2135 3165 type: Type.CacheBlobUrl,
2136 3166 id,
@@ -2145,10 +3175,12 @@
2145 3175 } catch (error) {
2146 3176 dispatch.cancelItem(
2147 3177 id,
2148 3178 new UploadError({
2149 - code: "IMAGE_TRANSCODING_ERROR",
2150 - message: "File could not be uploaded",
3179 + code: ErrorCode.IMAGE_TRANSCODING_ERROR,
3180 + message: (0, import_i18n7.__)(
3181 + "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
3182 + ),
2151 3183 file: item.file,
2152 3184 cause: error instanceof Error ? error : void 0
2153 3185 })
2154 3186 );
@@ -2166,8 +3198,9 @@
2166 3198 file: item.file
2167 3199 });
2168 3200 return;
2169 3201 }
3202 + const startTime = performance.now();
2170 3203 try {
2171 3204 const file = await vipsRotateImage(
2172 3205 item.id,
2173 3206 item.file,
@@ -2173,8 +3206,17 @@
2173 3206 item.file,
2174 3207 args.orientation,
2175 3208 item.abortController?.signal
2176 3209 );
3210 + measure({
3211 + measureName: `Rotate ${item.file.name}`,
3212 + startTime,
3213 + tooltipText: item.file.name,
3214 + properties: [
3215 + ["Item ID", item.id],
3216 + ["File name", item.file.name]
3217 + ]
3218 + });
2177 3219 const blobUrl = (0, import_blob.createBlobURL)(file);
2178 3220 dispatch({
2179 3221 type: Type.CacheBlobUrl,
2180 3222 id,
@@ -2189,10 +3231,12 @@
2189 3231 } catch (error) {
2190 3232 dispatch.cancelItem(
2191 3233 id,
2192 3234 new UploadError({
2193 - code: "IMAGE_ROTATION_ERROR",
2194 - message: "Image could not be rotated",
3235 + code: ErrorCode.IMAGE_ROTATION_ERROR,
3236 + message: (0, import_i18n7.__)(
3237 + "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
3238 + ),
2195 3239 file: item.file,
2196 3240 cause: error instanceof Error ? error : void 0
2197 3241 })
2198 3242 );
@@ -2210,19 +3254,34 @@
2210 3254 file: item.file
2211 3255 });
2212 3256 return;
2213 3257 }
3258 + const startTime = performance.now();
2214 3259 const outputMimeType = `image/${args.outputFormat}`;
2215 3260 const quality = args.outputQuality ?? DEFAULT_OUTPUT_QUALITY;
2216 3261 const interlaced = args.interlaced ?? false;
3262 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2217 3263 try {
2218 3264 const file = await vipsConvertImageFormat(
2219 3265 item.id,
2220 3266 item.file,
2221 3267 outputMimeType,
2222 - quality,
2223 - interlaced
3268 + {
3269 + quality,
3270 + interlaced,
3271 + stripMeta: imageStripMeta,
3272 + maxBitdepth: imageMaxBitDepth
3273 + }
2224 3274 );
3275 + measure({
3276 + measureName: `Transcode ${item.file.name}`,
3277 + startTime,
3278 + tooltipText: item.file.name,
3279 + properties: [
3280 + ["Item ID", item.id],
3281 + ["File name", item.file.name]
3282 + ]
3283 + });
2225 3284 const blobUrl = (0, import_blob.createBlobURL)(file);
2226 3285 dispatch({
2227 3286 type: Type.CacheBlobUrl,
2228 3287 id,
@@ -2237,9 +3296,9 @@
2237 3296 } catch (error) {
2238 3297 dispatch.cancelItem(
2239 3298 id,
2240 3299 new UploadError({
2241 - code: "MEDIA_TRANSCODING_ERROR",
3300 + code: ErrorCode.MEDIA_TRANSCODING_ERROR,
2242 3301 message: "Image could not be transcoded to the target format",
2243 3302 file: item.file,
2244 3303 cause: error instanceof Error ? error : void 0
2245 3304 })
@@ -2246,8 +3305,91 @@
2246 3305 );
2247 3306 }
2248 3307 };
2249 3308 }
3309 + function transcodeGifItem(id, args) {
3310 + return async ({ select: select2, dispatch }) => {
3311 + const item = select2.getItem(id);
3312 + if (!item) {
3313 + return;
3314 + }
3315 + const outputFormat = args?.outputFormat ?? "mp4";
3316 + const outputMimeType = `video/${outputFormat}`;
3317 + const gifFile = item.file;
3318 + try {
3319 + const file = await convertGifToVideo(
3320 + item.id,
3321 + gifFile,
3322 + outputMimeType,
3323 + {
3324 + timeout: args?.timeout,
3325 + maxTotalPixels: args?.maxTotalPixels
3326 + }
3327 + );
3328 + dispatch.finishOperation(id, { file });
3329 + dispatch.addSideloadItem({
3330 + file: gifFile,
3331 + batchId: v4_default(),
3332 + parentId: item.parentId,
3333 + additionalData: {
3334 + post: item.additionalData?.post,
3335 + image_size: "animated_video_poster",
3336 + convert_format: false
3337 + },
3338 + operations: [
3339 + [
3340 + OperationType.TranscodeImage,
3341 + {
3342 + outputFormat: "jpeg",
3343 + outputQuality: DEFAULT_OUTPUT_QUALITY,
3344 + interlaced: false
3345 + }
3346 + ],
3347 + OperationType.Upload
3348 + ]
3349 + });
3350 + } catch (error) {
3351 + if (isUnsupportedConversionError(error)) {
3352 + if (isSizeLimitConversionError(error)) {
3353 + debug(
3354 + `Skipping GIF to video conversion: ${error instanceof Error ? error.message : error}`
3355 + );
3356 + }
3357 + dispatch.cancelItem(
3358 + id,
3359 + new Error("Animated GIF conversion unsupported"),
3360 + true
3361 + );
3362 + return;
3363 + }
3364 + if (isConversionTimeoutError(error)) {
3365 + debug(
3366 + `GIF to video conversion timed out; keeping the original GIF only: ${error instanceof Error ? error.message : error}`
3367 + );
3368 + dispatch.cancelItem(
3369 + id,
3370 + new Error("Animated GIF conversion timed out"),
3371 + true
3372 + );
3373 + return;
3374 + }
3375 + console.error(
3376 + "[video-conversion] GIF to video conversion failed:",
3377 + error
3378 + );
3379 + dispatch.cancelItem(
3380 + id,
3381 + new UploadError({
3382 + code: ErrorCode.GIF_TRANSCODING_ERROR,
3383 + message: "Animated GIF could not be converted to video",
3384 + file: item.file,
3385 + cause: error instanceof Error ? error : void 0
3386 + }),
3387 + true
3388 + );
3389 + }
3390 + };
3391 + }
2250 3392 function generateThumbnails(id) {
2251 3393 return async ({ select: select2, dispatch }) => {
2252 3394 const item = select2.getItem(id);
2253 3395 if (!item) {
@@ -2265,35 +3407,57 @@
2265 3407 batchId: v4_default(),
2266 3408 parentId: item.id,
2267 3409 additionalData: {
2268 3410 post: attachment.id,
2269 - image_size: "original-heic",
3411 + image_size: "source_original",
2270 3412 convert_format: false
2271 3413 },
2272 3414 operations: [OperationType.Upload]
2273 3415 });
2274 3416 }
3417 + if (item.animatedGifFile && attachment.id) {
3418 + const outputFormat = settings.videoOutputFormat === "video/webm" ? "webm" : "mp4";
3419 + dispatch.addSideloadItem({
3420 + file: item.animatedGifFile,
3421 + batchId: v4_default(),
3422 + parentId: item.id,
3423 + additionalData: {
3424 + post: attachment.id,
3425 + image_size: "animated_video",
3426 + convert_format: false
3427 + },
3428 + operations: [
3429 + [
3430 + OperationType.TranscodeGif,
3431 + {
3432 + outputFormat
3433 + }
3434 + ],
3435 + OperationType.Upload
3436 + ]
3437 + });
3438 + }
3439 + let exifOrientation = attachment.exif_orientation || 1;
3440 + const sourceType = item.sourceFile.type;
3441 + const isHeifFamily = sourceType === "image/avif" || sourceType === "image/heif";
3442 + let needsClientRotation = false;
3443 + if (isHeifFamily) {
3444 + exifOrientation = getUnappliedExifOrientation(
3445 + await item.sourceFile.arrayBuffer()
3446 + );
3447 + needsClientRotation = exifOrientation !== 1;
3448 + }
3449 + let rotatedSource;
2275 3450 {
2276 - const needsRotation = attachment.exif_orientation && attachment.exif_orientation !== 1 && !item.file.name.includes("-scaled");
2277 - if (needsRotation && attachment.id) {
3451 + const needsRotation = exifOrientation !== 1 && !item.file.name.includes("-scaled");
3452 + if ((needsRotation || needsClientRotation) && attachment.id) {
2278 3453 try {
2279 - const rotatedFile = await vipsRotateImage(
3454 + rotatedSource = await vipsRotateImage(
2280 3455 item.id,
2281 3456 item.sourceFile,
2282 - attachment.exif_orientation,
3457 + exifOrientation,
2283 3458 item.abortController?.signal
2284 3459 );
2285 - dispatch.addSideloadItem({
2286 - file: rotatedFile,
2287 - batchId: v4_default(),
2288 - parentId: item.id,
2289 - additionalData: {
2290 - post: attachment.id,
2291 - image_size: "original",
2292 - convert_format: false
2293 - },
2294 - operations: [OperationType.Upload]
2295 - });
2296 3460 } catch {
2297 3461 console.warn(
2298 3462 "Failed to rotate image, continuing with thumbnails"
2299 3463 );
@@ -2298,23 +3462,48 @@
2298 3462 "Failed to rotate image, continuing with thumbnails"
2299 3463 );
2300 3464 }
2301 3465 }
3466 + if (needsRotation && rotatedSource && attachment.id) {
3467 + dispatch.addSideloadItem({
3468 + file: rotatedSource,
3469 + batchId: v4_default(),
3470 + parentId: item.id,
3471 + additionalData: {
3472 + post: attachment.id,
3473 + image_size: "original",
3474 + convert_format: false
3475 + },
3476 + operations: [OperationType.Upload]
3477 + });
3478 + }
2302 3479 }
2303 3480 if (!item.parentId && attachment.missing_image_sizes && attachment.missing_image_sizes.length > 0) {
2304 3481 const allImageSizes = settings.allImageSizes || {};
2305 3482 const sizesToGenerate = attachment.missing_image_sizes;
2306 - const thumbnailSource = item.sourceFile;
3483 + const thumbnailSource = needsClientRotation && rotatedSource ? rotatedSource : item.sourceFile;
2307 3484 const file = attachment.filename ? renameFile(thumbnailSource, attachment.filename) : thumbnailSource;
2308 3485 const batchId = v4_default();
3486 + const isUltraHdr = ultraHdrItems.has(item.id);
2309 3487 const outputMimeType = attachment.image_output_format;
2310 3488 const interlaced = attachment.image_save_progressive ?? false;
3489 + const imageQuality = attachment.image_quality;
3490 + const fallbackQuality = settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY;
3491 + const defaultQuality = typeof imageQuality?.default === "number" ? imageQuality.default / 100 : fallbackQuality;
3492 + const qualityForSize = (sizeName) => {
3493 + const sized = imageQuality?.sizes?.[sizeName];
3494 + if (typeof sized === "number") {
3495 + return sized / 100;
3496 + }
3497 + return defaultQuality;
3498 + };
2311 3499 let thumbnailTranscodeOperation = null;
2312 - if (outputMimeType) {
3500 + if (!isUltraHdr && outputMimeType) {
2313 3501 thumbnailTranscodeOperation = await getTranscodeImageOperation(
2314 3502 thumbnailSource,
2315 3503 outputMimeType,
2316 - interlaced
3504 + interlaced,
3505 + defaultQuality
2317 3506 );
2318 3507 }
2319 3508 const dimensionGroups = /* @__PURE__ */ new Map();
2320 3509 for (const name of sizesToGenerate) {
@@ -2334,13 +3523,23 @@
2334 3523 }
2335 3524 }
2336 3525 for (const [, names] of dimensionGroups) {
2337 3526 const imageSize = allImageSizes[names[0]];
3527 + const sizeQuality = qualityForSize(names[0]);
2338 3528 const thumbnailOperations = [
2339 - [OperationType.ResizeCrop, { resize: imageSize }]
3529 + [
3530 + OperationType.ResizeCrop,
3531 + { resize: imageSize, quality: sizeQuality }
3532 + ]
2340 3533 ];
2341 - if (thumbnailTranscodeOperation) {
2342 - thumbnailOperations.push(thumbnailTranscodeOperation);
3534 + if (!isUltraHdr && thumbnailTranscodeOperation) {
3535 + thumbnailOperations.push([
3536 + thumbnailTranscodeOperation[0],
3537 + {
3538 + ...thumbnailTranscodeOperation[1],
3539 + outputQuality: sizeQuality
3540 + }
3541 + ]);
2343 3542 }
2344 3543 thumbnailOperations.push(OperationType.Upload);
2345 3544 const imageSizeParam = names.length === 1 ? names[0] : names;
2346 3545 dispatch.addSideloadItem({
@@ -2372,13 +3571,14 @@
2372 3571 resize: {
2373 3572 width: bigImageSizeThreshold,
2374 3573 height: bigImageSizeThreshold
2375 3574 },
2376 - isThresholdResize: true
3575 + isThresholdResize: true,
3576 + quality: defaultQuality
2377 3577 }
2378 3578 ]
2379 3579 ];
2380 - if (thumbnailTranscodeOperation) {
3580 + if (!isUltraHdr && thumbnailTranscodeOperation) {
2381 3581 scaledOperations.push(
2382 3582 thumbnailTranscodeOperation
2383 3583 );
2384 3584 }
@@ -2408,16 +3608,32 @@
2408 3608 return;
2409 3609 }
2410 3610 const attachment = item.attachment;
2411 3611 const { mediaFinalize } = select2.getSettings();
3612 + const updates = {};
2412 3613 if (attachment?.id && mediaFinalize) {
2413 3614 try {
2414 - await mediaFinalize(attachment.id, item.subSizes || []);
3615 + const updatedAttachment = await mediaFinalize(
3616 + attachment.id,
3617 + item.subSizes || []
3618 + );
3619 + if (updatedAttachment) {
3620 + updates.attachment = updatedAttachment;
3621 + }
2415 3622 } catch (error) {
2416 3623 console.warn("Media finalization failed:", error);
3624 + dispatch.cancelItem(
3625 + id,
3626 + new UploadError({
3627 + code: ErrorCode.MEDIA_FINALIZE_ERROR,
3628 + message: (0, import_i18n7.__)("Could not finalize the upload."),
3629 + file: item.file
3630 + })
3631 + );
3632 + return;
2417 3633 }
2418 3634 }
2419 - dispatch.finishOperation(id, {});
3635 + dispatch.finishOperation(id, updates);
2420 3636 };
2421 3637 }
2422 3638 function revokeBlobUrls(id) {
2423 3639 return async ({ select: select2, dispatch }) => {
@@ -2520,7 +3736,115 @@
2520 3736 }, [settings, updateSettings2]);
2521 3737 return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
2522 3738 });
2523 3739 var provider_default = MediaUploadProvider;
3740 +
3741 + // packages/upload-media/build-module/error-messages.mjs
3742 + var import_i18n8 = __toESM(require_i18n(), 1);
3743 + function getErrorMessage(code, fileName) {
3744 + const messages = {
3745 + [ErrorCode.EMPTY_FILE]: {
3746 + title: (0, import_i18n8.__)("Empty file"),
3747 + description: (0, import_i18n8.sprintf)(
3748 + /* translators: %s: file name */
3749 + (0, import_i18n8.__)('"%s" is empty.'),
3750 + fileName
3751 + ),
3752 + action: (0, import_i18n8.__)("Please choose a different file.")
3753 + },
3754 + [ErrorCode.SIZE_ABOVE_LIMIT]: {
3755 + title: (0, import_i18n8.__)("File too large"),
3756 + description: (0, import_i18n8.sprintf)(
3757 + /* translators: %s: file name */
3758 + (0, import_i18n8.__)('"%s" exceeds the maximum upload size.'),
3759 + fileName
3760 + ),
3761 + action: (0, import_i18n8.__)("Please reduce the file size and try again.")
3762 + },
3763 + [ErrorCode.MIME_TYPE_NOT_SUPPORTED]: {
3764 + title: (0, import_i18n8.__)("Unsupported file type"),
3765 + description: (0, import_i18n8.sprintf)(
3766 + /* translators: %s: file name */
3767 + (0, import_i18n8.__)('"%s" is not a supported file type.'),
3768 + fileName
3769 + ),
3770 + action: (0, import_i18n8.__)("Please upload a different file format.")
3771 + },
3772 + [ErrorCode.MIME_TYPE_NOT_ALLOWED_FOR_USER]: {
3773 + title: (0, import_i18n8.__)("File type not allowed"),
3774 + description: (0, import_i18n8.sprintf)(
3775 + /* translators: %s: file name */
3776 + (0, import_i18n8.__)('You are not allowed to upload "%s".'),
3777 + fileName
3778 + ),
3779 + action: (0, import_i18n8.__)("Please contact your site administrator.")
3780 + },
3781 + [ErrorCode.HEIC_DECODE_ERROR]: {
3782 + title: (0, import_i18n8.__)("HEIC decode failed"),
3783 + description: (0, import_i18n8.sprintf)(
3784 + /* translators: %s: file name */
3785 + (0, import_i18n8.__)('Failed to decode HEIC file "%s".'),
3786 + fileName
3787 + ),
3788 + action: getHeicConversionAdvice()
3789 + },
3790 + [ErrorCode.IMAGE_TRANSCODING_ERROR]: {
3791 + title: (0, import_i18n8.__)("Image processing failed"),
3792 + description: (0, import_i18n8.sprintf)(
3793 + /* translators: %s: file name */
3794 + (0, import_i18n8.__)('Failed to process "%s".'),
3795 + fileName
3796 + ),
3797 + action: (0, import_i18n8.__)("The image may be corrupted. Try a different file.")
3798 + },
3799 + [ErrorCode.IMAGE_ROTATION_ERROR]: {
3800 + title: (0, import_i18n8.__)("Image rotation failed"),
3801 + description: (0, import_i18n8.sprintf)(
3802 + /* translators: %s: file name */
3803 + (0, import_i18n8.__)('Failed to rotate "%s".'),
3804 + fileName
3805 + ),
3806 + action: (0, import_i18n8.__)("The image may be corrupted. Try a different file.")
3807 + },
3808 + [ErrorCode.MEDIA_TRANSCODING_ERROR]: {
3809 + title: (0, import_i18n8.__)("Media processing failed"),
3810 + description: (0, import_i18n8.sprintf)(
3811 + /* translators: %s: file name */
3812 + (0, import_i18n8.__)('Failed to convert "%s" to the target format.'),
3813 + fileName
3814 + ),
3815 + action: (0, import_i18n8.__)("The file may be corrupted. Try a different file.")
3816 + },
3817 + [ErrorCode.MEDIA_FINALIZE_ERROR]: {
3818 + title: (0, import_i18n8.__)("Upload failed"),
3819 + description: (0, import_i18n8.sprintf)(
3820 + /* translators: %s: file name */
3821 + (0, import_i18n8.__)('Could not finalize the upload of "%s".'),
3822 + fileName
3823 + ),
3824 + action: (0, import_i18n8.__)("Please try again.")
3825 + },
3826 + [ErrorCode.GENERAL]: {
3827 + title: (0, import_i18n8.__)("Upload failed"),
3828 + description: (0, import_i18n8.sprintf)(
3829 + /* translators: %s: file name */
3830 + (0, import_i18n8.__)('Failed to upload "%s".'),
3831 + fileName
3832 + ),
3833 + action: (0, import_i18n8.__)("Please try again.")
3834 + }
3835 + };
3836 + return messages[code] || {
3837 + title: (0, import_i18n8.__)("Upload failed"),
3838 + description: (0, import_i18n8.sprintf)(
3839 + /* translators: %s: file name */
3840 + (0, import_i18n8.__)('Failed to upload "%s".'),
3841 + fileName
3842 + ),
3843 + action: (0, import_i18n8.__)("Please try again.")
3844 + };
3845 + }
2524 3846 return __toCommonJS(index_exports);
3847 +})();
3848 +(window.wp ||= {}).uploadMedia = wp.uploadMedia;
2525 3849 })();
2526 3850 //# sourceMappingURL=index.js.map