| @@ -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 |