@evfrenkel/decap-cms-core 3.13.0-image-conversions.2 → 3.13.0-image-conversions.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@evfrenkel/decap-cms-core.js +18 -18
- package/dist/@evfrenkel/decap-cms-core.js.map +1 -1
- package/dist/esm/bootstrap.js +2 -2
- package/dist/esm/components/UI/ErrorBoundary.js +2 -2
- package/dist/esm/lib/imageTransformations.js +13 -1
- package/dist/esm/reducers/entries.js +10 -2
- package/dist/esm/reducers/mediaLibrary.js +5 -1
- package/package.json +1 -1
- package/src/lib/__tests__/imageTransformations.spec.ts +53 -0
- package/src/lib/imageTransformations.ts +15 -1
- package/src/reducers/__tests__/entries.spec.js +12 -0
- package/src/reducers/__tests__/mediaLibrary.spec.js +29 -0
- package/src/reducers/entries.ts +13 -2
- package/src/reducers/mediaLibrary.ts +6 -1
package/dist/esm/bootstrap.js
CHANGED
|
@@ -49,8 +49,8 @@ function bootstrap(opts = {}) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Log the version number.
|
|
51
51
|
*/
|
|
52
|
-
if (typeof "3.13.0-image-conversions.
|
|
53
|
-
console.log(`decap-cms-core ${"3.13.0-image-conversions.
|
|
52
|
+
if (typeof "3.13.0-image-conversions.5" === 'string') {
|
|
53
|
+
console.log(`decap-cms-core ${"3.13.0-image-conversions.5"}`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -42,8 +42,8 @@ function buildIssueTemplate({
|
|
|
42
42
|
let version = '';
|
|
43
43
|
if (typeof DECAP_CMS_VERSION === 'string') {
|
|
44
44
|
version = `decap-cms@${DECAP_CMS_VERSION}`;
|
|
45
|
-
} else if (typeof "3.12.2-image-conversions.
|
|
46
|
-
version = `decap-cms-app@${"3.12.2-image-conversions.
|
|
45
|
+
} else if (typeof "3.12.2-image-conversions.5" === 'string') {
|
|
46
|
+
version = `decap-cms-app@${"3.12.2-image-conversions.5"}`;
|
|
47
47
|
}
|
|
48
48
|
const template = getIssueTemplate({
|
|
49
49
|
version,
|
|
@@ -98,11 +98,23 @@ function getImageBytes(image, format, quality = 75) {
|
|
|
98
98
|
}
|
|
99
99
|
return image.get_bytes();
|
|
100
100
|
}
|
|
101
|
+
async function getFileBytes(file) {
|
|
102
|
+
if (typeof file.arrayBuffer === 'function') {
|
|
103
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const reader = new FileReader();
|
|
107
|
+
reader.onload = () => resolve(new Uint8Array(reader.result));
|
|
108
|
+
reader.onerror = () => reject(reader.error);
|
|
109
|
+
reader.readAsArrayBuffer(file);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
101
112
|
export async function transformImage(file, originalPath, config) {
|
|
102
113
|
const photon = await import('@silvia-odwyer/photon');
|
|
103
114
|
const initPhoton = photon.default;
|
|
104
115
|
await initPhoton();
|
|
105
|
-
const
|
|
116
|
+
const imageBytes = await getFileBytes(file);
|
|
117
|
+
const originalImage = photon.PhotonImage.new_from_byteslice(imageBytes);
|
|
106
118
|
try {
|
|
107
119
|
const transformedFiles = config.variants.map(variant => {
|
|
108
120
|
const {
|
|
@@ -439,6 +439,14 @@ function getFileField(collectionFiles, slug) {
|
|
|
439
439
|
const file = collectionFiles.find(f => f?.get('name') === slug);
|
|
440
440
|
return file;
|
|
441
441
|
}
|
|
442
|
+
function getMediaPublicPathSegment(mediaPath) {
|
|
443
|
+
const normalizedPath = trim(mediaPath, '/');
|
|
444
|
+
const transformationIndex = normalizedPath.indexOf('_transformations/');
|
|
445
|
+
if (transformationIndex >= 0) {
|
|
446
|
+
return normalizedPath.slice(transformationIndex);
|
|
447
|
+
}
|
|
448
|
+
return basename(mediaPath);
|
|
449
|
+
}
|
|
442
450
|
function hasCustomFolder(folderKey, collection, slug, field) {
|
|
443
451
|
if (!collection) {
|
|
444
452
|
return false;
|
|
@@ -556,9 +564,9 @@ export function selectMediaFilePublicPath(config, collection, mediaPath, entryMa
|
|
|
556
564
|
publicFolder = normalizeDoubleSlashes(evaluateFolder(name, config, collection, entryMap, field));
|
|
557
565
|
}
|
|
558
566
|
if (isAbsolutePath(publicFolder)) {
|
|
559
|
-
return joinUrlPath(publicFolder,
|
|
567
|
+
return joinUrlPath(publicFolder, getMediaPublicPathSegment(mediaPath));
|
|
560
568
|
}
|
|
561
|
-
return join(publicFolder,
|
|
569
|
+
return join(publicFolder, getMediaPublicPathSegment(mediaPath));
|
|
562
570
|
}
|
|
563
571
|
export function selectEditingDraft(state) {
|
|
564
572
|
const entry = state.get('entry');
|
|
@@ -4,6 +4,10 @@ import { dirname } from 'path';
|
|
|
4
4
|
import { MEDIA_LIBRARY_OPEN, MEDIA_LIBRARY_CLOSE, MEDIA_LIBRARY_CREATE, MEDIA_INSERT, MEDIA_REMOVE_INSERTED, MEDIA_LOAD_REQUEST, MEDIA_LOAD_SUCCESS, MEDIA_LOAD_FAILURE, MEDIA_PERSIST_REQUEST, MEDIA_PERSIST_SUCCESS, MEDIA_PERSIST_FAILURE, MEDIA_DELETE_REQUEST, MEDIA_DELETE_SUCCESS, MEDIA_DELETE_FAILURE, MEDIA_DISPLAY_URL_REQUEST, MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE } from '../actions/mediaLibrary';
|
|
5
5
|
import { selectEditingDraft, selectMediaFolder } from './entries';
|
|
6
6
|
import { selectIntegration } from './';
|
|
7
|
+
function isMediaFileInFolder(filePath, mediaFolder) {
|
|
8
|
+
const fileFolder = dirname(filePath);
|
|
9
|
+
return fileFolder === mediaFolder || fileFolder.startsWith(`${mediaFolder}/_transformations/`);
|
|
10
|
+
}
|
|
7
11
|
const defaultState = {
|
|
8
12
|
isVisible: false,
|
|
9
13
|
showMediaButton: true,
|
|
@@ -231,7 +235,7 @@ export function selectMediaFiles(state, field) {
|
|
|
231
235
|
const entry = entryDraft.get('entry');
|
|
232
236
|
const collection = state.collections.get(entry?.get('collection'));
|
|
233
237
|
const mediaFolder = selectMediaFolder(state.config, collection, entry, field);
|
|
234
|
-
files = entryFiles.filter(f =>
|
|
238
|
+
files = entryFiles.filter(f => isMediaFileInFolder(f.path, mediaFolder)).map(file => ({
|
|
235
239
|
key: file.id,
|
|
236
240
|
...file
|
|
237
241
|
}));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evfrenkel/decap-cms-core",
|
|
3
3
|
"description": "Decap CMS core application, see decap-cms package for the main distribution.",
|
|
4
|
-
"version": "3.13.0-image-conversions.
|
|
4
|
+
"version": "3.13.0-image-conversions.6",
|
|
5
5
|
"module": "dist/esm/index.js",
|
|
6
6
|
"main": "dist/decap-cms-core.js",
|
|
7
7
|
"files": [
|
|
@@ -4,9 +4,41 @@ import {
|
|
|
4
4
|
getImageTransformationsConfig,
|
|
5
5
|
shouldTransformImage,
|
|
6
6
|
sortTransformationFilesForSelection,
|
|
7
|
+
transformImage,
|
|
7
8
|
} from '../imageTransformations';
|
|
8
9
|
|
|
10
|
+
const mockPhotonImage = {
|
|
11
|
+
get_width: jest.fn(() => 100),
|
|
12
|
+
get_height: jest.fn(() => 50),
|
|
13
|
+
get_bytes_webp: jest.fn(() => new Uint8Array([1, 2, 3])),
|
|
14
|
+
get_bytes_jpeg: jest.fn(() => new Uint8Array([4, 5, 6])),
|
|
15
|
+
get_bytes: jest.fn(() => new Uint8Array([7, 8, 9])),
|
|
16
|
+
free: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockNewFromByteslice = jest.fn(() => mockPhotonImage);
|
|
20
|
+
const mockNewFromBlob = jest.fn();
|
|
21
|
+
|
|
22
|
+
jest.mock(
|
|
23
|
+
'@silvia-odwyer/photon',
|
|
24
|
+
() => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: jest.fn(() => Promise.resolve()),
|
|
27
|
+
PhotonImage: {
|
|
28
|
+
new_from_byteslice: mockNewFromByteslice,
|
|
29
|
+
new_from_blob: mockNewFromBlob,
|
|
30
|
+
},
|
|
31
|
+
resize: jest.fn(),
|
|
32
|
+
SamplingFilter: { Lanczos3: 'Lanczos3' },
|
|
33
|
+
}),
|
|
34
|
+
{ virtual: true },
|
|
35
|
+
);
|
|
36
|
+
|
|
9
37
|
describe('imageTransformations', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
10
42
|
describe('getImageTransformationsConfig', () => {
|
|
11
43
|
it('normalizes array shorthand and keeps the original by default', () => {
|
|
12
44
|
expect(
|
|
@@ -105,4 +137,25 @@ describe('imageTransformations', () => {
|
|
|
105
137
|
expect(sortTransformationFilesForSelection([original, small])).toEqual([small, original]);
|
|
106
138
|
});
|
|
107
139
|
});
|
|
140
|
+
|
|
141
|
+
describe('transformImage', () => {
|
|
142
|
+
it('creates the Photon image from file bytes instead of blob', async () => {
|
|
143
|
+
const file = new File([new Uint8Array([255, 216, 255])], 'image.jpg', {
|
|
144
|
+
type: 'image/jpeg',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const files = await transformImage(file, 'uploads/image.jpg', {
|
|
148
|
+
keepOriginal: false,
|
|
149
|
+
variants: [{ name: 'webp', format: 'webp', keep_original_size: true }],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(mockNewFromByteslice).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(mockNewFromByteslice.mock.calls[0][0]).toEqual(new Uint8Array([255, 216, 255]));
|
|
154
|
+
expect(mockNewFromBlob).not.toHaveBeenCalled();
|
|
155
|
+
expect(files).toHaveLength(1);
|
|
156
|
+
expect(files[0].file.name).toBe('image.webp');
|
|
157
|
+
expect(files[0].path).toBe('uploads/_transformations/webp/image.webp');
|
|
158
|
+
expect(mockPhotonImage.free).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
108
161
|
});
|
|
@@ -174,6 +174,19 @@ function getImageBytes(image: PhotonImage, format: string, quality = 75) {
|
|
|
174
174
|
return image.get_bytes();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
async function getFileBytes(file: File) {
|
|
178
|
+
if (typeof file.arrayBuffer === 'function') {
|
|
179
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return new Promise<Uint8Array>((resolve, reject) => {
|
|
183
|
+
const reader = new FileReader();
|
|
184
|
+
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
|
|
185
|
+
reader.onerror = () => reject(reader.error);
|
|
186
|
+
reader.readAsArrayBuffer(file);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
177
190
|
export async function transformImage(
|
|
178
191
|
file: File,
|
|
179
192
|
originalPath: string,
|
|
@@ -183,7 +196,8 @@ export async function transformImage(
|
|
|
183
196
|
const initPhoton = (photon as unknown as { default: () => Promise<unknown> }).default;
|
|
184
197
|
await initPhoton();
|
|
185
198
|
|
|
186
|
-
const
|
|
199
|
+
const imageBytes = await getFileBytes(file);
|
|
200
|
+
const originalImage = photon.PhotonImage.new_from_byteslice(imageBytes);
|
|
187
201
|
|
|
188
202
|
try {
|
|
189
203
|
const transformedFiles = config.variants.map(variant => {
|
|
@@ -549,6 +549,18 @@ describe('entries', () => {
|
|
|
549
549
|
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
+
it('should preserve generated transformation paths', () => {
|
|
553
|
+
expect(
|
|
554
|
+
selectMediaFilePublicPath(
|
|
555
|
+
{ public_folder: '/uploads' },
|
|
556
|
+
null,
|
|
557
|
+
'public/uploads/_transformations/webp/kittens.webp',
|
|
558
|
+
undefined,
|
|
559
|
+
undefined,
|
|
560
|
+
),
|
|
561
|
+
).toBe('/uploads/_transformations/webp/kittens.webp');
|
|
562
|
+
});
|
|
563
|
+
|
|
552
564
|
it('should handle file public_folder', () => {
|
|
553
565
|
const entry = fromJS({
|
|
554
566
|
path: 'src/posts/index.md',
|
|
@@ -108,6 +108,35 @@ describe('mediaLibrary', () => {
|
|
|
108
108
|
expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it('should select draft transformation media files from collection when editing a draft', () => {
|
|
112
|
+
const { selectEditingDraft, selectMediaFolder } = require('../../reducers/entries');
|
|
113
|
+
|
|
114
|
+
selectEditingDraft.mockReturnValue(true);
|
|
115
|
+
selectMediaFolder.mockReturnValue('static/images/posts');
|
|
116
|
+
|
|
117
|
+
const imageField = fromJS({ name: 'image' });
|
|
118
|
+
const collection = fromJS({ fields: [imageField] });
|
|
119
|
+
const entry = fromJS({
|
|
120
|
+
collection: 'posts',
|
|
121
|
+
mediaFiles: [
|
|
122
|
+
{ id: 1, path: 'static/images/posts/_transformations/webp/logo.webp' },
|
|
123
|
+
{ id: 2, path: 'static/images/other/_transformations/webp/image.webp' },
|
|
124
|
+
],
|
|
125
|
+
data: {},
|
|
126
|
+
});
|
|
127
|
+
const state = {
|
|
128
|
+
config: {},
|
|
129
|
+
collections: fromJS({ posts: collection }),
|
|
130
|
+
entryDraft: fromJS({
|
|
131
|
+
entry,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
expect(selectMediaFiles(state, imageField)).toEqual([
|
|
136
|
+
{ id: 1, key: 1, path: 'static/images/posts/_transformations/webp/logo.webp' },
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
111
140
|
it('should select global media files when not editing a draft', () => {
|
|
112
141
|
const { selectEditingDraft } = require('../../reducers/entries');
|
|
113
142
|
|
package/src/reducers/entries.ts
CHANGED
|
@@ -545,6 +545,17 @@ function getFileField(collectionFiles: CollectionFiles, slug: string | undefined
|
|
|
545
545
|
return file;
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
function getMediaPublicPathSegment(mediaPath: string) {
|
|
549
|
+
const normalizedPath = trim(mediaPath, '/');
|
|
550
|
+
const transformationIndex = normalizedPath.indexOf('_transformations/');
|
|
551
|
+
|
|
552
|
+
if (transformationIndex >= 0) {
|
|
553
|
+
return normalizedPath.slice(transformationIndex);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return basename(mediaPath);
|
|
557
|
+
}
|
|
558
|
+
|
|
548
559
|
function hasCustomFolder(
|
|
549
560
|
folderKey: 'media_folder' | 'public_folder',
|
|
550
561
|
collection: Collection | null,
|
|
@@ -804,10 +815,10 @@ export function selectMediaFilePublicPath(
|
|
|
804
815
|
}
|
|
805
816
|
|
|
806
817
|
if (isAbsolutePath(publicFolder)) {
|
|
807
|
-
return joinUrlPath(publicFolder,
|
|
818
|
+
return joinUrlPath(publicFolder, getMediaPublicPathSegment(mediaPath));
|
|
808
819
|
}
|
|
809
820
|
|
|
810
|
-
return join(publicFolder,
|
|
821
|
+
return join(publicFolder, getMediaPublicPathSegment(mediaPath));
|
|
811
822
|
}
|
|
812
823
|
|
|
813
824
|
export function selectEditingDraft(state: EntryDraft) {
|
|
@@ -34,6 +34,11 @@ import type {
|
|
|
34
34
|
EntryField,
|
|
35
35
|
} from '../types/redux';
|
|
36
36
|
|
|
37
|
+
function isMediaFileInFolder(filePath: string, mediaFolder: string) {
|
|
38
|
+
const fileFolder = dirname(filePath);
|
|
39
|
+
return fileFolder === mediaFolder || fileFolder.startsWith(`${mediaFolder}/_transformations/`);
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
const defaultState: {
|
|
38
43
|
isVisible: boolean;
|
|
39
44
|
showMediaButton: boolean;
|
|
@@ -270,7 +275,7 @@ export function selectMediaFiles(state: State, field?: EntryField) {
|
|
|
270
275
|
const collection = state.collections.get(entry?.get('collection'));
|
|
271
276
|
const mediaFolder = selectMediaFolder(state.config, collection, entry, field);
|
|
272
277
|
files = entryFiles
|
|
273
|
-
.filter(f =>
|
|
278
|
+
.filter(f => isMediaFileInFolder(f.path, mediaFolder))
|
|
274
279
|
.map(file => ({ key: file.id, ...file }));
|
|
275
280
|
} else {
|
|
276
281
|
files = mediaLibrary.get('files') || [];
|