PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | build/scripts/upload-media/index.js +173 -53 23.9.1 → 24.1.0 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