PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
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 → 24.1.0 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