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 +173 -53 23.9.1 → trunk View file →
@@ -107,8 +107,9 @@
107 107 getHeicConversionAdvice: () => getHeicConversionAdvice,
108 108 getHeicUnsupportedMessage: () => getHeicUnsupportedMessage,
109 109 isClientSideMediaSupported: () => isClientSideMediaSupported,
110 110 isHeicCanvasSupported: () => isHeicCanvasSupported,
111 + isHeicFile: () => isHeicFile,
111 112 store: () => store
112 113 });
113 114
114 115 // packages/upload-media/build-module/store/index.mjs
@@ -193,9 +194,10 @@
193 194 mediaUpload: noop,
194 195 maxConcurrentUploads: DEFAULT_MAX_CONCURRENT_UPLOADS,
195 196 maxConcurrentImageProcessing: DEFAULT_MAX_CONCURRENT_IMAGE_PROCESSING,
196 197 retry: { ...DEFAULT_RETRY_SETTINGS }
197 - }
198 + },
199 + failureCount: 0
198 200 };
199 201 function reducer(state = DEFAULT_STATE, action = { type: Type.Unknown }) {
200 202 switch (action.type) {
201 203 case Type.PauseQueue: {
@@ -234,11 +236,15 @@
234 236 return {
235 237 ...state,
236 238 queue: [...state.queue, action.item]
237 239 };
238 - case Type.Cancel:
240 + case Type.Cancel: {
241 + const cancelled = state.queue.find(
242 + (item) => item.id === action.id
243 + );
239 244 return {
240 245 ...state,
246 + failureCount: cancelled && !cancelled.parentId ? state.failureCount + 1 : state.failureCount,
241 247 queue: state.queue.map(
242 248 (item) => item.id === action.id ? {
243 249 ...item,
244 250 error: action.error
@@ -244,8 +250,9 @@
244 250 error: action.error
245 251 } : item
246 252 )
247 253 };
254 + }
248 255 case Type.RetryItem:
249 256 return {
250 257 ...state,
251 258 queue: state.queue.map(
@@ -253,9 +260,20 @@
253 260 ...item,
254 261 status: ItemStatus.Processing,
255 262 error: void 0,
256 263 retryCount: (item.retryCount ?? 0) + 1,
257 - 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
258 276 } : item
259 277 )
260 278 };
261 279 case Type.ScheduleRetry:
@@ -420,8 +438,9 @@
420 438 getActiveVideoProcessingCount: () => getActiveVideoProcessingCount,
421 439 getAllItems: () => getAllItems,
422 440 getBlobUrls: () => getBlobUrls,
423 441 getFailedItems: () => getFailedItems,
442 + getFailureCount: () => getFailureCount,
424 443 getItem: () => getItem,
425 444 getItemProgress: () => getItemProgress,
426 445 getPendingImageProcessing: () => getPendingImageProcessing,
427 446 getPendingUploads: () => getPendingUploads,
@@ -432,8 +451,11 @@
432 451 });
433 452 function getAllItems(state) {
434 453 return state.queue;
435 454 }
455 + function getFailureCount(state) {
456 + return state.failureCount;
457 + }
436 458 function getItem(state, id) {
437 459 return state.queue.find((item) => item.id === id);
438 460 }
439 461 function isBatchUploaded(state, batchId) {
@@ -655,8 +677,56 @@
655 677 }
656 678 }
657 679 return false;
658 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 + }
659 729
660 730 // packages/upload-media/build-module/store/utils/index.mjs
661 731 var vipsModulePromise;
662 732 var vipsModule;
@@ -1568,8 +1638,15 @@
1568 1638 return `hvc1.${spacePrefix}${profileIdc}.${compatHex}.${tierChar}${levelIdc}${constraintStr}`;
1569 1639 }
1570 1640 function readItemData(buffer, loc, idatOffset) {
1571 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 + }
1572 1649 if (loc.extents.length === 1) {
1573 1650 const ext = loc.extents[0];
1574 1651 const start = baseOffset + ext.offset;
1575 1652 return new Uint8Array(buffer.slice(start, start + ext.length));
@@ -2056,8 +2133,11 @@
2056 2133 return `${cause} ${getHeicConversionAdvice()}`;
2057 2134 }
2058 2135
2059 2136 // packages/upload-media/build-module/canvas-utils.mjs
2137 + var IMAGE_DECODER_TIMEOUT = 15e3;
2138 + var HeicUnsupportedError = class extends Error {
2139 + };
2060 2140 async function canvasConvertToJpeg(file, quality = 0.82) {
2061 2141 const baseName = getFileBasename(file.name);
2062 2142 try {
2063 2143 const bitmap = await createImageBitmap(file);
@@ -2086,10 +2166,36 @@
2086 2166 const decoder = new ImageDecoder({
2087 2167 type: file.type,
2088 2168 data: file.stream()
2089 2169 });
2170 + let videoFrame;
2171 + let timeoutId;
2172 + let timedOut = false;
2090 2173 try {
2091 - 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) {
2092 2198 try {
2093 2199 const canvas = new OffscreenCanvas(
2094 2200 videoFrame.displayWidth,
2095 2201 videoFrame.displayHeight
@@ -2108,58 +2214,62 @@
2108 2214 });
2109 2215 } finally {
2110 2216 videoFrame.close();
2111 2217 }
2112 - } finally {
2113 - decoder.close();
2114 2218 }
2115 2219 }
2116 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 + }
2117 2229 if (typeof VideoDecoder !== "undefined") {
2230 + let supported = false;
2118 2231 try {
2119 - const heicData = parseHeic(await file.arrayBuffer());
2120 2232 const support = await VideoDecoder.isConfigSupported({
2121 2233 codec: heicData.codecString
2122 2234 });
2123 - if (support.supported) {
2124 - const canvas = new OffscreenCanvas(
2125 - heicData.outputWidth,
2126 - 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
2127 2254 );
2128 - const ctx = canvas.getContext("2d");
2129 - if (!ctx) {
2130 - throw new Error("Could not get canvas 2d context");
2255 + try {
2256 + ctx.drawImage(frame, tile.x, tile.y);
2257 + } finally {
2258 + frame.close();
2131 2259 }
2132 - for (const tile of heicData.tiles) {
2133 - const frame = await decodeHevcFrame(
2134 - heicData.codecString,
2135 - heicData.description,
2136 - heicData.tileWidth,
2137 - heicData.tileHeight,
2138 - tile.data
2139 - );
2140 - try {
2141 - ctx.drawImage(frame, tile.x, tile.y);
2142 - } finally {
2143 - frame.close();
2144 - }
2145 - }
2146 - const outputCanvas = heicData.rotation !== 0 ? applyRotation(canvas, heicData.rotation) : applyExifOrientation(
2147 - canvas,
2148 - heicData.exifOrientation
2149 - );
2150 - const jpegBlob = await outputCanvas.convertToBlob({
2151 - type: "image/jpeg",
2152 - quality
2153 - });
2154 - return new File([jpegBlob], `${baseName}.jpg`, {
2155 - type: "image/jpeg"
2156 - });
2157 2260 }
2158 - } 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 + });
2159 2269 }
2160 2270 }
2161 - throw new Error(getHeicUnsupportedMessage());
2271 + throw new HeicUnsupportedError(getHeicUnsupportedMessage());
2162 2272 }
2163 2273 function applyRotation(source, rotation) {
2164 2274 if (rotation === 0) {
2165 2275 return source;
@@ -2529,8 +2639,11 @@
2529 2639 const item = select2.getItem(id);
2530 2640 if (!item) {
2531 2641 return;
2532 2642 }
2643 + if (item.currentOperation) {
2644 + return;
2645 + }
2533 2646 const {
2534 2647 attachment,
2535 2648 onChange,
2536 2649 onSuccess,
@@ -2808,13 +2921,16 @@
2808 2921 }
2809 2922 }
2810 2923 }
2811 2924 let heicJpeg = null;
2812 - const isImage = file.type.startsWith("image/");
2813 - const isVipsSupported = CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(
2814 - file.type
2815 - );
2816 - 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;
2817 2933 let tooLargeForClient = false;
2818 2934 if (isImage && isVipsSupported) {
2819 2935 const dimensions = await getImageDimensions(file);
2820 2936 if (dimensions && exceedsClientProcessingMemory(dimensions)) {
@@ -2820,9 +2936,9 @@
2820 2936 if (dimensions && exceedsClientProcessingMemory(dimensions)) {
2821 2937 tooLargeForClient = true;
2822 2938 }
2823 2939 }
2824 - if (file.type === "image/jpeg" && !tooLargeForClient) {
2940 + if (file.type === "image/jpeg" && !isHeic && !tooLargeForClient) {
2825 2941 operations.push(OperationType.DetectUltraHdr);
2826 2942 }
2827 2943 if (isImage && isVipsSupported && !tooLargeForClient) {
2828 2944 operations.push(
@@ -2832,18 +2948,22 @@
2832 2948 );
2833 2949 } else if (isImage && isHeic) {
2834 2950 try {
2835 2951 heicJpeg = await canvasConvertToJpeg(
2836 - file,
2952 + heicFile,
2837 2953 settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY
2838 2954 );
2839 - } catch {
2955 + } catch (error) {
2956 + const unsupported = error instanceof HeicUnsupportedError;
2840 2957 dispatch.cancelItem(
2841 2958 id,
2842 2959 new UploadError({
2843 - code: ErrorCode.HEIC_DECODE_ERROR,
2844 - message: getHeicUnsupportedMessage(),
2845 - 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
2846 2966 })
2847 2967 );
2848 2968 return;
2849 2969 }
@@ -2865,9 +2985,9 @@
2865 2985 const vipsAvailable = isClientSideMediaSupported();
2866 2986 updates = {
2867 2987 file: heicJpeg,
2868 2988 sourceFile: heicJpeg,
2869 - originalHeicFile: item.file,
2989 + originalHeicFile: heicFile,
2870 2990 additionalData: {
2871 2991 ...item.additionalData,
2872 2992 generate_sub_sizes: !vipsAvailable,
2873 2993 convert_format: true