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 +934 -142 23.5.3 → 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;
@@ -102,10 +103,13 @@
102 103 UploadError: () => UploadError,
103 104 clearFeatureDetectionCache: () => clearFeatureDetectionCache,
104 105 detectClientSideMediaSupport: () => detectClientSideMediaSupport,
105 106 getErrorMessage: () => getErrorMessage,
107 + getHeicConversionAdvice: () => getHeicConversionAdvice,
108 + getHeicUnsupportedMessage: () => getHeicUnsupportedMessage,
106 109 isClientSideMediaSupported: () => isClientSideMediaSupported,
107 110 isHeicCanvasSupported: () => isHeicCanvasSupported,
111 + isHeicFile: () => isHeicFile,
108 112 store: () => store
109 113 });
110 114
111 115 // packages/upload-media/build-module/store/index.mjs
@@ -148,8 +152,9 @@
148 152 OperationType2["Upload"] = "UPLOAD";
149 153 OperationType2["ResizeCrop"] = "RESIZE_CROP";
150 154 OperationType2["Rotate"] = "ROTATE";
151 155 OperationType2["TranscodeImage"] = "TRANSCODE_IMAGE";
156 + OperationType2["TranscodeGif"] = "TRANSCODE_GIF";
152 157 OperationType2["ThumbnailGeneration"] = "THUMBNAIL_GENERATION";
153 158 OperationType2["Finalize"] = "FINALIZE";
154 159 OperationType2["DetectUltraHdr"] = "DETECT_ULTRAHDR";
155 160 return OperationType2;
@@ -189,9 +194,10 @@
189 194 mediaUpload: noop,
190 195 maxConcurrentUploads: DEFAULT_MAX_CONCURRENT_UPLOADS,
191 196 maxConcurrentImageProcessing: DEFAULT_MAX_CONCURRENT_IMAGE_PROCESSING,
192 197 retry: { ...DEFAULT_RETRY_SETTINGS }
193 - }
198 + },
199 + failureCount: 0
194 200 };
195 201 function reducer(state = DEFAULT_STATE, action = { type: Type.Unknown }) {
196 202 switch (action.type) {
197 203 case Type.PauseQueue: {
@@ -230,11 +236,15 @@
230 236 return {
231 237 ...state,
232 238 queue: [...state.queue, action.item]
233 239 };
234 - case Type.Cancel:
240 + case Type.Cancel: {
241 + const cancelled = state.queue.find(
242 + (item) => item.id === action.id
243 + );
235 244 return {
236 245 ...state,
246 + failureCount: cancelled && !cancelled.parentId ? state.failureCount + 1 : state.failureCount,
237 247 queue: state.queue.map(
238 248 (item) => item.id === action.id ? {
239 249 ...item,
240 250 error: action.error
@@ -240,8 +250,9 @@
240 250 error: action.error
241 251 } : item
242 252 )
243 253 };
254 + }
244 255 case Type.RetryItem:
245 256 return {
246 257 ...state,
247 258 queue: state.queue.map(
@@ -249,9 +260,20 @@
249 260 ...item,
250 261 status: ItemStatus.Processing,
251 262 error: void 0,
252 263 retryCount: (item.retryCount ?? 0) + 1,
253 - abortController: new AbortController()
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
254 276 } : item
255 277 )
256 278 };
257 279 case Type.ScheduleRetry:
@@ -412,15 +434,18 @@
412 434 var private_selectors_exports = {};
413 435 __export(private_selectors_exports, {
414 436 getActiveImageProcessingCount: () => getActiveImageProcessingCount,
415 437 getActiveUploadCount: () => getActiveUploadCount,
438 + getActiveVideoProcessingCount: () => getActiveVideoProcessingCount,
416 439 getAllItems: () => getAllItems,
417 440 getBlobUrls: () => getBlobUrls,
418 441 getFailedItems: () => getFailedItems,
442 + getFailureCount: () => getFailureCount,
419 443 getItem: () => getItem,
420 444 getItemProgress: () => getItemProgress,
421 445 getPendingImageProcessing: () => getPendingImageProcessing,
422 446 getPendingUploads: () => getPendingUploads,
447 + getPendingVideoProcessing: () => getPendingVideoProcessing,
423 448 hasPendingItemsByParentId: () => hasPendingItemsByParentId,
424 449 isBatchUploaded: () => isBatchUploaded,
425 450 isPaused: () => isPaused
426 451 });
@@ -426,8 +451,11 @@
426 451 });
427 452 function getAllItems(state) {
428 453 return state.queue;
429 454 }
455 + function getFailureCount(state) {
456 + return state.failureCount;
457 + }
430 458 function getItem(state, id) {
431 459 return state.queue.find((item) => item.id === id);
432 460 }
433 461 function isBatchUploaded(state, batchId) {
@@ -457,8 +485,13 @@
457 485 return state.queue.filter(
458 486 (item) => item.currentOperation === OperationType.ResizeCrop || item.currentOperation === OperationType.Rotate
459 487 ).length;
460 488 }
489 + function getActiveVideoProcessingCount(state) {
490 + return state.queue.filter(
491 + (item) => item.currentOperation === OperationType.TranscodeGif
492 + ).length;
493 + }
461 494 function getPendingImageProcessing(state) {
462 495 return state.queue.filter((item) => {
463 496 const nextOperation = Array.isArray(item.operations?.[0]) ? item.operations[0][0] : item.operations?.[0];
464 497 return (nextOperation === OperationType.ResizeCrop || nextOperation === OperationType.Rotate) && item.currentOperation !== OperationType.ResizeCrop && item.currentOperation !== OperationType.Rotate;
@@ -463,8 +496,14 @@
463 496 const nextOperation = Array.isArray(item.operations?.[0]) ? item.operations[0][0] : item.operations?.[0];
464 497 return (nextOperation === OperationType.ResizeCrop || nextOperation === OperationType.Rotate) && item.currentOperation !== OperationType.ResizeCrop && item.currentOperation !== OperationType.Rotate;
465 498 });
466 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 + }
467 506 function getFailedItems(state) {
468 507 return state.queue.filter((item) => item.error !== void 0);
469 508 }
470 509 function hasPendingItemsByParentId(state, parentId) {
@@ -622,8 +661,72 @@
622 661 }
623 662 function getFileBasename(name) {
624 663 return name.includes(".") ? name.split(".").slice(0, -1).join(".") : name;
625 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 + }
626 729
627 730 // packages/upload-media/build-module/store/utils/index.mjs
628 731 var vipsModulePromise;
629 732 var vipsModule;
@@ -637,9 +740,9 @@
637 740 );
638 741 }
639 742 return vipsModulePromise;
640 743 }
641 - async function vipsConvertImageFormat(id, file, type, quality, interlaced) {
744 + async function vipsConvertImageFormat(id, file, type, options = {}) {
642 745 const { vipsConvertImageFormat: convertImageFormat } = await loadVipsModule();
643 746 const buffer = await convertImageFormat(
644 747 id,
645 748 await file.arrayBuffer(),
@@ -644,10 +747,9 @@
644 747 id,
645 748 await file.arrayBuffer(),
646 749 file.type,
647 750 type,
648 - quality,
649 - interlaced
751 + options
650 752 );
651 753 const ext = type.split("/")[1];
652 754 const fileName = `${getFileBasename(file.name)}.${ext}`;
653 755 return new File([new Blob([buffer])], fileName, {
@@ -665,21 +767,28 @@
665 767 async function vipsGetUltraHdrInfo(buffer) {
666 768 const { vipsGetUltraHdrInfo: getUltraHdrInfo } = await loadVipsModule();
667 769 return getUltraHdrInfo(buffer);
668 770 }
669 - async function vipsResizeImage(id, file, resize, smartCrop, addSuffix, signal, scaledSuffix, quality) {
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;
670 781 if (signal?.aborted) {
671 782 throw new Error("Operation aborted");
672 783 }
673 784 const { vipsResizeImage: resizeImage } = await loadVipsModule();
674 - const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(
675 - id,
676 - await file.arrayBuffer(),
677 - file.type,
678 - resize,
785 + const { buffer, width, height, originalWidth, originalHeight } = await resizeImage(id, await file.arrayBuffer(), file.type, resize, {
679 786 smartCrop,
680 - quality
681 - );
787 + quality,
788 + stripMeta,
789 + maxBitdepth
790 + });
682 791 let fileName = file.name;
683 792 const wasResized = originalWidth > width || originalHeight > height;
684 793 if (wasResized) {
685 794 const basename = getFileBasename(file.name);
@@ -756,8 +865,105 @@
756 865 completedVipsOperations = 0;
757 866 }
758 867 }
759 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 +
760 966 // packages/upload-media/build-module/store/utils/debug-logger.mjs
761 967 function isDebugEnabled() {
762 968 return true;
763 969 }
@@ -808,8 +1014,10 @@
808 1014 ErrorCode2["HEIC_DECODE_ERROR"] = "HEIC_DECODE_ERROR";
809 1015 ErrorCode2["IMAGE_TRANSCODING_ERROR"] = "IMAGE_TRANSCODING_ERROR";
810 1016 ErrorCode2["IMAGE_ROTATION_ERROR"] = "IMAGE_ROTATION_ERROR";
811 1017 ErrorCode2["MEDIA_TRANSCODING_ERROR"] = "MEDIA_TRANSCODING_ERROR";
1018 + ErrorCode2["GIF_TRANSCODING_ERROR"] = "GIF_TRANSCODING_ERROR";
1019 + ErrorCode2["MEDIA_FINALIZE_ERROR"] = "MEDIA_FINALIZE_ERROR";
812 1020 ErrorCode2["GENERAL"] = "GENERAL";
813 1021 return ErrorCode2;
814 1022 })(ErrorCode || {});
815 1023 var UploadError = class extends Error {
@@ -986,9 +1194,12 @@
986 1194 }
987 1195 }
988 1196 }
989 1197 item.abortController?.abort();
990 - await vipsCancelOperations(id);
1198 + vipsCancelOperations(id).catch(() => {
1199 + });
1200 + cancelGifToVideoOperations(id).catch(() => {
1201 + });
991 1202 if (!silent) {
992 1203 const { onError } = item;
993 1204 onError?.(error ?? new Error("Upload cancelled"));
994 1205 if (!onError && error && !item.parentId) {
@@ -1016,8 +1227,13 @@
1016 1227 for (const pending of select2.getPendingUploads()) {
1017 1228 dispatch.processItem(pending.id);
1018 1229 }
1019 1230 }
1231 + if (currentOperation === OperationType.TranscodeGif) {
1232 + for (const pending of select2.getPendingVideoProcessing()) {
1233 + dispatch.processItem(pending.id);
1234 + }
1235 + }
1020 1236 if (currentOperation === OperationType.ResizeCrop || currentOperation === OperationType.Rotate || currentOperation === OperationType.TranscodeImage) {
1021 1237 maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
1022 1238 }
1023 1239 if (parentId) {
@@ -1022,13 +1238,14 @@
1022 1238 }
1023 1239 if (parentId) {
1024 1240 const parentItem = select2.getItem(parentId);
1025 1241 if (parentItem) {
1242 + const isOptionalCompanion = item.additionalData?.image_size === "animated_video" || item.additionalData?.image_size === "animated_video_poster";
1026 1243 if (select2.hasPendingItemsByParentId(parentId)) {
1027 1244 if (parentItem.operations && parentItem.operations.length > 0) {
1028 1245 dispatch.processItem(parentId);
1029 1246 }
1030 - } else if (parentItem.subSizes && parentItem.subSizes.length > 0) {
1247 + } else if (parentItem.subSizes && parentItem.subSizes.length > 0 || isOptionalCompanion) {
1031 1248 if (parentItem.operations && parentItem.operations.length > 0) {
1032 1249 dispatch.processItem(parentId);
1033 1250 }
1034 1251 } else {
@@ -1037,9 +1254,9 @@
1037 1254 if (parentAttachmentId && mediaDelete) {
1038 1255 mediaDelete(parentAttachmentId).catch(() => {
1039 1256 });
1040 1257 }
1041 - dispatch.cancelItem(
1258 + await dispatch.cancelItem(
1042 1259 parentId,
1043 1260 new UploadError({
1044 1261 code: error instanceof UploadError && error.code || ErrorCode.GENERAL,
1045 1262 message: error?.message || (0, import_i18n5.__)("The image could not be uploaded."),
@@ -1141,8 +1358,9 @@
1141 1358 resumeQueue: () => resumeQueue,
1142 1359 revokeBlobUrls: () => revokeBlobUrls,
1143 1360 rotateItem: () => rotateItem,
1144 1361 sideloadItem: () => sideloadItem,
1362 + transcodeGifItem: () => transcodeGifItem,
1145 1363 transcodeImageItem: () => transcodeImageItem,
1146 1364 updateItemProgress: () => updateItemProgress,
1147 1365 updateSettings: () => updateSettings,
1148 1366 uploadItem: () => uploadItem
@@ -1147,9 +1365,9 @@
1147 1365 updateSettings: () => updateSettings,
1148 1366 uploadItem: () => uploadItem
1149 1367 });
1150 1368 var import_blob = __toESM(require_blob(), 1);
1151 - var import_i18n6 = __toESM(require_i18n(), 1);
1369 + var import_i18n7 = __toESM(require_i18n(), 1);
1152 1370
1153 1371 // packages/upload-media/build-module/heic-parser.mjs
1154 1372 var Reader = class {
1155 1373 view;
@@ -1420,8 +1638,15 @@
1420 1638 return `hvc1.${spacePrefix}${profileIdc}.${compatHex}.${tierChar}${levelIdc}${constraintStr}`;
1421 1639 }
1422 1640 function readItemData(buffer, loc, idatOffset) {
1423 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 + }
1424 1649 if (loc.extents.length === 1) {
1425 1650 const ext = loc.extents[0];
1426 1651 const start = baseOffset + ext.offset;
1427 1652 return new Uint8Array(buffer.slice(start, start + ext.length));
@@ -1556,9 +1781,10 @@
1556 1781 tileWidth: width,
1557 1782 tileHeight: height,
1558 1783 outputWidth: width,
1559 1784 outputHeight: height,
1560 - rotation
1785 + rotation,
1786 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1561 1787 };
1562 1788 }
1563 1789 function parseGridImage(r, buffer, gridItemId, locations, allAssoc, properties, irefBox, idatOffset) {
1564 1790 const gridLoc = locations.get(gridItemId);
@@ -1649,13 +1875,269 @@
1649 1875 tileWidth,
1650 1876 tileHeight,
1651 1877 outputWidth,
1652 1878 outputHeight,
1653 - rotation
1879 + rotation,
1880 + exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
1654 1881 };
1655 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 + }
1656 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 +
1657 2136 // packages/upload-media/build-module/canvas-utils.mjs
2137 + var IMAGE_DECODER_TIMEOUT = 15e3;
2138 + var HeicUnsupportedError = class extends Error {
2139 + };
1658 2140 async function canvasConvertToJpeg(file, quality = 0.82) {
1659 2141 const baseName = getFileBasename(file.name);
1660 2142 try {
1661 2143 const bitmap = await createImageBitmap(file);
@@ -1684,10 +2166,36 @@
1684 2166 const decoder = new ImageDecoder({
1685 2167 type: file.type,
1686 2168 data: file.stream()
1687 2169 });
2170 + let videoFrame;
2171 + let timeoutId;
2172 + let timedOut = false;
1688 2173 try {
1689 - 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) {
1690 2198 try {
1691 2199 const canvas = new OffscreenCanvas(
1692 2200 videoFrame.displayWidth,
1693 2201 videoFrame.displayHeight
@@ -1706,57 +2214,62 @@
1706 2214 });
1707 2215 } finally {
1708 2216 videoFrame.close();
1709 2217 }
1710 - } finally {
1711 - decoder.close();
1712 2218 }
1713 2219 }
1714 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 + }
1715 2229 if (typeof VideoDecoder !== "undefined") {
2230 + let supported = false;
1716 2231 try {
1717 - const heicData = parseHeic(await file.arrayBuffer());
1718 2232 const support = await VideoDecoder.isConfigSupported({
1719 2233 codec: heicData.codecString
1720 2234 });
1721 - if (support.supported) {
1722 - const canvas = new OffscreenCanvas(
1723 - heicData.outputWidth,
1724 - 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
1725 2254 );
1726 - const ctx = canvas.getContext("2d");
1727 - if (!ctx) {
1728 - throw new Error("Could not get canvas 2d context");
2255 + try {
2256 + ctx.drawImage(frame, tile.x, tile.y);
2257 + } finally {
2258 + frame.close();
1729 2259 }
1730 - for (const tile of heicData.tiles) {
1731 - const frame = await decodeHevcFrame(
1732 - heicData.codecString,
1733 - heicData.description,
1734 - heicData.tileWidth,
1735 - heicData.tileHeight,
1736 - tile.data
1737 - );
1738 - try {
1739 - ctx.drawImage(frame, tile.x, tile.y);
1740 - } finally {
1741 - frame.close();
1742 - }
1743 - }
1744 - const outputCanvas = applyRotation(canvas, heicData.rotation);
1745 - const jpegBlob = await outputCanvas.convertToBlob({
1746 - type: "image/jpeg",
1747 - quality
1748 - });
1749 - return new File([jpegBlob], `${baseName}.jpg`, {
1750 - type: "image/jpeg"
1751 - });
1752 2260 }
1753 - } 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 + });
1754 2269 }
1755 2270 }
1756 - throw new Error(
1757 - "This browser cannot decode HEIC images. Please use Safari or convert to JPEG before uploading."
1758 - );
2271 + throw new HeicUnsupportedError(getHeicUnsupportedMessage());
1759 2272 }
1760 2273 function applyRotation(source, rotation) {
1761 2274 if (rotation === 0) {
1762 2275 return source;
@@ -1773,8 +2286,45 @@
1773 2286 ctx.rotate(-rotation * Math.PI / 180);
1774 2287 ctx.drawImage(source, -source.width / 2, -source.height / 2);
1775 2288 return rotated;
1776 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 + }
1777 2327 function decodeHevcFrame(codec, description, width, height, data) {
1778 2328 return new Promise((resolve, reject) => {
1779 2329 const decoder = new VideoDecoder({
1780 2330 output: (frame) => {
@@ -2089,8 +2639,11 @@
2089 2639 const item = select2.getItem(id);
2090 2640 if (!item) {
2091 2641 return;
2092 2642 }
2643 + if (item.currentOperation) {
2644 + return;
2645 + }
2093 2646 const {
2094 2647 attachment,
2095 2648 onChange,
2096 2649 onSuccess,
@@ -2113,8 +2666,14 @@
2113 2666 if (activeCount >= settings.maxConcurrentImageProcessing) {
2114 2667 return;
2115 2668 }
2116 2669 }
2670 + if (operation === OperationType.TranscodeGif) {
2671 + const activeCount = select2.getActiveVideoProcessingCount();
2672 + if (activeCount >= 1) {
2673 + return;
2674 + }
2675 + }
2117 2676 if (attachment) {
2118 2677 const isHeicUrl = attachment.url && /\.hei[cf]$/i.test(attachment.url);
2119 2678 if (!isHeicUrl) {
2120 2679 onChange?.([attachment]);
@@ -2158,8 +2717,11 @@
2158 2717 type: Type.OperationStart,
2159 2718 id,
2160 2719 operation
2161 2720 });
2721 + debug(
2722 + `Starting operation ${operation} for ${item.file.name} (item ${item.id})`
2723 + );
2162 2724 switch (operation) {
2163 2725 case OperationType.Prepare:
2164 2726 dispatch.prepareItem(item.id);
2165 2727 break;
@@ -2180,8 +2742,14 @@
2180 2742 item.id,
2181 2743 operationArgs
2182 2744 );
2183 2745 break;
2746 + case OperationType.TranscodeGif:
2747 + dispatch.transcodeGifItem(
2748 + item.id,
2749 + operationArgs
2750 + );
2751 + break;
2184 2752 case OperationType.Upload:
2185 2753 if (item.parentId) {
2186 2754 dispatch.sideloadItem(id);
2187 2755 } else {
@@ -2240,8 +2808,9 @@
2240 2808 id
2241 2809 });
2242 2810 if (select2.getAllItems().length === 0) {
2243 2811 terminateVipsWorker();
2812 + terminateVideoConversionWorker();
2244 2813 }
2245 2814 };
2246 2815 }
2247 2816 function finishOperation(id, updates) {
@@ -2265,8 +2834,14 @@
2265 2834 for (const pendingItem of pendingItems) {
2266 2835 dispatch.processItem(pendingItem.id);
2267 2836 }
2268 2837 }
2838 + if (previousOperation === OperationType.TranscodeGif) {
2839 + const pendingItems = select2.getPendingVideoProcessing();
2840 + for (const pendingItem of pendingItems) {
2841 + dispatch.processItem(pendingItem.id);
2842 + }
2843 + }
2269 2844 if (previousOperation === OperationType.ResizeCrop || previousOperation === OperationType.Rotate || previousOperation === OperationType.TranscodeImage) {
2270 2845 maybeRecycleVipsWorker(select2.getActiveImageProcessingCount());
2271 2846 }
2272 2847 };
@@ -2274,9 +2849,9 @@
2274 2849 var VALID_IMAGE_FORMATS = ["jpeg", "webp", "avif", "png", "gif"];
2275 2850 function isValidImageFormat(format) {
2276 2851 return VALID_IMAGE_FORMATS.includes(format);
2277 2852 }
2278 - async function getTranscodeImageOperation(file, outputMimeType, interlaced = false) {
2853 + async function getTranscodeImageOperation(file, outputMimeType, interlaced = false, quality = DEFAULT_OUTPUT_QUALITY) {
2279 2854 if (file.type === "image/png" && outputMimeType === "image/jpeg") {
2280 2855 const blobUrl = (0, import_blob.createBlobURL)(file);
2281 2856 try {
2282 2857 const hasAlpha = await vipsHasTransparency(blobUrl);
@@ -2296,9 +2871,9 @@
2296 2871 return [
2297 2872 OperationType.TranscodeImage,
2298 2873 {
2299 2874 outputFormat: formatPart,
2300 - outputQuality: DEFAULT_OUTPUT_QUALITY,
2875 + outputQuality: quality,
2301 2876 interlaced
2302 2877 }
2303 2878 ];
2304 2879 }
@@ -2310,14 +2885,52 @@
2310 2885 }
2311 2886 const { file } = item;
2312 2887 const operations = [];
2313 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 + }
2314 2924 let heicJpeg = null;
2315 - const isImage = file.type.startsWith("image/");
2316 - const isVipsSupported = CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(
2317 - file.type
2318 - );
2319 - 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;
2320 2933 let tooLargeForClient = false;
2321 2934 if (isImage && isVipsSupported) {
2322 2935 const dimensions = await getImageDimensions(file);
2323 2936 if (dimensions && exceedsClientProcessingMemory(dimensions)) {
@@ -2323,9 +2936,9 @@
2323 2936 if (dimensions && exceedsClientProcessingMemory(dimensions)) {
2324 2937 tooLargeForClient = true;
2325 2938 }
2326 2939 }
2327 - if (file.type === "image/jpeg" && !tooLargeForClient) {
2940 + if (file.type === "image/jpeg" && !isHeic && !tooLargeForClient) {
2328 2941 operations.push(OperationType.DetectUltraHdr);
2329 2942 }
2330 2943 if (isImage && isVipsSupported && !tooLargeForClient) {
2331 2944 operations.push(
@@ -2335,18 +2948,22 @@
2335 2948 );
2336 2949 } else if (isImage && isHeic) {
2337 2950 try {
2338 2951 heicJpeg = await canvasConvertToJpeg(
2339 - file,
2952 + heicFile,
2340 2953 settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY
2341 2954 );
2342 - } catch {
2955 + } catch (error) {
2956 + const unsupported = error instanceof HeicUnsupportedError;
2343 2957 dispatch.cancelItem(
2344 2958 id,
2345 2959 new UploadError({
2346 - code: ErrorCode.HEIC_DECODE_ERROR,
2347 - message: "This browser cannot decode HEIC images and the server does not support them either. Please convert to JPEG before uploading.",
2348 - 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
2349 2966 })
2350 2967 );
2351 2968 return;
2352 2969 }
@@ -2368,9 +2985,9 @@
2368 2985 const vipsAvailable = isClientSideMediaSupported();
2369 2986 updates = {
2370 2987 file: heicJpeg,
2371 2988 sourceFile: heicJpeg,
2372 - originalHeicFile: item.file,
2989 + originalHeicFile: heicFile,
2373 2990 additionalData: {
2374 2991 ...item.additionalData,
2375 2992 generate_sub_sizes: !vipsAvailable,
2376 2993 convert_format: true
@@ -2440,8 +3057,13 @@
2440 3057 select2.getSettings().mediaUpload({
2441 3058 filesList: [item.file],
2442 3059 additionalData: item.additionalData,
2443 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,
2444 3066 onFileChange: ([attachment]) => {
2445 3067 if (attachment && !(0, import_blob.isBlobURL)(attachment.url)) {
2446 3068 finishUpload(attachment);
2447 3069 }
@@ -2512,18 +3134,23 @@
2512 3134 }
2513 3135 const startTime = performance.now();
2514 3136 const addSuffix = Boolean(item.parentId);
2515 3137 const scaledSuffix = Boolean(args.isThresholdResize);
3138 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2516 3139 try {
2517 3140 const file = await vipsResizeImage(
2518 3141 item.id,
2519 3142 item.file,
2520 3143 args.resize,
2521 - false,
2522 - // smartCrop
2523 - addSuffix,
2524 - item.abortController?.signal,
2525 - 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 + }
2526 3153 );
2527 3154 measure({
2528 3155 measureName: `ResizeCrop ${item.file.name}`,
2529 3156 startTime,
@@ -2549,9 +3176,9 @@
2549 3176 dispatch.cancelItem(
2550 3177 id,
2551 3178 new UploadError({
2552 3179 code: ErrorCode.IMAGE_TRANSCODING_ERROR,
2553 - message: (0, import_i18n6.__)(
3180 + message: (0, import_i18n7.__)(
2554 3181 "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
2555 3182 ),
2556 3183 file: item.file,
2557 3184 cause: error instanceof Error ? error : void 0
@@ -2605,9 +3232,9 @@
2605 3232 dispatch.cancelItem(
2606 3233 id,
2607 3234 new UploadError({
2608 3235 code: ErrorCode.IMAGE_ROTATION_ERROR,
2609 - message: (0, import_i18n6.__)(
3236 + message: (0, import_i18n7.__)(
2610 3237 "The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading."
2611 3238 ),
2612 3239 file: item.file,
2613 3240 cause: error instanceof Error ? error : void 0
@@ -2631,15 +3258,20 @@
2631 3258 const startTime = performance.now();
2632 3259 const outputMimeType = `image/${args.outputFormat}`;
2633 3260 const quality = args.outputQuality ?? DEFAULT_OUTPUT_QUALITY;
2634 3261 const interlaced = args.interlaced ?? false;
3262 + const { imageStripMeta, imageMaxBitDepth } = select2.getSettings();
2635 3263 try {
2636 3264 const file = await vipsConvertImageFormat(
2637 3265 item.id,
2638 3266 item.file,
2639 3267 outputMimeType,
2640 - quality,
2641 - interlaced
3268 + {
3269 + quality,
3270 + interlaced,
3271 + stripMeta: imageStripMeta,
3272 + maxBitdepth: imageMaxBitDepth
3273 + }
2642 3274 );
2643 3275 measure({
2644 3276 measureName: `Transcode ${item.file.name}`,
2645 3277 startTime,
@@ -2673,8 +3305,91 @@
2673 3305 );
2674 3306 }
2675 3307 };
2676 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 + }
2677 3392 function generateThumbnails(id) {
2678 3393 return async ({ select: select2, dispatch }) => {
2679 3394 const item = select2.getItem(id);
2680 3395 if (!item) {
@@ -2692,35 +3407,57 @@
2692 3407 batchId: v4_default(),
2693 3408 parentId: item.id,
2694 3409 additionalData: {
2695 3410 post: attachment.id,
2696 - image_size: "original-heic",
3411 + image_size: "source_original",
2697 3412 convert_format: false
2698 3413 },
2699 3414 operations: [OperationType.Upload]
2700 3415 });
2701 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;
2702 3450 {
2703 - const needsRotation = attachment.exif_orientation && attachment.exif_orientation !== 1 && !item.file.name.includes("-scaled");
2704 - if (needsRotation && attachment.id) {
3451 + const needsRotation = exifOrientation !== 1 && !item.file.name.includes("-scaled");
3452 + if ((needsRotation || needsClientRotation) && attachment.id) {
2705 3453 try {
2706 - const rotatedFile = await vipsRotateImage(
3454 + rotatedSource = await vipsRotateImage(
2707 3455 item.id,
2708 3456 item.sourceFile,
2709 - attachment.exif_orientation,
3457 + exifOrientation,
2710 3458 item.abortController?.signal
2711 3459 );
2712 - dispatch.addSideloadItem({
2713 - file: rotatedFile,
2714 - batchId: v4_default(),
2715 - parentId: item.id,
2716 - additionalData: {
2717 - post: attachment.id,
2718 - image_size: "original",
2719 - convert_format: false
2720 - },
2721 - operations: [OperationType.Upload]
2722 - });
2723 3460 } catch {
2724 3461 console.warn(
2725 3462 "Failed to rotate image, continuing with thumbnails"
2726 3463 );
@@ -2725,24 +3462,48 @@
2725 3462 "Failed to rotate image, continuing with thumbnails"
2726 3463 );
2727 3464 }
2728 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 + }
2729 3479 }
2730 3480 if (!item.parentId && attachment.missing_image_sizes && attachment.missing_image_sizes.length > 0) {
2731 3481 const allImageSizes = settings.allImageSizes || {};
2732 3482 const sizesToGenerate = attachment.missing_image_sizes;
2733 - const thumbnailSource = item.sourceFile;
3483 + const thumbnailSource = needsClientRotation && rotatedSource ? rotatedSource : item.sourceFile;
2734 3484 const file = attachment.filename ? renameFile(thumbnailSource, attachment.filename) : thumbnailSource;
2735 3485 const batchId = v4_default();
2736 3486 const isUltraHdr = ultraHdrItems.has(item.id);
2737 3487 const outputMimeType = attachment.image_output_format;
2738 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 + };
2739 3499 let thumbnailTranscodeOperation = null;
2740 3500 if (!isUltraHdr && outputMimeType) {
2741 3501 thumbnailTranscodeOperation = await getTranscodeImageOperation(
2742 3502 thumbnailSource,
2743 3503 outputMimeType,
2744 - interlaced
3504 + interlaced,
3505 + defaultQuality
2745 3506 );
2746 3507 }
2747 3508 const dimensionGroups = /* @__PURE__ */ new Map();
2748 3509 for (const name of sizesToGenerate) {
@@ -2762,13 +3523,23 @@
2762 3523 }
2763 3524 }
2764 3525 for (const [, names] of dimensionGroups) {
2765 3526 const imageSize = allImageSizes[names[0]];
3527 + const sizeQuality = qualityForSize(names[0]);
2766 3528 const thumbnailOperations = [
2767 - [OperationType.ResizeCrop, { resize: imageSize }]
3529 + [
3530 + OperationType.ResizeCrop,
3531 + { resize: imageSize, quality: sizeQuality }
3532 + ]
2768 3533 ];
2769 3534 if (!isUltraHdr && thumbnailTranscodeOperation) {
2770 - thumbnailOperations.push(thumbnailTranscodeOperation);
3535 + thumbnailOperations.push([
3536 + thumbnailTranscodeOperation[0],
3537 + {
3538 + ...thumbnailTranscodeOperation[1],
3539 + outputQuality: sizeQuality
3540 + }
3541 + ]);
2771 3542 }
2772 3543 thumbnailOperations.push(OperationType.Upload);
2773 3544 const imageSizeParam = names.length === 1 ? names[0] : names;
2774 3545 dispatch.addSideloadItem({
@@ -2800,9 +3571,10 @@
2800 3571 resize: {
2801 3572 width: bigImageSizeThreshold,
2802 3573 height: bigImageSizeThreshold
2803 3574 },
2804 - isThresholdResize: true
3575 + isThresholdResize: true,
3576 + quality: defaultQuality
2805 3577 }
2806 3578 ]
2807 3579 ];
2808 3580 if (!isUltraHdr && thumbnailTranscodeOperation) {
@@ -2848,8 +3620,17 @@
2848 3620 updates.attachment = updatedAttachment;
2849 3621 }
2850 3622 } catch (error) {
2851 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;
2852 3633 }
2853 3634 }
2854 3635 dispatch.finishOperation(id, updates);
2855 3636 };
@@ -2957,102 +3738,113 @@
2957 3738 });
2958 3739 var provider_default = MediaUploadProvider;
2959 3740
2960 3741 // packages/upload-media/build-module/error-messages.mjs
2961 - var import_i18n7 = __toESM(require_i18n(), 1);
3742 + var import_i18n8 = __toESM(require_i18n(), 1);
2962 3743 function getErrorMessage(code, fileName) {
2963 3744 const messages = {
2964 3745 [ErrorCode.EMPTY_FILE]: {
2965 - title: (0, import_i18n7.__)("Empty file"),
2966 - description: (0, import_i18n7.sprintf)(
3746 + title: (0, import_i18n8.__)("Empty file"),
3747 + description: (0, import_i18n8.sprintf)(
2967 3748 /* translators: %s: file name */
2968 - (0, import_i18n7.__)('"%s" is empty.'),
3749 + (0, import_i18n8.__)('"%s" is empty.'),
2969 3750 fileName
2970 3751 ),
2971 - action: (0, import_i18n7.__)("Please choose a different file.")
3752 + action: (0, import_i18n8.__)("Please choose a different file.")
2972 3753 },
2973 3754 [ErrorCode.SIZE_ABOVE_LIMIT]: {
2974 - title: (0, import_i18n7.__)("File too large"),
2975 - description: (0, import_i18n7.sprintf)(
3755 + title: (0, import_i18n8.__)("File too large"),
3756 + description: (0, import_i18n8.sprintf)(
2976 3757 /* translators: %s: file name */
2977 - (0, import_i18n7.__)('"%s" exceeds the maximum upload size.'),
3758 + (0, import_i18n8.__)('"%s" exceeds the maximum upload size.'),
2978 3759 fileName
2979 3760 ),
2980 - action: (0, import_i18n7.__)("Please reduce the file size and try again.")
3761 + action: (0, import_i18n8.__)("Please reduce the file size and try again.")
2981 3762 },
2982 3763 [ErrorCode.MIME_TYPE_NOT_SUPPORTED]: {
2983 - title: (0, import_i18n7.__)("Unsupported file type"),
2984 - description: (0, import_i18n7.sprintf)(
3764 + title: (0, import_i18n8.__)("Unsupported file type"),
3765 + description: (0, import_i18n8.sprintf)(
2985 3766 /* translators: %s: file name */
2986 - (0, import_i18n7.__)('"%s" is not a supported file type.'),
3767 + (0, import_i18n8.__)('"%s" is not a supported file type.'),
2987 3768 fileName
2988 3769 ),
2989 - action: (0, import_i18n7.__)("Please upload a different file format.")
3770 + action: (0, import_i18n8.__)("Please upload a different file format.")
2990 3771 },
2991 3772 [ErrorCode.MIME_TYPE_NOT_ALLOWED_FOR_USER]: {
2992 - title: (0, import_i18n7.__)("File type not allowed"),
2993 - description: (0, import_i18n7.sprintf)(
3773 + title: (0, import_i18n8.__)("File type not allowed"),
3774 + description: (0, import_i18n8.sprintf)(
2994 3775 /* translators: %s: file name */
2995 - (0, import_i18n7.__)('You are not allowed to upload "%s".'),
3776 + (0, import_i18n8.__)('You are not allowed to upload "%s".'),
2996 3777 fileName
2997 3778 ),
2998 - action: (0, import_i18n7.__)("Please contact your site administrator.")
3779 + action: (0, import_i18n8.__)("Please contact your site administrator.")
2999 3780 },
3000 3781 [ErrorCode.HEIC_DECODE_ERROR]: {
3001 - title: (0, import_i18n7.__)("HEIC decode failed"),
3002 - description: (0, import_i18n7.sprintf)(
3782 + title: (0, import_i18n8.__)("HEIC decode failed"),
3783 + description: (0, import_i18n8.sprintf)(
3003 3784 /* translators: %s: file name */
3004 - (0, import_i18n7.__)('Failed to decode HEIC file "%s".'),
3785 + (0, import_i18n8.__)('Failed to decode HEIC file "%s".'),
3005 3786 fileName
3006 3787 ),
3007 - action: (0, import_i18n7.__)("Try converting the image to JPEG or PNG first.")
3788 + action: getHeicConversionAdvice()
3008 3789 },
3009 3790 [ErrorCode.IMAGE_TRANSCODING_ERROR]: {
3010 - title: (0, import_i18n7.__)("Image processing failed"),
3011 - description: (0, import_i18n7.sprintf)(
3791 + title: (0, import_i18n8.__)("Image processing failed"),
3792 + description: (0, import_i18n8.sprintf)(
3012 3793 /* translators: %s: file name */
3013 - (0, import_i18n7.__)('Failed to process "%s".'),
3794 + (0, import_i18n8.__)('Failed to process "%s".'),
3014 3795 fileName
3015 3796 ),
3016 - action: (0, import_i18n7.__)("The image may be corrupted. Try a different file.")
3797 + action: (0, import_i18n8.__)("The image may be corrupted. Try a different file.")
3017 3798 },
3018 3799 [ErrorCode.IMAGE_ROTATION_ERROR]: {
3019 - title: (0, import_i18n7.__)("Image rotation failed"),
3020 - description: (0, import_i18n7.sprintf)(
3800 + title: (0, import_i18n8.__)("Image rotation failed"),
3801 + description: (0, import_i18n8.sprintf)(
3021 3802 /* translators: %s: file name */
3022 - (0, import_i18n7.__)('Failed to rotate "%s".'),
3803 + (0, import_i18n8.__)('Failed to rotate "%s".'),
3023 3804 fileName
3024 3805 ),
3025 - action: (0, import_i18n7.__)("The image may be corrupted. Try a different file.")
3806 + action: (0, import_i18n8.__)("The image may be corrupted. Try a different file.")
3026 3807 },
3027 3808 [ErrorCode.MEDIA_TRANSCODING_ERROR]: {
3028 - title: (0, import_i18n7.__)("Media processing failed"),
3029 - description: (0, import_i18n7.sprintf)(
3809 + title: (0, import_i18n8.__)("Media processing failed"),
3810 + description: (0, import_i18n8.sprintf)(
3030 3811 /* translators: %s: file name */
3031 - (0, import_i18n7.__)('Failed to convert "%s" to the target format.'),
3812 + (0, import_i18n8.__)('Failed to convert "%s" to the target format.'),
3032 3813 fileName
3033 3814 ),
3034 - action: (0, import_i18n7.__)("The file may be corrupted. Try a different file.")
3815 + action: (0, import_i18n8.__)("The file may be corrupted. Try a different file.")
3035 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 + },
3036 3826 [ErrorCode.GENERAL]: {
3037 - title: (0, import_i18n7.__)("Upload failed"),
3038 - description: (0, import_i18n7.sprintf)(
3827 + title: (0, import_i18n8.__)("Upload failed"),
3828 + description: (0, import_i18n8.sprintf)(
3039 3829 /* translators: %s: file name */
3040 - (0, import_i18n7.__)('Failed to upload "%s".'),
3830 + (0, import_i18n8.__)('Failed to upload "%s".'),
3041 3831 fileName
3042 3832 ),
3043 - action: (0, import_i18n7.__)("Please try again.")
3833 + action: (0, import_i18n8.__)("Please try again.")
3044 3834 }
3045 3835 };
3046 3836 return messages[code] || {
3047 - title: (0, import_i18n7.__)("Upload failed"),
3048 - description: (0, import_i18n7.sprintf)(
3837 + title: (0, import_i18n8.__)("Upload failed"),
3838 + description: (0, import_i18n8.sprintf)(
3049 3839 /* translators: %s: file name */
3050 - (0, import_i18n7.__)('Failed to upload "%s".'),
3840 + (0, import_i18n8.__)('Failed to upload "%s".'),
3051 3841 fileName
3052 3842 ),
3053 - action: (0, import_i18n7.__)("Please try again.")
3843 + action: (0, import_i18n8.__)("Please try again.")
3054 3844 };
3055 3845 }
3056 3846 return __toCommonJS(index_exports);
3847 +})();
3848 +(window.wp ||= {}).uploadMedia = wp.uploadMedia;
3057 3849 })();
3058 3850 //# sourceMappingURL=index.js.map