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 +1382 -121 23.3.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;
@@ -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 = [];
@@ -502,8 +570,47 @@
502 570
503 571 // packages/upload-media/build-module/store/actions.mjs
504 572 var import_i18n5 = __toESM(require_i18n(), 1);
505 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 +
506 613 // packages/upload-media/build-module/image-file.mjs
507 614 var ImageFile = class extends File {
508 615 width = 0;
509 616 height = 0;
@@ -554,8 +661,72 @@
554 661 }
555 662 function getFileBasename(name) {
556 663 return name.includes(".") ? name.split(".").slice(0, -1).join(".") : name;
557 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 + }
558 729
559 730 // packages/upload-media/build-module/store/utils/index.mjs
560 731 var vipsModulePromise;
561 732 var vipsModule;
@@ -569,9 +740,9 @@
569 740 );
570 741 }
571 742 return vipsModulePromise;
572 743 }
573 - async function vipsConvertImageFormat(id, file, type, quality, interlaced) {
744 + async function vipsConvertImageFormat(id, file, type, options = {}) {
574 745 const { vipsConvertImageFormat: convertImageFormat } = await loadVipsModule();
575 746 const buffer = await convertImageFormat(
576 747 id,
577 748 await file.arrayBuffer(),
@@ -576,10 +747,9 @@
576 747 id,
577 748 await file.arrayBuffer(),
578 749 file.type,
579 750 type,
580 - quality,
581 - interlaced
751 + options
582 752 );
583 753 const ext = type.split("/")[1];
584 754 const fileName = `${getFileBasename(file.name)}.${ext}`;
585 755 return new File([new Blob([buffer])], fileName, {
@@ -593,21 +763,32 @@
593 763 throw new Error(`Failed to fetch image: ${response.status}`);
594 764 }
595 765 return hasTransparency(await response.arrayBuffer());
596 766 }
597 - 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;
598 781 if (signal?.aborted) {
599 782 throw new Error("Operation aborted");
600 783 }
601 784 const { vipsResizeImage: resizeImage } = await loadVipsModule();
602 - const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(
603 - id,
604 - await file.arrayBuffer(),
605 - file.type,
606 - resize,
785 + const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(id, await file.arrayBuffer(), file.type, resize, {
607 786 smartCrop,
608 - quality
609 - );
787 + quality,
788 + stripMeta,
789 + maxBitdepth
790 + });
610 791 let fileName = file.name;
611 792 const wasResized = originalWidth > width || originalHeight > height;
612 793 if (wasResized) {
613 794 const basename = getFileBasename(file.name);
@@ -684,9 +865,162 @@
684 865 completedVipsOperations = 0;
685 866 }
686 867 }
687 868
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 + }
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 +
688 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 || {});
689 1023 var UploadError = class extends Error {
690 1024 code;
691 1025 file;
692 1026 constructor({ code, message, file, cause }) {
@@ -710,9 +1044,9 @@
710 1044 return file.type.startsWith(`${allowedType}/`);
711 1045 });
712 1046 if (file.type && !isAllowedType) {
713 1047 throw new UploadError({
714 - code: "MIME_TYPE_NOT_SUPPORTED",
1048 + code: ErrorCode.MIME_TYPE_NOT_SUPPORTED,
715 1049 message: (0, import_i18n2.sprintf)(
716 1050 // translators: %s: file name.
717 1051 (0, import_i18n2.__)("%s: Sorry, this file type is not supported here."),
718 1052 file.name
@@ -754,9 +1088,9 @@
754 1088 file.type
755 1089 );
756 1090 if (file.type && !isAllowedMimeTypeForUser) {
757 1091 throw new UploadError({
758 - code: "MIME_TYPE_NOT_ALLOWED_FOR_USER",
1092 + code: ErrorCode.MIME_TYPE_NOT_ALLOWED_FOR_USER,
759 1093 message: (0, import_i18n3.sprintf)(
760 1094 // translators: %s: file name.
761 1095 (0, import_i18n3.__)(
762 1096 "%s: Sorry, you are not allowed to upload this file type."
@@ -772,9 +1106,9 @@
772 1106 var import_i18n4 = __toESM(require_i18n(), 1);
773 1107 function validateFileSize(file, maxUploadFileSize) {
774 1108 if (file.size <= 0) {
775 1109 throw new UploadError({
776 - code: "EMPTY_FILE",
1110 + code: ErrorCode.EMPTY_FILE,
777 1111 message: (0, import_i18n4.sprintf)(
778 1112 // translators: %s: file name.
779 1113 (0, import_i18n4.__)("%s: This file is empty."),
780 1114 file.name
@@ -783,9 +1117,9 @@
783 1117 });
784 1118 }
785 1119 if (maxUploadFileSize && file.size > maxUploadFileSize) {
786 1120 throw new UploadError({
787 - code: "SIZE_ABOVE_LIMIT",
1121 + code: ErrorCode.SIZE_ABOVE_LIMIT,
788 1122 message: (0, import_i18n4.sprintf)(
789 1123 // translators: %s: file name.
790 1124 (0, import_i18n4.__)(
791 1125 "%s: This file exceeds the maximum upload size for this site."
@@ -846,10 +1180,26 @@
846 1180 const item = select2.getItem(id);
847 1181 if (!item) {
848 1182 return;
849 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 + }
850 1197 item.abortController?.abort();
851 - await vipsCancelOperations(id);
1198 + vipsCancelOperations(id).catch(() => {
1199 + });
1200 + cancelGifToVideoOperations(id).catch(() => {
1201 + });
852 1202 if (!silent) {
853 1203 const { onError } = item;
854 1204 onError?.(error ?? new Error("Upload cancelled"));
855 1205 if (!onError && error && !item.parentId) {
@@ -854,8 +1204,12 @@
854 1204 onError?.(error ?? new Error("Upload cancelled"));
855 1205 if (!onError && error && !item.parentId) {
856 1206 console.error("Upload cancelled", error);
857 1207 }
1208 + } else {
1209 + debug(
1210 + `Item cancelled: ${item.file.name} (item ${id}): ${error instanceof Error ? error.message : error}`
1211 + );
858 1212 }
859 1213 const { currentOperation, parentId, batchId } = item;
860 1214 dispatch({
861 1215 type: Type.Cancel,
@@ -873,8 +1227,13 @@
873 1227 for (const pending of select2.getPendingUploads()) {
874 1228 dispatch.processItem(pending.id);
875 1229 }
876 1230 }
1231 + if (currentOperation === OperationType.TranscodeGif) {
1232 + for (const pending of select2.getPendingVideoProcessing()) {
1233 + dispatch.processItem(pending.id);
1234 + }
1235 + }
877 1236 if (currentOperation === OperationType.ResizeCrop || currentOperation === OperationType.Rotate || currentOperation === OperationType.TranscodeImage) {
878 1237 maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
879 1238 }
880 1239 if (parentId) {
@@ -879,13 +1238,14 @@
879 1238 }
880 1239 if (parentId) {
881 1240 const parentItem = select2.getItem(parentId);
882 1241 if (parentItem) {
1242 + const isOptionalCompanion = item.additionalData?.image_size === "animated_video" || item.additionalData?.image_size === "animated_video_poster";
883 1243 if (select2.hasPendingItemsByParentId(parentId)) {
884 1244 if (parentItem.operations && parentItem.operations.length > 0) {
885 1245 dispatch.processItem(parentId);
886 1246 }
887 - } else if (parentItem.subSizes && parentItem.subSizes.length > 0) {
1247 + } else if (parentItem.subSizes && parentItem.subSizes.length > 0 || isOptionalCompanion) {
888 1248 if (parentItem.operations && parentItem.operations.length > 0) {
889 1249 dispatch.processItem(parentId);
890 1250 }
891 1251 } else {
@@ -894,12 +1254,12 @@
894 1254 if (parentAttachmentId && mediaDelete) {
895 1255 mediaDelete(parentAttachmentId).catch(() => {
896 1256 });
897 1257 }
898 - dispatch.cancelItem(
1258 + await dispatch.cancelItem(
899 1259 parentId,
900 1260 new UploadError({
901 - code: error instanceof UploadError && error.code || "UPLOAD_ERROR",
1261 + code: error instanceof UploadError && error.code || ErrorCode.GENERAL,
902 1262 message: error?.message || (0, import_i18n5.__)("The image could not be uploaded."),
903 1263 file: parentItem.file,
904 1264 cause: error instanceof Error ? error : void 0
905 1265 })
@@ -907,8 +1267,9 @@
907 1267 }
908 1268 }
909 1269 }
910 1270 if (batchId && select2.isBatchUploaded(batchId)) {
1271 + debug(`Batch completed: ${batchId}`);
911 1272 item.onBatchSuccess?.();
912 1273 }
913 1274 };
914 1275 }
@@ -927,8 +1288,57 @@
927 1288 });
928 1289 dispatch.processItem(id);
929 1290 };
930 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 + }
931 1341
932 1342 // packages/upload-media/build-module/store/private-actions.mjs
933 1343 var private_actions_exports = {};
934 1344 __export(private_actions_exports, {
@@ -933,8 +1343,9 @@
933 1343 var private_actions_exports = {};
934 1344 __export(private_actions_exports, {
935 1345 addItem: () => addItem,
936 1346 addSideloadItem: () => addSideloadItem,
1347 + detectUltraHdr: () => detectUltraHdr,
937 1348 finalizeItem: () => finalizeItem,
938 1349 finishOperation: () => finishOperation,
939 1350 generateThumbnails: () => generateThumbnails,
940 1351 getTranscodeImageOperation: () => getTranscodeImageOperation,
@@ -947,8 +1358,9 @@
947 1358 resumeQueue: () => resumeQueue,
948 1359 revokeBlobUrls: () => revokeBlobUrls,
949 1360 rotateItem: () => rotateItem,
950 1361 sideloadItem: () => sideloadItem,
1362 + transcodeGifItem: () => transcodeGifItem,
951 1363 transcodeImageItem: () => transcodeImageItem,
952 1364 updateItemProgress: () => updateItemProgress,
953 1365 updateSettings: () => updateSettings,
954 1366 uploadItem: () => uploadItem
@@ -953,9 +1365,9 @@
953 1365 updateSettings: () => updateSettings,
954 1366 uploadItem: () => uploadItem
955 1367 });
956 1368 var import_blob = __toESM(require_blob(), 1);
957 - var import_i18n6 = __toESM(require_i18n(), 1);
1369 + var import_i18n7 = __toESM(require_i18n(), 1);
958 1370
959 1371 // packages/upload-media/build-module/heic-parser.mjs
960 1372 var Reader = class {
961 1373 view;
@@ -1226,8 +1638,15 @@
1226 1638 return `hvc1.${spacePrefix}${profileIdc}.${compatHex}.${tierChar}${levelIdc}${constraintStr}`;
1227 1639 }
1228 1640 function readItemData(buffer, loc, idatOffset) {
1229 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 + }
1230 1649 if (loc.extents.length === 1) {
1231 1650 const ext = loc.extents[0];
1232 1651 const start = baseOffset + ext.offset;
1233 1652 return new Uint8Array(buffer.slice(start, start + ext.length));
@@ -1362,9 +1781,10 @@
1362 1781 tileWidth: width,
1363 1782 tileHeight: height,
1364 1783 outputWidth: width,
1365 1784 outputHeight: height,
1366 - rotation
1785 + rotation,
1786 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1367 1787 };
1368 1788 }
1369 1789 function parseGridImage(r, buffer, gridItemId, locations, allAssoc, properties, irefBox, idatOffset) {
1370 1790 const gridLoc = locations.get(gridItemId);
@@ -1455,13 +1875,269 @@
1455 1875 tileWidth,
1456 1876 tileHeight,
1457 1877 outputWidth,
1458 1878 outputHeight,
1459 - rotation
1879 + rotation,
1880 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1460 1881 };
1461 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 + }
1462 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 +
1463 2136 // packages/upload-media/build-module/canvas-utils.mjs
2137 + var IMAGE_DECODER_TIMEOUT = 15e3;
2138 + var HeicUnsupportedError = class extends Error {
2139 + };
1464 2140 async function canvasConvertToJpeg(file, quality = 0.82) {
1465 2141 const baseName = getFileBasename(file.name);
1466 2142 try {
1467 2143 const bitmap = await createImageBitmap(file);
@@ -1490,10 +2166,36 @@
1490 2166 const decoder = new ImageDecoder({
1491 2167 type: file.type,
1492 2168 data: file.stream()
1493 2169 });
2170 + let videoFrame;
2171 + let timeoutId;
2172 + let timedOut = false;
1494 2173 try {
1495 - 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) {
1496 2198 try {
1497 2199 const canvas = new OffscreenCanvas(
1498 2200 videoFrame.displayWidth,
1499 2201 videoFrame.displayHeight
@@ -1512,57 +2214,62 @@
1512 2214 });
1513 2215 } finally {
1514 2216 videoFrame.close();
1515 2217 }
1516 - } finally {
1517 - decoder.close();
1518 2218 }
1519 2219 }
1520 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 + }
1521 2229 if (typeof VideoDecoder !== "undefined") {
2230 + let supported = false;
1522 2231 try {
1523 - const heicData = parseHeic(await file.arrayBuffer());
1524 2232 const support = await VideoDecoder.isConfigSupported({
1525 2233 codec: heicData.codecString
1526 2234 });
1527 - if (support.supported) {
1528 - const canvas = new OffscreenCanvas(
1529 - heicData.outputWidth,
1530 - 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
1531 2254 );
1532 - const ctx = canvas.getContext("2d");
1533 - if (!ctx) {
1534 - throw new Error("Could not get canvas 2d context");
2255 + try {
2256 + ctx.drawImage(frame, tile.x, tile.y);
2257 + } finally {
2258 + frame.close();
1535 2259 }
1536 - for (const tile of heicData.tiles) {
1537 - const frame = await decodeHevcFrame(
1538 - heicData.codecString,
1539 - heicData.description,
1540 - heicData.tileWidth,
1541 - heicData.tileHeight,
1542 - tile.data
1543 - );
1544 - try {
1545 - ctx.drawImage(frame, tile.x, tile.y);
1546 - } finally {
1547 - frame.close();
1548 - }
1549 - }
1550 - const outputCanvas = applyRotation(canvas, heicData.rotation);
1551 - const jpegBlob = await outputCanvas.convertToBlob({
1552 - type: "image/jpeg",
1553 - quality
1554 - });
1555 - return new File([jpegBlob], `${baseName}.jpg`, {
1556 - type: "image/jpeg"
1557 - });
1558 2260 }
1559 - } 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 + });
1560 2269 }
1561 2270 }
1562 - throw new Error(
1563 - "This browser cannot decode HEIC images. Please use Safari or convert to JPEG before uploading."
1564 - );
2271 + throw new HeicUnsupportedError(getHeicUnsupportedMessage());
1565 2272 }
1566 2273 function applyRotation(source, rotation) {
1567 2274 if (rotation === 0) {
1568 2275 return source;
@@ -1579,8 +2286,45 @@
1579 2286 ctx.rotate(-rotation * Math.PI / 180);
1580 2287 ctx.drawImage(source, -source.width / 2, -source.height / 2);
1581 2288 return rotated;
1582 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 + }
1583 2327 function decodeHevcFrame(codec, description, width, height, data) {
1584 2328 return new Promise((resolve, reject) => {
1585 2329 const decoder = new VideoDecoder({
1586 2330 output: (frame) => {
@@ -1707,9 +2451,94 @@
1707 2451 }
1708 2452 function clearFeatureDetectionCache() {
1709 2453 cachedResult = null;
1710 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 + }
1711 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 +
1712 2541 // packages/upload-media/build-module/stub-file.mjs
1713 2542 var StubFile = class extends File {
1714 2543 constructor(fileName = "stub-file") {
1715 2544 super([], fileName);
@@ -1717,8 +2546,9 @@
1717 2546 };
1718 2547
1719 2548 // packages/upload-media/build-module/store/private-actions.mjs
1720 2549 var DEFAULT_OUTPUT_QUALITY = 0.82;
2550 + var ultraHdrItems = /* @__PURE__ */ new Set();
1721 2551 function addItem({
1722 2552 file: fileOrBlob,
1723 2553 batchId,
1724 2554 onChange,
@@ -1809,8 +2639,11 @@
1809 2639 const item = select2.getItem(id);
1810 2640 if (!item) {
1811 2641 return;
1812 2642 }
2643 + if (item.currentOperation) {
2644 + return;
2645 + }
1813 2646 const {
1814 2647 attachment,
1815 2648 onChange,
1816 2649 onSuccess,
@@ -1833,8 +2666,14 @@
1833 2666 if (activeCount >= settings.maxConcurrentImageProcessing) {
1834 2667 return;
1835 2668 }
1836 2669 }
2670 + if (operation === OperationType.TranscodeGif) {
2671 + const activeCount = select2.getActiveVideoProcessingCount();
2672 + if (activeCount >= 1) {
2673 + return;
2674 + }
2675 + }
1837 2676 if (attachment) {
1838 2677 const isHeicUrl = attachment.url && /\.hei[cf]$/i.test(attachment.url);
1839 2678 if (!isHeicUrl) {
1840 2679 onChange?.([attachment]);
@@ -1878,8 +2717,11 @@
1878 2717 type: Type.OperationStart,
1879 2718 id,
1880 2719 operation
1881 2720 });
2721 + debug(
2722 + `Starting operation ${operation} for ${item.file.name} (item ${item.id})`
2723 + );
1882 2724 switch (operation) {
1883 2725 case OperationType.Prepare:
1884 2726 dispatch.prepareItem(item.id);
1885 2727 break;
@@ -1900,8 +2742,14 @@
1900 2742 item.id,
1901 2743 operationArgs
1902 2744 );
1903 2745 break;
2746 + case OperationType.TranscodeGif:
2747 + dispatch.transcodeGifItem(
2748 + item.id,
2749 + operationArgs
2750 + );
2751 + break;
1904 2752 case OperationType.Upload:
1905 2753 if (item.parentId) {
1906 2754 dispatch.sideloadItem(id);
1907 2755 } else {
@@ -1913,8 +2761,11 @@
1913 2761 break;
1914 2762 case OperationType.Finalize:
1915 2763 dispatch.finalizeItem(id);
1916 2764 break;
2765 + case OperationType.DetectUltraHdr:
2766 + dispatch.detectUltraHdr(id);
2767 + break;
1917 2768 }
1918 2769 };
1919 2770 }
1920 2771 function pauseQueue() {
@@ -1927,9 +2778,13 @@
1927 2778 dispatch({
1928 2779 type: Type.ResumeQueue
1929 2780 });
1930 2781 for (const item of select2.getAllItems()) {
1931 - dispatch.processItem(item.id);
2782 + if (item.status === ItemStatus.PendingRetry) {
2783 + dispatch.executeRetry(item.id);
2784 + } else {
2785 + dispatch.processItem(item.id);
2786 + }
1932 2787 }
1933 2788 };
1934 2789 }
1935 2790 function pauseItem(id) {
@@ -1945,8 +2800,10 @@
1945 2800 const item = select2.getItem(id);
1946 2801 if (!item) {
1947 2802 return;
1948 2803 }
2804 + ultraHdrItems.delete(id);
2805 + clearRetryTimer(id);
1949 2806 dispatch({
1950 2807 type: Type.Remove,
1951 2808 id
1952 2809 });
@@ -1951,8 +2808,9 @@
1951 2808 id
1952 2809 });
1953 2810 if (select2.getAllItems().length === 0) {
1954 2811 terminateVipsWorker();
2812 + terminateVideoConversionWorker();
1955 2813 }
1956 2814 };
1957 2815 }
1958 2816 function finishOperation(id, updates) {
@@ -1976,8 +2834,14 @@
1976 2834 for (const pendingItem of pendingItems) {
1977 2835 dispatch.processItem(pendingItem.id);
1978 2836 }
1979 2837 }
2838 + if (previousOperation === OperationType.TranscodeGif) {
2839 + const pendingItems = select2.getPendingVideoProcessing();
2840 + for (const pendingItem of pendingItems) {
2841 + dispatch.processItem(pendingItem.id);
2842 + }
2843 + }
1980 2844 if (previousOperation === OperationType.ResizeCrop || previousOperation === OperationType.Rotate || previousOperation === OperationType.TranscodeImage) {
1981 2845 maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
1982 2846 }
1983 2847 };
@@ -1985,9 +2849,9 @@
1985 2849 var VALID_IMAGE_FORMATS = ["jpeg", "webp", "avif", "png", "gif"];
1986 2850 function isValidImageFormat(format) {
1987 2851 return VALID_IMAGE_FORMATS.includes(format);
1988 2852 }
1989 - async function getTranscodeImageOperation(file, outputMimeType, interlaced = false) {
2853 + async function getTranscodeImageOperation(file, outputMimeType, interlaced = false, quality = DEFAULT_OUTPUT_QUALITY) {
1990 2854 if (file.type === "image/png" && outputMimeType === "image/jpeg") {
1991 2855 const blobUrl = (0, import_blob.createBlobURL)(file);
1992 2856 try {
1993 2857 const hasAlpha = await vipsHasTransparency(blobUrl);
@@ -2007,9 +2871,9 @@
2007 2871 return [
2008 2872 OperationType.TranscodeImage,
2009 2873 {
2010 2874 outputFormat: formatPart,
2011 - outputQuality: DEFAULT_OUTPUT_QUALITY,
2875 + outputQuality: quality,
2012 2876 interlaced
2013 2877 }
2014 2878 ];
2015 2879 }
@@ -2021,15 +2885,63 @@
2021 2885 }
2022 2886 const { file } = item;
2023 2887 const operations = [];
2024 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 + }
2025 2924 let heicJpeg = null;
2026 - const isImage = file.type.startsWith("image/");
2027 - const isVipsSupported = CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(
2028 - file.type
2029 - );
2030 - 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;
2031 2934 if (isImage && isVipsSupported) {
2935 + const dimensions = await getImageDimensions(file);
2936 + if (dimensions && exceedsClientProcessingMemory(dimensions)) {
2937 + tooLargeForClient = true;
2938 + }
2939 + }
2940 + if (file.type === "image/jpeg" && !isHeic && !tooLargeForClient) {
2941 + operations.push(OperationType.DetectUltraHdr);
2942 + }
2943 + if (isImage && isVipsSupported && !tooLargeForClient) {
2032 2944 operations.push(
2033 2945 OperationType.Upload,
2034 2946 OperationType.ThumbnailGeneration,
2035 2947 OperationType.Finalize
@@ -2036,18 +2948,22 @@
2036 2948 );
2037 2949 } else if (isImage && isHeic) {
2038 2950 try {
2039 2951 heicJpeg = await canvasConvertToJpeg(
2040 - file,
2952 + heicFile,
2041 2953 settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY
2042 2954 );
2043 - } catch {
2955 + } catch (error) {
2956 + const unsupported = error instanceof HeicUnsupportedError;
2044 2957 dispatch.cancelItem(
2045 2958 id,
2046 2959 new UploadError({
2047 - code: "HEIC_DECODE_ERROR",
2048 - message: "This browser cannot decode HEIC images and the server does not support them either. Please convert to JPEG before uploading.",
2049 - 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
2050 2966 })
2051 2967 );
2052 2968 return;
2053 2969 }
@@ -2069,9 +2985,9 @@
2069 2985 const vipsAvailable = isClientSideMediaSupported();
2070 2986 updates = {
2071 2987 file: heicJpeg,
2072 2988 sourceFile: heicJpeg,
2073 - originalHeicFile: item.file,
2989 + originalHeicFile: heicFile,
2074 2990 additionalData: {
2075 2991 ...item.additionalData,
2076 2992 generate_sub_sizes: !vipsAvailable,
2077 2993 convert_format: true
@@ -2076,9 +2992,9 @@
2076 2992 generate_sub_sizes: !vipsAvailable,
2077 2993 convert_format: true
2078 2994 }
2079 2995 };
2080 - } else if (!isVipsSupported || !isImage) {
2996 + } else if (!isVipsSupported || !isImage || tooLargeForClient) {
2081 2997 updates = {
2082 2998 additionalData: {
2083 2999 ...item.additionalData,
2084 3000 generate_sub_sizes: true,
@@ -2095,8 +3011,26 @@
2095 3011 }
2096 3012 dispatch.finishOperation(id, updates);
2097 3013 };
2098 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 + }
2099 3033 function uploadItem(id) {
2100 3034 return async ({ select: select2, dispatch }) => {
2101 3035 const item = select2.getItem(id);
2102 3036 if (!item) {
@@ -2101,23 +3035,42 @@
2101 3035 const item = select2.getItem(id);
2102 3036 if (!item) {
2103 3037 return;
2104 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 + };
2105 3057 select2.getSettings().mediaUpload({
2106 3058 filesList: [item.file],
2107 3059 additionalData: item.additionalData,
2108 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,
2109 3066 onFileChange: ([attachment]) => {
2110 3067 if (attachment && !(0, import_blob.isBlobURL)(attachment.url)) {
2111 - dispatch.finishOperation(id, {
2112 - attachment
2113 - });
3068 + finishUpload(attachment);
2114 3069 }
2115 3070 },
2116 3071 onSuccess: ([attachment]) => {
2117 - dispatch.finishOperation(id, {
2118 - attachment
2119 - });
3072 + finishUpload(attachment);
2120 3073 },
2121 3074 onError: (error) => {
2122 3075 dispatch.cancelItem(id, error);
2123 3076 }
@@ -2135,8 +3088,9 @@
2135 3088 if (!mediaSideload) {
2136 3089 dispatch.finishOperation(id, {});
2137 3090 return;
2138 3091 }
3092 + const startTime = performance.now();
2139 3093 mediaSideload({
2140 3094 file: item.file,
2141 3095 attachmentId: post,
2142 3096 additionalData,
@@ -2141,8 +3095,17 @@
2141 3095 attachmentId: post,
2142 3096 additionalData,
2143 3097 signal: item.abortController?.signal,
2144 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 + });
2145 3108 if (item.parentId) {
2146 3109 dispatch({
2147 3110 type: Type.AccumulateSubSize,
2148 3111 id: item.parentId,
@@ -2168,21 +3131,36 @@
2168 3131 file: item.file
2169 3132 });
2170 3133 return;
2171 3134 }
3135 + const startTime = performance.now();
2172 3136 const addSuffix = Boolean(item.parentId);
2173 3137 const scaledSuffix = Boolean(args.isThresholdResize);
3138 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2174 3139 try {
2175 3140 const file = await vipsResizeImage(
2176 3141 item.id,
2177 3142 item.file,
2178 3143 args.resize,
2179 - false,
2180 - // smartCrop
2181 - addSuffix,
2182 - item.abortController?.signal,
2183 - 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 + }
2184 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 + });
2185 3163 const blobUrl = (0, import_blob.createBlobURL)(file);
2186 3164 dispatch({
2187 3165 type: Type.CacheBlobUrl,
2188 3166 id,
@@ -2197,10 +3175,10 @@
2197 3175 } catch (error) {
2198 3176 dispatch.cancelItem(
2199 3177 id,
2200 3178 new UploadError({
2201 - code: "IMAGE_TRANSCODING_ERROR",
2202 - message: (0, import_i18n6.__)(
3179 + code: ErrorCode.IMAGE_TRANSCODING_ERROR,
3180 + message: (0, import_i18n7.__)(
2203 3181 "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
2204 3182 ),
2205 3183 file: item.file,
2206 3184 cause: error instanceof Error ? error : void 0
@@ -2220,8 +3198,9 @@
2220 3198 file: item.file
2221 3199 });
2222 3200 return;
2223 3201 }
3202 + const startTime = performance.now();
2224 3203 try {
2225 3204 const file = await vipsRotateImage(
2226 3205 item.id,
2227 3206 item.file,
@@ -2227,8 +3206,17 @@
2227 3206 item.file,
2228 3207 args.orientation,
2229 3208 item.abortController?.signal
2230 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 + });
2231 3219 const blobUrl = (0, import_blob.createBlobURL)(file);
2232 3220 dispatch({
2233 3221 type: Type.CacheBlobUrl,
2234 3222 id,
@@ -2243,10 +3231,10 @@
2243 3231 } catch (error) {
2244 3232 dispatch.cancelItem(
2245 3233 id,
2246 3234 new UploadError({
2247 - code: "IMAGE_ROTATION_ERROR",
2248 - message: (0, import_i18n6.__)(
3235 + code: ErrorCode.IMAGE_ROTATION_ERROR,
3236 + message: (0, import_i18n7.__)(
2249 3237 "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
2250 3238 ),
2251 3239 file: item.file,
2252 3240 cause: error instanceof Error ? error : void 0
@@ -2266,19 +3254,34 @@
2266 3254 file: item.file
2267 3255 });
2268 3256 return;
2269 3257 }
3258 + const startTime = performance.now();
2270 3259 const outputMimeType = `image/${args.outputFormat}`;
2271 3260 const quality = args.outputQuality ?? DEFAULT_OUTPUT_QUALITY;
2272 3261 const interlaced = args.interlaced ?? false;
3262 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2273 3263 try {
2274 3264 const file = await vipsConvertImageFormat(
2275 3265 item.id,
2276 3266 item.file,
2277 3267 outputMimeType,
2278 - quality,
2279 - interlaced
3268 + {
3269 + quality,
3270 + interlaced,
3271 + stripMeta: imageStripMeta,
3272 + maxBitdepth: imageMaxBitDepth
3273 + }
2280 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 + });
2281 3284 const blobUrl = (0, import_blob.createBlobURL)(file);
2282 3285 dispatch({
2283 3286 type: Type.CacheBlobUrl,
2284 3287 id,
@@ -2293,9 +3296,9 @@
2293 3296 } catch (error) {
2294 3297 dispatch.cancelItem(
2295 3298 id,
2296 3299 new UploadError({
2297 - code: "MEDIA_TRANSCODING_ERROR",
3300 + code: ErrorCode.MEDIA_TRANSCODING_ERROR,
2298 3301 message: "Image could not be transcoded to the target format",
2299 3302 file: item.file,
2300 3303 cause: error instanceof Error ? error : void 0
2301 3304 })
@@ -2302,8 +3305,91 @@
2302 3305 );
2303 3306 }
2304 3307 };
2305 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 + }
2306 3392 function generateThumbnails(id) {
2307 3393 return async ({ select: select2, dispatch }) => {
2308 3394 const item = select2.getItem(id);
2309 3395 if (!item) {
@@ -2321,35 +3407,57 @@
2321 3407 batchId: v4_default(),
2322 3408 parentId: item.id,
2323 3409 additionalData: {
2324 3410 post: attachment.id,
2325 - image_size: "original-heic",
3411 + image_size: "source_original",
2326 3412 convert_format: false
2327 3413 },
2328 3414 operations: [OperationType.Upload]
2329 3415 });
2330 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;
2331 3450 {
2332 - const needsRotation = attachment.exif_orientation && attachment.exif_orientation !== 1 && !item.file.name.includes("-scaled");
2333 - if (needsRotation && attachment.id) {
3451 + const needsRotation = exifOrientation !== 1 && !item.file.name.includes("-scaled");
3452 + if ((needsRotation || needsClientRotation) && attachment.id) {
2334 3453 try {
2335 - const rotatedFile = await vipsRotateImage(
3454 + rotatedSource = await vipsRotateImage(
2336 3455 item.id,
2337 3456 item.sourceFile,
2338 - attachment.exif_orientation,
3457 + exifOrientation,
2339 3458 item.abortController?.signal
2340 3459 );
2341 - dispatch.addSideloadItem({
2342 - file: rotatedFile,
2343 - batchId: v4_default(),
2344 - parentId: item.id,
2345 - additionalData: {
2346 - post: attachment.id,
2347 - image_size: "original",
2348 - convert_format: false
2349 - },
2350 - operations: [OperationType.Upload]
2351 - });
2352 3460 } catch {
2353 3461 console.warn(
2354 3462 "Failed to rotate image, continuing with thumbnails"
2355 3463 );
@@ -2354,23 +3462,48 @@
2354 3462 "Failed to rotate image, continuing with thumbnails"
2355 3463 );
2356 3464 }
2357 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 + }
2358 3479 }
2359 3480 if (!item.parentId && attachment.missing_image_sizes && attachment.missing_image_sizes.length > 0) {
2360 3481 const allImageSizes = settings.allImageSizes || {};
2361 3482 const sizesToGenerate = attachment.missing_image_sizes;
2362 - const thumbnailSource = item.sourceFile;
3483 + const thumbnailSource = needsClientRotation && rotatedSource ? rotatedSource : item.sourceFile;
2363 3484 const file = attachment.filename ? renameFile(thumbnailSource, attachment.filename) : thumbnailSource;
2364 3485 const batchId = v4_default();
3486 + const isUltraHdr = ultraHdrItems.has(item.id);
2365 3487 const outputMimeType = attachment.image_output_format;
2366 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 + };
2367 3499 let thumbnailTranscodeOperation = null;
2368 - if (outputMimeType) {
3500 + if (!isUltraHdr && outputMimeType) {
2369 3501 thumbnailTranscodeOperation = await getTranscodeImageOperation(
2370 3502 thumbnailSource,
2371 3503 outputMimeType,
2372 - interlaced
3504 + interlaced,
3505 + defaultQuality
2373 3506 );
2374 3507 }
2375 3508 const dimensionGroups = /* @__PURE__ */ new Map();
2376 3509 for (const name of sizesToGenerate) {
@@ -2390,13 +3523,23 @@
2390 3523 }
2391 3524 }
2392 3525 for (const [, names] of dimensionGroups) {
2393 3526 const imageSize = allImageSizes[names[0]];
3527 + const sizeQuality = qualityForSize(names[0]);
2394 3528 const thumbnailOperations = [
2395 - [OperationType.ResizeCrop, { resize: imageSize }]
3529 + [
3530 + OperationType.ResizeCrop,
3531 + { resize: imageSize, quality: sizeQuality }
3532 + ]
2396 3533 ];
2397 - if (thumbnailTranscodeOperation) {
2398 - thumbnailOperations.push(thumbnailTranscodeOperation);
3534 + if (!isUltraHdr && thumbnailTranscodeOperation) {
3535 + thumbnailOperations.push([
3536 + thumbnailTranscodeOperation[0],
3537 + {
3538 + ...thumbnailTranscodeOperation[1],
3539 + outputQuality: sizeQuality
3540 + }
3541 + ]);
2399 3542 }
2400 3543 thumbnailOperations.push(OperationType.Upload);
2401 3544 const imageSizeParam = names.length === 1 ? names[0] : names;
2402 3545 dispatch.addSideloadItem({
@@ -2428,13 +3571,14 @@
2428 3571 resize: {
2429 3572 width: bigImageSizeThreshold,
2430 3573 height: bigImageSizeThreshold
2431 3574 },
2432 - isThresholdResize: true
3575 + isThresholdResize: true,
3576 + quality: defaultQuality
2433 3577 }
2434 3578 ]
2435 3579 ];
2436 - if (thumbnailTranscodeOperation) {
3580 + if (!isUltraHdr && thumbnailTranscodeOperation) {
2437 3581 scaledOperations.push(
2438 3582 thumbnailTranscodeOperation
2439 3583 );
2440 3584 }
@@ -2476,8 +3620,17 @@
2476 3620 updates.attachment = updatedAttachment;
2477 3621 }
2478 3622 } catch (error) {
2479 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;
2480 3633 }
2481 3634 }
2482 3635 dispatch.finishOperation(id, updates);
2483 3636 };
@@ -2583,7 +3736,115 @@
2583 3736 }, [settings, updateSettings2]);
2584 3737 return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
2585 3738 });
2586 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 + }
2587 3846 return __toCommonJS(index_exports);
3847 +})();
3848 +(window.wp ||= {}).uploadMedia = wp.uploadMedia;
2588 3849 })();
2589 3850 //# sourceMappingURL=index.js.map