├── .editorconfig
├── .gitattributes
├── .gitignore
├── README.md
├── admin
├── build
│ ├── 53771798877e88bccc275e15ba634a83.svg
│ ├── c6e5a6171e9789587d2e50f79a728506.svg
│ ├── fa63055a671545051cd06bbbf3c544a3.svg
│ ├── main.js
│ └── main.js.map
└── src
│ ├── components
│ └── StepEditor
│ │ └── index.js
│ ├── containers
│ ├── App
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── selectors.js
│ ├── EditImageFormatPage
│ │ ├── Preview
│ │ │ ├── actions.js
│ │ │ ├── constants.js
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ ├── saga.js
│ │ │ ├── selectors.js
│ │ │ └── styles.scss
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── saga.js
│ │ ├── selectors.js
│ │ └── styles.scss
│ ├── HomePage
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── saga.js
│ │ ├── selectors.js
│ │ └── styles.scss
│ └── NotFoundPage
│ │ └── index.js
│ ├── jimpMethodConfigs
│ ├── blur.js
│ ├── contain.js
│ ├── crop.js
│ ├── index.js
│ ├── pixelate.js
│ └── resize.js
│ ├── pluginId.js
│ └── translations
│ ├── ar.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── it.json
│ ├── ko.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt-BR.json
│ ├── pt.json
│ ├── ru.json
│ ├── tr.json
│ ├── zh-Hans.json
│ └── zh.json
├── azure-pipelines.yml
├── config
├── functions
│ └── bootstrap.js
├── queries
│ ├── bookshelf.js
│ └── mongoose.js
└── routes.json
├── controllers
└── ImageFormats.js
├── models
├── FormattedImage.js
├── FormattedImage.settings.json
├── ImageFormat.js
└── ImageFormat.settings.json
├── package.json
├── services
├── ImageFormats.js
├── jimpMethods
│ ├── JimpMethod.js
│ ├── JimpMethod.spec.js
│ └── index.js
├── sample_photo.jpg
└── utils
│ └── upload
│ ├── getFileDescriptor.js
│ ├── getUploadProvider.js
│ └── relateFileToContent.js
└── video_thumbnail.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = false
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text eol=lf
23 | *.coffee text
24 | *.json text
25 | *.htm text
26 | *.html text
27 | *.xml text
28 | *.svg text
29 | *.txt text
30 | *.ini text
31 | *.inc text
32 | *.pl text
33 | *.rb text
34 | *.py text
35 | *.scm text
36 | *.sql text
37 | *.sh text
38 | *.bat text
39 |
40 | # templates
41 | *.ejs text
42 | *.hbt text
43 | *.jade text
44 | *.haml text
45 | *.hbs text
46 | *.dot text
47 | *.tmpl text
48 | *.phtml text
49 |
50 | # git config
51 | .gitattributes text
52 | .gitignore text
53 | .gitconfig text
54 |
55 | # code analysis config
56 | .jshintrc text
57 | .jscsrc text
58 | .jshintignore text
59 | .csslintrc text
60 |
61 | # misc config
62 | *.yaml text
63 | *.yml text
64 | .editorconfig text
65 |
66 | # build config
67 | *.npmignore text
68 | *.bowerrc text
69 |
70 | # Heroku
71 | Procfile text
72 | .slugignore text
73 |
74 | # Documentation
75 | *.md text
76 | LICENSE text
77 | AUTHORS text
78 |
79 |
80 | #
81 | ## These files are binary and should be left untouched
82 | #
83 |
84 | # (binary is a macro for -text -diff)
85 | *.png binary
86 | *.jpg binary
87 | *.jpeg binary
88 | *.gif binary
89 | *.ico binary
90 | *.mov binary
91 | *.mp4 binary
92 | *.mp3 binary
93 | *.flv binary
94 | *.fla binary
95 | *.swf binary
96 | *.gz binary
97 | *.zip binary
98 | *.7z binary
99 | *.ttf binary
100 | *.eot binary
101 | *.woff binary
102 | *.pyc binary
103 | *.pdf binary
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't check auto-generated stuff into git
2 | coverage
3 | node_modules
4 | stats.json
5 | package-lock.json
6 |
7 | # Cruft
8 | .DS_Store
9 | npm-debug.log
10 | .idea
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Strapi Image Formats Plugin
2 |
3 | Process uploaded images on demand with Strapi Node CMS
4 |
5 | [](https://dev.azure.com/joebeuckman0156/Strapi%20Plugins/_build/latest?definitionId=2&branchName=master)
6 |
7 | ### Installation
8 |
9 | ```
10 | cd my-strapi-project/plugins
11 | git clone https://github.com/jbeuckm/strapi-plugin-image-formats.git image-formats
12 | cd image-formats && npm install
13 | cd ../..
14 | npm run setup --plugins
15 | ```
16 |
17 | _\* the last step takes a notoriously long time..._
18 |
19 | ### Configuration
20 |
21 | When plugin has been installed, you need to allow access to the endpoints.
22 |
23 | 1. Navigate to Users & Permissions.
24 | 2. Pick the role you would like to give permission.
25 | 3. Scroll down and expand the section **Image Formats**.
26 | 4. Check "Select All" for the endpoints under "Imageformats".
27 | 5. Scroll up and press "Save"
28 |
29 | ### Usage
30 |
31 | Click for video demo:
32 | [](https://youtu.be/tE8nNDoTiuk)
33 |
--------------------------------------------------------------------------------
/admin/build/53771798877e88bccc275e15ba634a83.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/build/c6e5a6171e9789587d2e50f79a728506.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/admin/build/fa63055a671545051cd06bbbf3c544a3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/build/main.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"main.js","sources":["webpack:///main.js"],"mappings":"AAAA;;;;;AAKA;;;;;;;;AAQA;;;;;;;;AAQA;;;;;;;;AAQA;;;;;;AAMA;;;;;;AAMA;;;;;;AAMA;;;;;;;;AAQA;;;;;AAKA;;;;;AAKA;;;;;;;;AAQA","sourceRoot":""}
--------------------------------------------------------------------------------
/admin/src/components/StepEditor/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import PropTypes from "prop-types";
3 | import jimpMethodConfigs from "../../jimpMethodConfigs";
4 |
5 | const styles = {
6 | argument: {
7 | paddingLeft: 8,
8 | paddingRight: 8
9 | },
10 | argumentLabel: {
11 | fontWeight: 900,
12 | paddingRight: 8
13 | }
14 | };
15 |
16 | class StepEditor extends Component {
17 | constructor(props) {
18 | super(props);
19 |
20 | this.state = props.step;
21 | }
22 |
23 | reportChanged = () => {
24 | this.props.onChange(this.state);
25 | };
26 |
27 | onChangeMethod = event => {
28 | const newMethod = event.target.value;
29 | const argumentConfigs = jimpMethodConfigs[newMethod];
30 |
31 | const params = _.mapValues(argumentConfigs, "default");
32 |
33 | this.setState({ method: newMethod, params }, this.reportChanged);
34 | };
35 |
36 | onChangeValue = (argumentName, newValue) => {
37 | this.setState(
38 | {
39 | params: {
40 | ...this.state.params,
41 | [argumentName]: newValue
42 | }
43 | },
44 | this.reportChanged
45 | );
46 | };
47 |
48 | renderInput = (argumentName, config) => {
49 | const value = this.state.params[argumentName];
50 |
51 | switch (config.type) {
52 | case "integer":
53 | return (
54 |
60 | this.onChangeValue(argumentName, parseFloat(event.target.value))
61 | }
62 | />
63 | );
64 |
65 | case "select":
66 | return (
67 |
77 | );
78 |
79 | default:
80 | return null;
81 | }
82 | };
83 |
84 | render() {
85 | const argumentConfigs = jimpMethodConfigs[this.state.method];
86 |
87 | return (
88 |
89 |
90 |
91 |
96 |
97 |
98 | {Object.keys(argumentConfigs).map(argumentName => {
99 | const config = argumentConfigs[argumentName];
100 |
101 | return (
102 |
103 |
104 | {this.renderInput(argumentName, config)}
105 |
106 | );
107 | })}
108 |
109 | );
110 | }
111 | }
112 |
113 | StepEditor.propTypes = {
114 | step: PropTypes.object.isRequired,
115 | onChange: PropTypes.func.isRequired
116 | };
117 |
118 | export default StepEditor;
119 |
--------------------------------------------------------------------------------
/admin/src/containers/App/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App actions
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/admin/src/containers/App/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App constants
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/admin/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { createStructuredSelector } from 'reselect';
5 | import { Switch, Route } from 'react-router-dom';
6 | import { bindActionCreators, compose } from 'redux';
7 |
8 | // Utils
9 | import pluginId from 'pluginId';
10 |
11 | // Containers
12 | import HomePage from 'containers/HomePage';
13 | import EditImageFormatPage from 'containers/EditImageFormatPage';
14 | import NotFoundPage from 'containers/NotFoundPage';
15 |
16 | import reducer from './reducer';
17 |
18 | class App extends React.Component {
19 | render() {
20 | return (
21 |
22 |
23 |
24 |
28 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | App.contextTypes = {
40 | plugins: PropTypes.object,
41 | updatePlugin: PropTypes.func
42 | };
43 |
44 | App.propTypes = {
45 | history: PropTypes.object.isRequired
46 | };
47 |
48 | export function mapDispatchToProps(dispatch) {
49 | return bindActionCreators({}, dispatch);
50 | }
51 |
52 | const mapStateToProps = createStructuredSelector({});
53 |
54 | // Wrap the component to inject dispatch and state into it
55 | const withConnect = connect(
56 | mapStateToProps,
57 | mapDispatchToProps
58 | );
59 | const withReducer = strapi.injectReducer({ key: 'global', reducer, pluginId });
60 |
61 | export default compose(
62 | withReducer,
63 | withConnect
64 | )(App);
65 |
--------------------------------------------------------------------------------
/admin/src/containers/App/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 |
9 | const initialState = fromJS({});
10 |
11 | function appReducer(state = initialState, action) {
12 | switch (action.type) {
13 | default:
14 | return state;
15 | }
16 | }
17 |
18 | export default appReducer;
19 |
--------------------------------------------------------------------------------
/admin/src/containers/App/selectors.js:
--------------------------------------------------------------------------------
1 | // import { createSelector } from 'reselect';
2 | // import pluginId from 'pluginId';
3 |
4 | /**
5 | * Direct selector to the list state domain
6 | */
7 |
8 | // const selectGlobalDomain = () => state => state.get(`${pluginId}_global`);
9 |
10 | export {};
11 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | FETCH_PREVIEW,
3 | FETCH_PREVIEW_ERROR,
4 | FETCH_PREVIEW_SUCCESS
5 | } from "./constants";
6 |
7 | export const fetchPreview = steps => ({
8 | type: FETCH_PREVIEW,
9 | payload: { steps }
10 | });
11 | export const fetchPreviewSuccess = imageDataUri => ({
12 | type: FETCH_PREVIEW_SUCCESS,
13 | payload: { imageDataUri }
14 | });
15 | export const fetchPreviewError = error => ({
16 | type: FETCH_PREVIEW_ERROR,
17 | error: true,
18 | payload: error
19 | });
20 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/constants.js:
--------------------------------------------------------------------------------
1 | import pluginId from "pluginId";
2 |
3 | const buildString = suffix => `${pluginId}/ImageFormatPreview/${suffix}`;
4 |
5 | export const FETCH_PREVIEW = buildString("FETCH_PREVIEW");
6 | export const FETCH_PREVIEW_ERROR = buildString("FETCH_PREVIEW_ERROR");
7 | export const FETCH_PREVIEW_SUCCESS = buildString("FETCH_PREVIEW_SUCCESS");
8 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { createStructuredSelector } from "reselect";
4 | import { injectIntl } from "react-intl";
5 | import { compose } from "redux";
6 | import pluginId from "pluginId";
7 | import styles from "./styles.scss";
8 |
9 | import { fetchPreview } from "./actions";
10 | import { makeSelectLoading, makeSelectImageDataUri } from "./selectors";
11 | import reducer from "./reducer";
12 | import saga from "./saga";
13 |
14 | import PropTypes from "prop-types";
15 |
16 | class Preview extends Component {
17 | state = { dimensions: null };
18 |
19 | componentDidMount() {
20 | this.imageNeedsUpdate = true;
21 | this.updateImageData();
22 | }
23 |
24 | componentDidUpdate(prevProps) {
25 | const prevSteps = prevProps.steps;
26 | const nextSteps = this.props.steps;
27 |
28 | if (!_.isEqual(prevSteps, nextSteps)) {
29 | this.imageNeedsUpdate = true;
30 | this.updateImageData();
31 | }
32 | }
33 |
34 | updateImageData = async () => {
35 | if (this.props.loadingImage || !this.imageNeedsUpdate) {
36 | return;
37 | }
38 | this.imageNeedsUpdate = false;
39 |
40 | this.props.fetchPreview(this.props.steps);
41 | };
42 |
43 | onImageLoaded = event => {
44 | const { naturalWidth, naturalHeight } = event.target;
45 | this.setState({ dimensions: `${naturalWidth} x ${naturalHeight}` });
46 |
47 | this.updateImageData();
48 | };
49 |
50 | render() {
51 | const { dimensions } = this.state;
52 | const { imageDataUri } = this.props;
53 |
54 | return (
55 |
56 |
57 |
{dimensions}
58 |

63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | Preview.propTypes = {
70 | steps: PropTypes.array.isRequired,
71 | fetchPreview: PropTypes.func.isRequired,
72 | imageDataUri: PropTypes.string.isRequired,
73 | loading: PropTypes.bool
74 | };
75 |
76 | const mapDispatchToProps = {
77 | fetchPreview
78 | };
79 |
80 | const mapStateToProps = createStructuredSelector({
81 | loading: makeSelectLoading(),
82 | imageDataUri: makeSelectImageDataUri()
83 | });
84 |
85 | const withConnect = connect(
86 | mapStateToProps,
87 | mapDispatchToProps
88 | );
89 |
90 | const withReducer = strapi.injectReducer({
91 | key: "imageFormatPreview",
92 | reducer,
93 | pluginId
94 | });
95 | const withSaga = strapi.injectSaga({
96 | key: "imageFormatPreview",
97 | saga,
98 | pluginId
99 | });
100 |
101 | export default compose(
102 | withReducer,
103 | withSaga,
104 | withConnect
105 | )(injectIntl(Preview));
106 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from "immutable";
2 |
3 | import {
4 | FETCH_PREVIEW,
5 | FETCH_PREVIEW_ERROR,
6 | FETCH_PREVIEW_SUCCESS
7 | } from "./constants";
8 |
9 | const initialState = fromJS({
10 | loading: false,
11 | error: null,
12 | imageDataUri: null
13 | });
14 |
15 | function previewImageReducer(state = initialState, action) {
16 | const { type, payload } = action;
17 |
18 | switch (type) {
19 | case FETCH_PREVIEW:
20 | return state
21 | .set("error", null)
22 | .set("loading", true)
23 | .set("imageDataUri", null);
24 |
25 | case FETCH_PREVIEW_SUCCESS: {
26 | return state
27 | .set("loading", false)
28 | .set("imageDataUri", payload.imageDataUri);
29 | }
30 |
31 | case FETCH_PREVIEW_ERROR:
32 | return state.set("loading", false).set("error", payload);
33 |
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | export default previewImageReducer;
40 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/saga.js:
--------------------------------------------------------------------------------
1 | import { fork, takeLatest, call, put } from "redux-saga/effects";
2 | import auth from "utils/auth";
3 |
4 | import { fetchPreviewError, fetchPreviewSuccess } from "./actions";
5 | import { FETCH_PREVIEW } from "./constants";
6 |
7 | function _arrayBufferToBase64(buffer) {
8 | var binary = "";
9 | var bytes = new Uint8Array(buffer);
10 | var len = bytes.byteLength;
11 | for (var i = 0; i < len; i++) {
12 | binary += String.fromCharCode(bytes[i]);
13 | }
14 | return window.btoa(binary);
15 | }
16 |
17 | export function* fetchPreviewSaga(event) {
18 | try {
19 | const { steps } = event.payload;
20 |
21 | const options = {
22 | method: "POST",
23 | headers: {
24 | Authorization: `Bearer ${auth.getToken()}`,
25 | "Content-Type": "application/json"
26 | },
27 | body: JSON.stringify({ steps })
28 | };
29 |
30 | const request = () =>
31 | fetch(`${strapi.backendURL}/image-formats/preview`, options);
32 |
33 | const response = yield call(request);
34 |
35 | const buffer = yield call(() => response.arrayBuffer());
36 |
37 | const imageData = _arrayBufferToBase64(buffer);
38 |
39 | const imageDataUri = `data:image/jpeg;charset=utf-8;base64,${imageData}`;
40 |
41 | yield put(fetchPreviewSuccess(imageDataUri));
42 |
43 | // yield put(fetchPreviewError(saved));
44 | } catch (error) {
45 | console.log({ error });
46 | strapi.notification.error("notification.error");
47 | yield put(fetchPreviewError(error));
48 | }
49 | }
50 |
51 | export function* defaultSaga() {
52 | yield fork(takeLatest, FETCH_PREVIEW, fetchPreviewSaga);
53 | }
54 |
55 | export default defaultSaga;
56 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect";
2 | import pluginId from "pluginId";
3 |
4 | /**
5 | * Direct selector to the examplePage state domain
6 | */
7 | const selectImagePreviewDomain = () => state =>
8 | state.get(`${pluginId}_imageFormatPreview`);
9 |
10 | /**
11 | * Default selector used by HomePage
12 | */
13 |
14 | const makeSelectLoading = () =>
15 | createSelector(
16 | selectImagePreviewDomain(),
17 | substate => substate.get("loading")
18 | );
19 |
20 | const makeSelectImageDataUri = () =>
21 | createSelector(
22 | selectImagePreviewDomain(),
23 | substate => substate.get("imageDataUri")
24 | );
25 |
26 | const makeSelectError = () =>
27 | createSelector(
28 | selectImagePreviewDomain(),
29 | substate => substate.get("error")
30 | );
31 |
32 | export { makeSelectLoading, makeSelectError, makeSelectImageDataUri };
33 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/Preview/styles.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 600;
3 | text-align: "center";
4 | }
5 | .previewimage {
6 | max-height: 250;
7 | max-width: 600;
8 | }
9 |
10 | .dimensions {
11 | font-size: 14;
12 | color: "gray";
13 | font-style: "italic";
14 | }
15 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_IMAGE_FORMAT,
3 | LOAD_IMAGE_FORMAT_ERROR,
4 | LOAD_IMAGE_FORMAT_SUCCESS,
5 | SAVE_IMAGE_FORMAT,
6 | SAVE_IMAGE_FORMAT_ERROR,
7 | SAVE_IMAGE_FORMAT_SUCCESS
8 | } from './constants';
9 |
10 | export const loadImageFormat = imageFormatId => ({
11 | type: LOAD_IMAGE_FORMAT,
12 | payload: { imageFormatId }
13 | });
14 | export const loadImageFormatSuccess = imageFormat => ({
15 | type: LOAD_IMAGE_FORMAT_SUCCESS,
16 | payload: { imageFormat }
17 | });
18 | export const loadImageFormatError = error => ({
19 | type: LOAD_IMAGE_FORMAT_ERROR,
20 | error: true,
21 | payload: error
22 | });
23 |
24 | export const saveImageFormat = imageFormat => ({
25 | type: SAVE_IMAGE_FORMAT,
26 | payload: { imageFormat }
27 | });
28 | export const saveImageFormatSuccess = saved => ({
29 | type: SAVE_IMAGE_FORMAT_SUCCESS,
30 | payload: { saved }
31 | });
32 | export const saveImageFormatError = error => ({
33 | type: SAVE_IMAGE_FORMAT_ERROR,
34 | error: true,
35 | payload: error
36 | });
37 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/constants.js:
--------------------------------------------------------------------------------
1 | import pluginId from 'pluginId';
2 |
3 | const buildString = suffix => `${pluginId}/EditImageFormatPage/${suffix}`;
4 |
5 | export const LOAD_IMAGE_FORMAT = buildString('LOAD_IMAGE_FORMAT');
6 | export const LOAD_IMAGE_FORMAT_SUCCESS = buildString(
7 | 'LOAD_IMAGE_FORMAT_SUCCESS'
8 | );
9 | export const LOAD_IMAGE_FORMAT_ERROR = buildString('LOAD_IMAGE_FORMAT_ERROR');
10 |
11 | export const SAVE_IMAGE_FORMAT = buildString('SAVE_IMAGE_FORMAT');
12 | export const SAVE_IMAGE_FORMAT_SUCCESS = buildString(
13 | 'SAVE_IMAGE_FORMAT_SUCCESS'
14 | );
15 | export const SAVE_IMAGE_FORMAT_ERROR = buildString('SAVE_IMAGE_FORMAT_ERROR');
16 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { createStructuredSelector } from "reselect";
5 | import { injectIntl } from "react-intl";
6 | import { compose } from "redux";
7 | import pluginId from "pluginId";
8 | import uuid from "uuid/v4";
9 |
10 | import Button from "components/Button";
11 | import PluginHeader from "components/PluginHeader";
12 | import StepEditor from "../../components/StepEditor";
13 | import IcoContainer from "components/IcoContainer";
14 | import Preview from "./Preview";
15 | import InputText from "components/InputsIndex";
16 |
17 | import styles from "./styles.scss";
18 | import { loadImageFormat, saveImageFormat } from "./actions";
19 | import {
20 | makeSelectLoading,
21 | makeSelectImageFormat,
22 | makeSelectCreated,
23 | makeSelectSaving
24 | } from "./selectors";
25 | import reducer from "./reducer";
26 | import saga from "./saga";
27 |
28 | export class CreateImageFormatPage extends Component {
29 | state = {
30 | name: null,
31 | description: null,
32 | steps: []
33 | };
34 |
35 | componentDidMount() {
36 | const { imageFormatId } = this.props.match.params;
37 | imageFormatId && this.props.loadImageFormat(imageFormatId);
38 | }
39 |
40 | componentWillReceiveProps(nextProps) {
41 | if (!this.props.imageFormat && nextProps.imageFormat) {
42 | this.setState({
43 | name: nextProps.imageFormat.name,
44 | description: nextProps.imageFormat.description,
45 | steps: nextProps.imageFormat.steps
46 | });
47 | }
48 |
49 | if (!this.props.created && nextProps.created) {
50 | this.props.history.push(`/plugins/${pluginId}`);
51 | }
52 | }
53 |
54 | onChangeName = event => {
55 | const submittedValue = event.target.value;
56 | const lowercase = submittedValue.toLowerCase();
57 | const replaced = lowercase.replace(/[^a-z0-9]/g, "-");
58 | this.setState({ name: replaced.substring(0, 16) });
59 | };
60 | onChangeDescription = event => {
61 | this.setState({ description: event.target.value });
62 | };
63 |
64 | onSaveImageFormat = () => {
65 | const { imageFormatId } = this.props.match.params;
66 | const imageFormat = {
67 | id: imageFormatId,
68 | name: this.state.name,
69 | description: this.state.description,
70 | steps: JSON.stringify(this.state.steps)
71 | };
72 |
73 | this.props.saveImageFormat(imageFormat);
74 | };
75 |
76 | addStep = () => {
77 | this.setState({
78 | steps: [
79 | ...this.state.steps,
80 | { id: uuid(), method: "contain", params: { width: 100, height: 100 } }
81 | ]
82 | });
83 | };
84 |
85 | removeStep = _id => () => {
86 | this.setState({
87 | steps: _.filter(this.state.steps, step => step.id !== _id)
88 | });
89 | };
90 |
91 | stepChanged = updated => {
92 | const newSteps = _.cloneDeep(this.state.steps);
93 |
94 | const index = _.findIndex(newSteps, step => step.id == updated.id);
95 | newSteps[index] = updated;
96 |
97 | this.setState({ steps: newSteps });
98 | };
99 |
100 | render() {
101 | const { loading, saving } = this.props;
102 | const { name, description, steps } = this.state;
103 |
104 | const saveDisabled = loading || saving || steps == [];
105 |
106 | return (
107 |
108 |
112 |
113 |
118 |
119 |
120 |
121 |
127 |
128 |
134 |
135 |
Steps
136 |
137 | {steps.map(step => (
138 |
139 |
140 |
141 | |
142 |
143 |
151 | |
152 |
153 | ))}
154 |
155 |
156 |
161 | |
162 |
163 |
164 |
165 |
166 |
167 |
177 |
178 | );
179 | }
180 | }
181 |
182 | CreateImageFormatPage.contextTypes = {
183 | router: PropTypes.object
184 | };
185 |
186 | CreateImageFormatPage.propTypes = {
187 | loadImageFormat: PropTypes.func.isRequired,
188 | saveImageFormat: PropTypes.func.isRequired,
189 | imageFormat: PropTypes.object.isRequired,
190 | loading: PropTypes.bool.isRequired,
191 | saving: PropTypes.bool.isRequired,
192 | created: PropTypes.object.isRequired
193 | };
194 |
195 | const mapDispatchToProps = {
196 | loadImageFormat,
197 | saveImageFormat
198 | };
199 |
200 | const mapStateToProps = createStructuredSelector({
201 | loading: makeSelectLoading(),
202 | imageFormat: makeSelectImageFormat(),
203 | created: makeSelectCreated(),
204 | saving: makeSelectSaving()
205 | });
206 |
207 | const withConnect = connect(
208 | mapStateToProps,
209 | mapDispatchToProps
210 | );
211 |
212 | const withReducer = strapi.injectReducer({
213 | key: "createImageFormatPage",
214 | reducer,
215 | pluginId
216 | });
217 | const withSaga = strapi.injectSaga({
218 | key: "createImageFormatPage",
219 | saga,
220 | pluginId
221 | });
222 |
223 | export default compose(
224 | withReducer,
225 | withSaga,
226 | withConnect
227 | )(injectIntl(CreateImageFormatPage));
228 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from "immutable";
2 |
3 | import {
4 | LOAD_IMAGE_FORMAT,
5 | LOAD_IMAGE_FORMAT_SUCCESS,
6 | LOAD_IMAGE_FORMAT_ERROR,
7 | SAVE_IMAGE_FORMAT,
8 | SAVE_IMAGE_FORMAT_SUCCESS,
9 | SAVE_IMAGE_FORMAT_ERROR
10 | } from "./constants";
11 |
12 | const initialState = fromJS({
13 | loading: false,
14 | imageFormat: null,
15 | loadError: null,
16 | saving: false,
17 | created: null,
18 | saveError: null
19 | });
20 |
21 | function createImageFormatPageReducer(state = initialState, action) {
22 | const { type, payload } = action;
23 |
24 | switch (type) {
25 | case LOAD_IMAGE_FORMAT:
26 | return state.set("loading", true).set("imageFormat", null);
27 |
28 | case LOAD_IMAGE_FORMAT_SUCCESS: {
29 | const { imageFormat } = payload;
30 |
31 | if (typeof imageFormat.steps === "string") {
32 | imageFormat.steps = JSON.parse(imageFormat.steps);
33 | }
34 |
35 | return state.set("loading", false).set("imageFormat", imageFormat);
36 | }
37 |
38 | case LOAD_IMAGE_FORMAT_ERROR:
39 | return state.set("loading", false).set("loadError", payload);
40 |
41 | case SAVE_IMAGE_FORMAT:
42 | return state.set("saving", true).set("created", null);
43 |
44 | case SAVE_IMAGE_FORMAT_SUCCESS: {
45 | return state.set("saving", false).set("created", payload.saved);
46 | }
47 |
48 | case SAVE_IMAGE_FORMAT_ERROR:
49 | return state.set("loading", false).set("saveError", payload);
50 |
51 | default:
52 | return state;
53 | }
54 | }
55 |
56 | export default createImageFormatPageReducer;
57 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/saga.js:
--------------------------------------------------------------------------------
1 | import { fork, takeLatest, call, put } from 'redux-saga/effects';
2 | import request from 'utils/request';
3 |
4 | import {
5 | loadImageFormatError,
6 | loadImageFormatSuccess,
7 | saveImageFormatError,
8 | saveImageFormatSuccess
9 | } from './actions';
10 | import { LOAD_IMAGE_FORMAT, SAVE_IMAGE_FORMAT } from './constants';
11 |
12 | export function* loadImageFormat(event) {
13 | try {
14 | const { imageFormatId } = event.payload;
15 |
16 | const imageFormat = yield call(request, `/image-formats/${imageFormatId}`, {
17 | method: 'GET'
18 | });
19 |
20 | yield put(loadImageFormatSuccess(imageFormat));
21 | } catch (error) {
22 | strapi.notification.error('notification.error');
23 | yield put(loadImageFormatError(error));
24 | }
25 | }
26 |
27 | export function* saveImageFormat(event) {
28 | try {
29 | const { imageFormat } = event.payload;
30 |
31 | if (imageFormat.id) {
32 | const saved = yield call(request, `/image-formats/${imageFormat.id}`, {
33 | method: 'PUT',
34 | body: imageFormat
35 | });
36 |
37 | yield put(saveImageFormatSuccess(saved));
38 | } else {
39 | const saved = yield call(request, '/image-formats', {
40 | method: 'POST',
41 | body: imageFormat
42 | });
43 |
44 | yield put(saveImageFormatSuccess(saved));
45 | }
46 | } catch (error) {
47 | strapi.notification.error('notification.error');
48 | yield put(saveImageFormatError(error));
49 | }
50 | }
51 |
52 | export function* defaultSaga() {
53 | yield fork(takeLatest, LOAD_IMAGE_FORMAT, loadImageFormat);
54 | yield fork(takeLatest, SAVE_IMAGE_FORMAT, saveImageFormat);
55 | }
56 |
57 | export default defaultSaga;
58 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import pluginId from 'pluginId';
3 |
4 | /**
5 | * Direct selector to the examplePage state domain
6 | */
7 | const selectEditImageFormatPageDomain = () => state =>
8 | state.get(`${pluginId}_createImageFormatPage`);
9 |
10 | /**
11 | * Default selector used by HomePage
12 | */
13 |
14 | const makeSelectLoading = () =>
15 | createSelector(selectEditImageFormatPageDomain(), substate =>
16 | substate.get('loading')
17 | );
18 |
19 | const makeSelectImageFormat = () =>
20 | createSelector(selectEditImageFormatPageDomain(), substate =>
21 | substate.get('imageFormat')
22 | );
23 |
24 | const makeSelectCreated = () =>
25 | createSelector(selectEditImageFormatPageDomain(), substate =>
26 | substate.get('created')
27 | );
28 |
29 | const makeSelectSaving = () =>
30 | createSelector(selectEditImageFormatPageDomain(), substate =>
31 | substate.get('saving')
32 | );
33 |
34 | export {
35 | makeSelectLoading,
36 | makeSelectImageFormat,
37 | makeSelectCreated,
38 | makeSelectSaving
39 | };
40 |
--------------------------------------------------------------------------------
/admin/src/containers/EditImageFormatPage/styles.scss:
--------------------------------------------------------------------------------
1 | .editImageFormatPage {
2 | padding: 1.7rem 3rem;
3 | background: rgba(14, 22, 34, 0.02);
4 | min-height: calc(100vh - 6rem);
5 | }
6 |
7 | .stepRow {
8 | border-top: 1px solid #999;
9 | margin-bottom: 5;
10 | }
11 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_IMAGE_FORMATS,
3 | LOAD_IMAGE_FORMATS_ERROR,
4 | LOAD_IMAGE_FORMATS_SUCCESS,
5 | UNDO_IMPORT,
6 | UNDO_IMPORT_SUCCESS,
7 | UNDO_IMPORT_ERROR,
8 | DELETE_IMPORT,
9 | DELETE_IMPORT_SUCCESS,
10 | DELETE_IMPORT_ERROR
11 | } from './constants';
12 |
13 | export const loadImageFormats = () => ({
14 | type: LOAD_IMAGE_FORMATS
15 | });
16 | export const loadImageFormatsSuccess = imageFormats => ({
17 | type: LOAD_IMAGE_FORMATS_SUCCESS,
18 | payload: { imageFormats }
19 | });
20 | export const loadImageFormatsError = error => ({
21 | type: LOAD_IMAGE_FORMATS_ERROR,
22 | payload: error,
23 | error: true
24 | });
25 |
26 | export const undoImport = id => ({
27 | type: UNDO_IMPORT,
28 | payload: { id }
29 | });
30 | export const undoImportSuccess = () => ({
31 | type: UNDO_IMPORT_SUCCESS
32 | });
33 | export const undoImportError = error => ({
34 | type: UNDO_IMPORT_ERROR,
35 | payload: error,
36 | error: true
37 | });
38 |
39 | export const deleteImageFormat = id => ({
40 | type: DELETE_IMPORT,
41 | payload: { id }
42 | });
43 | export const deleteImageFormatSuccess = () => ({
44 | type: DELETE_IMPORT_SUCCESS
45 | });
46 | export const deleteImageFormatError = error => ({
47 | type: DELETE_IMPORT_ERROR,
48 | payload: error,
49 | error: true
50 | });
51 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/constants.js:
--------------------------------------------------------------------------------
1 | import pluginId from "pluginId";
2 |
3 | const buildString = suffix => `${pluginId}/HomePage/${suffix}`;
4 |
5 | export const LOAD_IMAGE_FORMATS = buildString("LOAD_IMAGE_FORMATS");
6 | export const LOAD_IMAGE_FORMATS_ERROR = buildString(
7 | "LOAD_IMAGE_FORMATS_ERROR"
8 | );
9 | export const LOAD_IMAGE_FORMATS_SUCCESS = buildString(
10 | "LOAD_IMAGE_FORMATS_SUCCESS"
11 | );
12 |
13 | export const UNDO_IMPORT = buildString("UNDO_IMPORT");
14 | export const UNDO_IMPORT_SUCCESS = buildString("UNDO_IMPORT_SUCCESS");
15 | export const UNDO_IMPORT_ERROR = buildString("UNDO_IMPORT_ERROR");
16 |
17 | export const DELETE_IMPORT = buildString("DELETE_IMPORT");
18 | export const DELETE_IMPORT_SUCCESS = buildString("DELETE_IMPORT_SUCCESS");
19 | export const DELETE_IMPORT_ERROR = buildString("DELETE_IMPORT_ERROR");
20 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Button from 'components/Button';
3 | import IcoContainer from 'components/IcoContainer';
4 | import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import { createStructuredSelector } from 'reselect';
7 | import { injectIntl } from 'react-intl';
8 | import { compose } from 'redux';
9 | import pluginId from 'pluginId';
10 | import PluginHeader from 'components/PluginHeader';
11 |
12 | import {
13 | selectImageFormats,
14 | selectImageFormatsError,
15 | selectImageFormatsLoading
16 | } from './selectors';
17 |
18 | import styles from './styles.scss';
19 |
20 | import { loadImageFormats, deleteImageFormat } from './actions';
21 | import reducer from './reducer';
22 | import saga from './saga';
23 |
24 | export class HomePage extends Component {
25 | componentDidMount() {
26 | this.props.loadImageFormats();
27 | }
28 |
29 | navigateToCreateImageFormat = () => {
30 | this.props.history.push(`/plugins/${pluginId}/create`);
31 | };
32 |
33 | navigateToEditImageFormat = imageFormatId => () => {
34 | this.props.history.push(`/plugins/${pluginId}/edit/${imageFormatId}`);
35 | };
36 |
37 | deleteImageFormat = id => () => {
38 | this.props.deleteImageFormat(id);
39 | };
40 |
41 | render() {
42 | const { imageFormats } = this.props;
43 |
44 | return (
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 | Name |
58 | Description |
59 |
60 |
61 |
62 | {imageFormats &&
63 | imageFormats.map(item => {
64 | return (
65 |
66 | {item.name} |
67 | {item.description} |
68 |
69 |
81 | |
82 |
83 | );
84 | })}
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | HomePage.contextTypes = {
93 | router: PropTypes.object
94 | };
95 |
96 | HomePage.propTypes = {
97 | history: PropTypes.object.isRequired,
98 | loadImageFormats: PropTypes.func.isRequired,
99 | imageFormats: PropTypes.array,
100 | deleteImageFormat: PropTypes.func.isRequired
101 | };
102 |
103 | const mapDispatchToProps = {
104 | loadImageFormats,
105 | deleteImageFormat
106 | };
107 |
108 | const mapStateToProps = createStructuredSelector({
109 | imageFormats: selectImageFormats(),
110 | loading: selectImageFormatsLoading(),
111 | error: selectImageFormatsError()
112 | });
113 |
114 | const withConnect = connect(
115 | mapStateToProps,
116 | mapDispatchToProps
117 | );
118 |
119 | const withReducer = strapi.injectReducer({
120 | key: 'homePage',
121 | reducer,
122 | pluginId
123 | });
124 | const withSaga = strapi.injectSaga({ key: 'homePage', saga, pluginId });
125 |
126 | export default compose(
127 | withReducer,
128 | withSaga,
129 | withConnect
130 | )(injectIntl(HomePage));
131 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import {
4 | LOAD_IMAGE_FORMATS,
5 | LOAD_IMAGE_FORMATS_SUCCESS,
6 | LOAD_IMAGE_FORMATS_ERROR
7 | } from './constants';
8 |
9 | const initialState = fromJS({
10 | imageFormats: null,
11 | loading: false,
12 | error: null
13 | });
14 |
15 | function homePageReducer(state = initialState, action) {
16 | const { type, payload } = action;
17 |
18 | switch (type) {
19 | case LOAD_IMAGE_FORMATS:
20 | return state.set('loading', true);
21 |
22 | case LOAD_IMAGE_FORMATS_SUCCESS:
23 | return state
24 | .set('loading', false)
25 | .set('imageFormats', payload.imageFormats);
26 |
27 | case LOAD_IMAGE_FORMATS_ERROR:
28 | return state.set('loading', false).set('error', payload);
29 |
30 | default:
31 | return state;
32 | }
33 | }
34 |
35 | export default homePageReducer;
36 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/saga.js:
--------------------------------------------------------------------------------
1 | import { fork, takeLatest, call, put } from 'redux-saga/effects';
2 | import request from 'utils/request';
3 |
4 | import {
5 | loadImageFormats,
6 | loadImageFormatsSuccess,
7 | loadImageFormatsError,
8 | undoImportError,
9 | undoImportSuccess,
10 | deleteImageFormatError,
11 | deleteImageFormatSuccess
12 | } from './actions';
13 | import { LOAD_IMAGE_FORMATS, DELETE_IMPORT, UNDO_IMPORT } from './constants';
14 |
15 | export function* loadImageFormatsSaga() {
16 | try {
17 | const imageFormats = yield call(request, '/image-formats', {
18 | method: 'GET'
19 | });
20 |
21 | yield put(loadImageFormatsSuccess(imageFormats));
22 | } catch (error) {
23 | strapi.notification.error('notification.error');
24 | yield put(loadImageFormatsError(error));
25 | }
26 | }
27 |
28 | export function* deleteImageFormatSaga(event) {
29 | const { id } = event.payload;
30 |
31 | try {
32 | const imageFormats = yield call(request, `/image-formats/${id}`, {
33 | method: 'DELETE'
34 | });
35 |
36 | yield put(deleteImageFormatSuccess(imageFormats));
37 | yield put(loadImageFormats());
38 | } catch (error) {
39 | strapi.notification.error('notification.error');
40 | yield put(deleteImageFormatError(error));
41 | }
42 | }
43 |
44 | export function* undoImportSaga(event) {
45 | const { id } = event.payload;
46 |
47 | try {
48 | const imageFormats = yield call(request, `/image-formats/${id}/undo`, {
49 | method: 'POST'
50 | });
51 |
52 | yield put(undoImportSuccess(imageFormats));
53 | yield put(loadImageFormats());
54 | } catch (error) {
55 | strapi.notification.error('notification.error');
56 | yield put(undoImportError(error));
57 | }
58 | }
59 |
60 | export function* defaultSaga() {
61 | yield fork(takeLatest, LOAD_IMAGE_FORMATS, loadImageFormatsSaga);
62 | yield fork(takeLatest, UNDO_IMPORT, undoImportSaga);
63 | yield fork(takeLatest, DELETE_IMPORT, deleteImageFormatSaga);
64 | }
65 |
66 | export default defaultSaga;
67 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import pluginId from 'pluginId';
3 | /**
4 | * Direct selector to the homePage state domain
5 | */
6 | const selectHomePageDomain = () => state => state.get(`${pluginId}_homePage`);
7 |
8 | /**
9 | * Default selector used by HomePage
10 | */
11 |
12 | export const selectImageFormats = () =>
13 | createSelector(selectHomePageDomain(), substate =>
14 | substate.get('imageFormats')
15 | );
16 |
17 | export const selectImageFormatsError = () =>
18 | createSelector(selectHomePageDomain(), substate => substate.get('error'));
19 |
20 | export const selectImageFormatsLoading = () =>
21 | createSelector(selectHomePageDomain(), substate => substate.get('loading'));
22 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/styles.scss:
--------------------------------------------------------------------------------
1 | .homePage {
2 | padding: 1.7rem 3rem;
3 | background: rgba(14, 22, 34, 0.02);
4 | min-height: calc(100vh - 6rem);
5 | }
6 |
7 | th,
8 | td {
9 | padding-right: 15px;
10 | }
11 |
--------------------------------------------------------------------------------
/admin/src/containers/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a neccessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 |
14 | import NotFound from 'components/NotFound';
15 |
16 | export default class NotFoundPage extends React.Component {
17 | render() {
18 | return ;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/blur.js:
--------------------------------------------------------------------------------
1 | const FIELD_CONFIGS = {
2 | radius: {
3 | type: 'integer',
4 | min: 1,
5 | max: 4096,
6 | required: true,
7 | default: 4
8 | }
9 | };
10 |
11 | module.exports = FIELD_CONFIGS;
12 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/contain.js:
--------------------------------------------------------------------------------
1 | const Jimp = require('jimp');
2 |
3 | const RESIZE_MODES = [
4 | Jimp.HORIZONTAL_ALIGN_LEFT,
5 | Jimp.HORIZONTAL_ALIGN_CENTER,
6 | Jimp.HORIZONTAL_ALIGN_RIGHT,
7 |
8 | Jimp.VERTICAL_ALIGN_TOP,
9 | Jimp.VERTICAL_ALIGN_MIDDLE,
10 | Jimp.VERTICAL_ALIGN_BOTTOM
11 | ];
12 |
13 | const FIELD_CONFIGS = {
14 | width: {
15 | type: 'integer',
16 | min: 1,
17 | max: 4096,
18 | required: true,
19 | default: 150
20 | },
21 | height: {
22 | type: 'integer',
23 | min: 1,
24 | max: 4096,
25 | required: true,
26 | default: 150
27 | }
28 | // mode: {
29 | // type: 'select',
30 | // options: RESIZE_MODES
31 | // }
32 | };
33 |
34 | module.exports = FIELD_CONFIGS;
35 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/crop.js:
--------------------------------------------------------------------------------
1 | const FIELD_CONFIGS = {
2 | x: {
3 | type: 'integer',
4 | min: 1,
5 | max: 4096,
6 | required: true,
7 | default: 1
8 | },
9 | y: {
10 | type: 'integer',
11 | min: 1,
12 | max: 4096,
13 | required: true,
14 | default: 1
15 | },
16 | width: {
17 | type: 'integer',
18 | min: 1,
19 | max: 4096,
20 | required: true,
21 | default: 100
22 | },
23 | height: {
24 | type: 'integer',
25 | min: 1,
26 | max: 4096,
27 | required: true,
28 | default: 100
29 | }
30 | };
31 |
32 | module.exports = FIELD_CONFIGS;
33 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/index.js:
--------------------------------------------------------------------------------
1 | const contain = require('./contain');
2 | const resize = require('./resize');
3 | const crop = require('./crop');
4 | const blur = require('./blur');
5 | const pixelate = require('./pixelate');
6 |
7 | module.exports = {
8 | contain,
9 | cover: contain,
10 | resize,
11 | scaleToFit: contain,
12 | crop,
13 | invert: {},
14 | greyscale: {},
15 | sepia: {},
16 | normalize: {},
17 | dither565: {},
18 | blur,
19 | gaussian: blur,
20 | pixelate
21 | };
22 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/pixelate.js:
--------------------------------------------------------------------------------
1 | const FIELD_CONFIGS = {
2 | size: {
3 | type: 'integer',
4 | min: 1,
5 | max: 4096,
6 | required: true,
7 | default: 4
8 | }
9 | };
10 |
11 | module.exports = FIELD_CONFIGS;
12 |
--------------------------------------------------------------------------------
/admin/src/jimpMethodConfigs/resize.js:
--------------------------------------------------------------------------------
1 | const Jimp = require('jimp');
2 |
3 | const RESIZE_MODES = [
4 | Jimp.RESIZE_NEAREST_NEIGHBOR,
5 | Jimp.RESIZE_BILINEAR,
6 | Jimp.RESIZE_BICUBIC,
7 | Jimp.RESIZE_HERMITE,
8 | Jimp.RESIZE_BEZIER
9 | ];
10 |
11 | const FIELD_CONFIGS = {
12 | width: {
13 | type: 'integer',
14 | min: 1,
15 | max: 4096,
16 | required: true,
17 | default: 150
18 | },
19 | height: {
20 | type: 'integer',
21 | min: 1,
22 | max: 4096,
23 | required: true,
24 | default: 150
25 | },
26 | mode: {
27 | type: 'select',
28 | options: RESIZE_MODES
29 | }
30 | };
31 |
32 | module.exports = FIELD_CONFIGS;
33 |
--------------------------------------------------------------------------------
/admin/src/pluginId.js:
--------------------------------------------------------------------------------
1 | const pluginPkg = require('../../package.json');
2 | const pluginId = pluginPkg.name.replace(
3 | /^strapi-plugin-/i,
4 | ''
5 | );
6 |
7 | module.exports = pluginId;
8 |
--------------------------------------------------------------------------------
/admin/src/translations/ar.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/de.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/es.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/fr.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/it.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ko.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/nl.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/pl.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/pt-BR.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/pt.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ru.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-image-formats/982ee8bb9ed050de6f9e9d207087d090713e701c/admin/src/translations/ru.json
--------------------------------------------------------------------------------
/admin/src/translations/tr.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/zh-Hans.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-image-formats/982ee8bb9ed050de6f9e9d207087d090713e701c/admin/src/translations/zh-Hans.json
--------------------------------------------------------------------------------
/admin/src/translations/zh.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Node.js with React
2 | # Build a Node.js project that uses React.
3 | # Add steps that analyze code, save build artifacts, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'Ubuntu-16.04'
11 |
12 | steps:
13 | - task: NodeTool@0
14 | inputs:
15 | versionSpec: '10.x'
16 | displayName: 'Install Node.js'
17 |
18 | - script: |
19 | npm install
20 | npm run test
21 | displayName: 'npm install and test'
22 |
--------------------------------------------------------------------------------
/config/functions/bootstrap.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const findAuthenticatedRole = async () => {
4 | const result = await strapi
5 | .query("role", "users-permissions")
6 | .findOne({ type: "authenticated" });
7 |
8 | return result;
9 | };
10 |
11 | const setDefaultPermissions = async () => {
12 | const role = await findAuthenticatedRole();
13 |
14 | const permissions = await strapi
15 | .query("permission", "users-permissions")
16 | .find({ type: "image-formats", role: role.id });
17 |
18 | await Promise.all(
19 | permissions.map(p =>
20 | strapi
21 | .query("permission", "users-permissions")
22 | .update({ id: p.id }, { enabled: true })
23 | )
24 | );
25 | };
26 |
27 | const isFirstRun = async () => {
28 | const pluginStore = strapi.store({
29 | environment: strapi.config.environment,
30 | type: "plugin",
31 | name: "image-formats"
32 | });
33 |
34 | const initHasRun = await pluginStore.get({ key: "initHasRun" });
35 |
36 | await pluginStore.set({ key: "initHasRun", value: true });
37 |
38 | return !initHasRun;
39 | };
40 |
41 | module.exports = async callback => {
42 | const shouldSetDefaultPermissions = await isFirstRun();
43 |
44 | if (shouldSetDefaultPermissions) {
45 | await setDefaultPermissions();
46 | }
47 |
48 | callback();
49 | };
50 |
--------------------------------------------------------------------------------
/config/queries/bookshelf.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const { convertRestQueryParams, buildQuery } = require('strapi-utils');
3 |
4 | module.exports = {
5 | find: async function(params, populate) {
6 | const model = this;
7 |
8 | const filters = convertRestQueryParams(params);
9 |
10 | return this.query(buildQuery({ model, filters }))
11 | .fetchAll({
12 | withRelated: populate || this.associations.map(x => x.alias)
13 | })
14 | .then(data => data.toJSON());
15 | },
16 |
17 | count: async function(params = {}) {
18 | const model = this;
19 |
20 | const { where } = convertRestQueryParams(params);
21 |
22 | return this.query(buildQuery({ model, filters: { where } })).count();
23 | },
24 |
25 | findOne: async function(params, populate) {
26 | const primaryKey = params[this.primaryKey] || params.id;
27 |
28 | if (primaryKey) {
29 | params = {
30 | [this.primaryKey]: primaryKey
31 | };
32 | }
33 |
34 | const record = await this.forge(params).fetch({
35 | withRelated: populate || this.associations.map(x => x.alias)
36 | });
37 |
38 | return record ? record.toJSON() : record;
39 | },
40 |
41 | create: async function(params) {
42 | return this.forge()
43 | .save(
44 | Object.keys(params).reduce((acc, current) => {
45 | if (
46 | _.get(this._attributes, [current, 'type']) ||
47 | _.get(this._attributes, [current, 'model'])
48 | ) {
49 | acc[current] = params[current];
50 | }
51 |
52 | return acc;
53 | }, {})
54 | )
55 | .catch(err => {
56 | if (err.detail) {
57 | const field = _.last(_.words(err.detail.split('=')[0]));
58 | err = { message: `This ${field} is already taken`, field };
59 | }
60 |
61 | throw err;
62 | });
63 | },
64 |
65 | update: async function(search, params = {}) {
66 | if (_.isEmpty(params)) {
67 | params = search;
68 | }
69 |
70 | const primaryKey = search[this.primaryKey] || search.id;
71 |
72 | if (primaryKey) {
73 | search = {
74 | [this.primaryKey]: primaryKey
75 | };
76 | } else {
77 | const entry = await module.exports.findOne.call(this, search);
78 |
79 | search = {
80 | [this.primaryKey]: entry[this.primaryKey] || entry.id
81 | };
82 | }
83 |
84 | return this.forge(search)
85 | .save(params, {
86 | patch: true
87 | })
88 | .catch(err => {
89 | console.log('SQLite does not parse this error correctly', { err });
90 | const field = _.last(_.words(err.detail.split('=')[0]));
91 | const error = { message: `This ${field} is already taken`, field };
92 |
93 | throw error;
94 | });
95 | },
96 |
97 | delete: async function(params) {
98 | return await this.forge({
99 | [this.primaryKey]: params[this.primaryKey] || params.id
100 | }).destroy();
101 | },
102 |
103 | search: async function(params) {
104 | return this.query(function(qb) {
105 | qb.whereRaw('LOWER(hash) LIKE ?', [`%${params.id}%`]).orWhereRaw(
106 | 'LOWER(name) LIKE ?',
107 | [`%${params.id}%`]
108 | );
109 | }).fetchAll();
110 | },
111 |
112 | addPermission: async function(params) {
113 | return this.forge(params).save();
114 | },
115 |
116 | removePermission: async function(params) {
117 | return this.forge({
118 | [this.primaryKey]: params[this.primaryKey] || params.id
119 | }).destroy();
120 | }
121 | };
122 |
--------------------------------------------------------------------------------
/config/queries/mongoose.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const { convertRestQueryParams, buildQuery } = require('strapi-utils');
3 |
4 | module.exports = {
5 | find: async function(params, populate) {
6 | const model = this;
7 | const filters = convertRestQueryParams(params);
8 |
9 | return buildQuery({
10 | model,
11 | filters,
12 | populate: populate || model.associations.map(x => x.alias),
13 | }).lean();
14 | },
15 |
16 | count: async function(params) {
17 | const model = this;
18 |
19 | const filters = convertRestQueryParams(params);
20 |
21 | return buildQuery({
22 | model,
23 | filters: { where: filters.where },
24 | }).count();
25 | },
26 |
27 | findOne: async function(params, populate) {
28 | const primaryKey = params[this.primaryKey] || params.id;
29 |
30 | if (primaryKey) {
31 | params = {
32 | [this.primaryKey]: primaryKey,
33 | };
34 | }
35 |
36 | return this.findOne(params)
37 | .populate(populate || this.associations.map(x => x.alias).join(' '))
38 | .lean();
39 | },
40 |
41 | create: async function(params) {
42 | // Exclude relationships.
43 | const values = Object.keys(params).reduce((acc, current) => {
44 | if (
45 | _.get(this._attributes, [current, 'type']) ||
46 | _.get(this._attributes, [current, 'model'])
47 | ) {
48 | acc[current] = params[current];
49 | }
50 |
51 | return acc;
52 | }, {});
53 |
54 | return this.create(values).catch(err => {
55 | if (err.message.indexOf('index:') !== -1) {
56 | const message = err.message.split('index:');
57 | const field = _.words(_.last(message).split('_')[0]);
58 | const error = { message: `This ${field} is already taken`, field };
59 |
60 | throw error;
61 | }
62 |
63 | throw err;
64 | });
65 | },
66 |
67 | update: async function(search, params = {}) {
68 | if (_.isEmpty(params)) {
69 | params = search;
70 | }
71 |
72 | const primaryKey = search[this.primaryKey] || search.id;
73 |
74 | if (primaryKey) {
75 | search = {
76 | [this.primaryKey]: primaryKey,
77 | };
78 | }
79 |
80 | return this.updateOne(search, params, {
81 | strict: false,
82 | }).catch(error => {
83 | const field = _.last(_.words(error.message.split('_')[0]));
84 | const err = { message: `This ${field} is already taken`, field };
85 |
86 | throw err;
87 | });
88 | },
89 |
90 | delete: async function(params) {
91 | // Delete entry.
92 | return this.remove({
93 | [this.primaryKey]: params[this.primaryKey] || params.id,
94 | });
95 | },
96 |
97 | search: async function(params) {
98 | const re = new RegExp(params.id, 'i');
99 |
100 | return this.find({
101 | $or: [{ hash: re }, { name: re }],
102 | });
103 | },
104 |
105 | addPermission: async function(params) {
106 | return this.create(params);
107 | },
108 |
109 | removePermission: async function(params) {
110 | return this.remove(params);
111 | },
112 | };
113 |
--------------------------------------------------------------------------------
/config/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "method": "POST",
5 | "path": "/preview",
6 | "handler": "ImageFormats.preview",
7 | "config": {
8 | "policies": []
9 | }
10 | },
11 | {
12 | "method": "POST",
13 | "path": "/",
14 | "handler": "ImageFormats.create",
15 | "config": {
16 | "policies": []
17 | }
18 | },
19 | {
20 | "method": "GET",
21 | "path": "/",
22 | "handler": "ImageFormats.index",
23 | "config": {
24 | "policies": []
25 | }
26 | },
27 | {
28 | "method": "GET",
29 | "path": "/:imageFormatId",
30 | "handler": "ImageFormats.fetch",
31 | "config": {
32 | "policies": []
33 | }
34 | },
35 | {
36 | "method": "PUT",
37 | "path": "/:imageFormatId",
38 | "handler": "ImageFormats.update",
39 | "config": {
40 | "policies": []
41 | }
42 | },
43 | {
44 | "method": "DELETE",
45 | "path": "/:imageFormatId",
46 | "handler": "ImageFormats.delete",
47 | "config": {
48 | "policies": []
49 | }
50 | },
51 | {
52 | "method": "GET",
53 | "path": "/:imageFormatName/:fileId",
54 | "handler": "ImageFormats.getFormattedImage",
55 | "config": {
56 | "policies": []
57 | }
58 | }
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/controllers/ImageFormats.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * ImageFormats.js controller
5 | *
6 | * @description: A set of functions called "actions" of the `image-formats` plugin.
7 | */
8 |
9 | module.exports = {
10 | index: async ctx => {
11 | const entries = await strapi.query("imageformat", "image-formats").find();
12 |
13 | ctx.send(entries);
14 | },
15 |
16 | preview: async ctx => {
17 | const imageFormat = ctx.request.body;
18 |
19 | const { mime, buffer } = await strapi.plugins["image-formats"].services[
20 | "imageformats"
21 | ].preview({ imageFormat });
22 |
23 | ctx.set("Content-Type", mime);
24 | ctx.send(buffer);
25 | },
26 |
27 | create: async ctx => {
28 | const imageFormat = ctx.request.body;
29 | console.log("create", imageFormat);
30 |
31 | const entry = await strapi
32 | .query("imageformat", "image-formats")
33 | .create(imageFormat);
34 |
35 | ctx.send(entry);
36 | },
37 |
38 | fetch: async ctx => {
39 | const imageFormatId = ctx.params.imageFormatId;
40 |
41 | const model = await strapi.query("imageformat", "image-formats").findOne({
42 | id: imageFormatId
43 | });
44 |
45 | ctx.send(model);
46 | },
47 |
48 | update: async ctx => {
49 | const imageFormatId = ctx.params.imageFormatId;
50 | const updatedModel = ctx.request.body;
51 | console.log("create", updatedModel);
52 |
53 | const model = await strapi.query("imageformat", "image-formats").update(
54 | {
55 | id: imageFormatId
56 | },
57 | updatedModel
58 | );
59 |
60 | ctx.send(model);
61 | },
62 |
63 | delete: async ctx => {
64 | const imageFormatId = ctx.params.imageFormatId;
65 |
66 | await strapi.query("imageformat", "image-formats").delete({
67 | id: imageFormatId
68 | });
69 |
70 | ctx.send({ message: "ok" });
71 | },
72 |
73 | /**
74 | * Default action.
75 | *
76 | * @return {Object}
77 | */
78 | getFormattedImage: async ctx => {
79 | const { imageFormatName, fileId } = ctx.params;
80 |
81 | const { mime, buffer } = await strapi.plugins["image-formats"].services[
82 | "imageformats"
83 | ].getFormattedImage({ imageFormatName, fileId });
84 |
85 | ctx.set("Content-Type", mime);
86 | ctx.send(buffer);
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/models/FormattedImage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | // Before saving a value.
5 | // Fired before an `insert` or `update` query.
6 | // beforeSave: async (model) => {},
7 | // After saving a value.
8 | // Fired after an `insert` or `update` query.
9 | // afterSave: async (model, result) => {},
10 | // Before fetching all values.
11 | // Fired before a `fetchAll` operation.
12 | // beforeFetchAll: async (model) => {},
13 | // After fetching all values.
14 | // Fired after a `fetchAll` operation.
15 | // afterFetchAll: async (model, results) => {},
16 | // Fired before a `fetch` operation.
17 | // beforeFetch: async (model) => {},
18 | // After fetching a value.
19 | // Fired after a `fetch` operation.
20 | // afterFetch: async (model, result) => {},
21 | // Before creating a value.
22 | // Fired before `insert` query.
23 | // beforeCreate: async (model) => {},
24 | // After creating a value.
25 | // Fired after `insert` query.
26 | // afterCreate: async (model, result) => {},
27 | // Before updating a value.
28 | // Fired before an `update` query.
29 | // beforeUpdate: async (model) => {},
30 | // After updating a value.
31 | // Fired after an `update` query.
32 | // afterUpdate: async (model, result) => {},
33 | // Before destroying a value.
34 | // Fired before a `delete` query.
35 | // beforeDestroy: async model => {},
36 | // After destroying a value.
37 | // Fired after a `delete` query.
38 | // afterDestroy: async (model, result) => {}
39 | };
40 |
--------------------------------------------------------------------------------
/models/FormattedImage.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "connection": "default",
3 | "info": {
4 | "name": "imageformat",
5 | "description": "Named set of steps for processing an image"
6 | },
7 | "options": {
8 | "timestamps": true
9 | },
10 | "attributes": {
11 | "imageFormatId": {
12 | "type": "integer"
13 | },
14 | "originalFileId": {
15 | "type": "integer"
16 | },
17 | "file": {
18 | "collection": "file",
19 | "via": "related",
20 | "plugin": "upload"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/models/ImageFormat.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | // Before saving a value.
5 | // Fired before an `insert` or `update` query.
6 | // beforeSave: async (model) => {},
7 | // After saving a value.
8 | // Fired after an `insert` or `update` query.
9 | // afterSave: async (model, result) => {},
10 | // Before fetching all values.
11 | // Fired before a `fetchAll` operation.
12 | // beforeFetchAll: async (model) => {},
13 | // After fetching all values.
14 | // Fired after a `fetchAll` operation.
15 | // afterFetchAll: async (model, results) => {},
16 | // Fired before a `fetch` operation.
17 | // beforeFetch: async (model) => {},
18 | // After fetching a value.
19 | // Fired after a `fetch` operation.
20 | // afterFetch: async (model, result) => {},
21 | // Before creating a value.
22 | // Fired before `insert` query.
23 | // beforeCreate: async (model) => {},
24 | // After creating a value.
25 | // Fired after `insert` query.
26 | // afterCreate: async (model, result) => {},
27 | // Before updating a value.
28 | // Fired before an `update` query.
29 | // beforeUpdate: async (model) => {},
30 | // After updating a value.
31 | // Fired after an `update` query.
32 | afterUpdate: async (model, result) => {
33 | resetFormattedImageCache(model);
34 | },
35 | // Before destroying a value.
36 | // Fired before a `delete` query.
37 | // beforeDestroy: async (model) => {},
38 | // After destroying a value.
39 | // Fired after a `delete` query.
40 | afterDestroy: async (model, result) => {
41 | resetFormattedImageCache(model);
42 | }
43 | };
44 |
45 | const resetFormattedImageCache = async imageFormat => {
46 | const formattedImages = await strapi
47 | .query('formattedimage', 'image-formats')
48 | .find({ imageFormatId: imageFormat.id });
49 |
50 | formattedImages.forEach(async record => {
51 | strapi.query('formattedimage', 'image-formats').delete({
52 | id: record.id
53 | });
54 |
55 | const fileid = record.file[0].id;
56 |
57 | const uploadProviderConfig = await strapi
58 | .store({
59 | environment: strapi.config.environment,
60 | type: 'plugin',
61 | name: 'upload'
62 | })
63 | .get({ key: 'provider' });
64 |
65 | strapi.plugins['upload'].services['upload'].remove(
66 | { id: fileid },
67 | uploadProviderConfig
68 | );
69 | });
70 | };
71 |
--------------------------------------------------------------------------------
/models/ImageFormat.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "connection": "default",
3 | "info": {
4 | "name": "imageformat",
5 | "description": "Named set of steps for processing an image"
6 | },
7 | "options": {
8 | "timestamps": true
9 | },
10 | "attributes": {
11 | "name": {
12 | "type": "string"
13 | },
14 | "description": {
15 | "type": "text"
16 | },
17 | "steps": {
18 | "type": "json"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "strapi-plugin-image-formats",
3 | "version": "0.3.0",
4 | "description": "Process image uploads.",
5 | "strapi": {
6 | "name": "Image Formats",
7 | "icon": "crop",
8 | "description": "Process image uploads."
9 | },
10 | "scripts": {
11 | "analyze:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf stats.json",
12 | "preanalyze": "npm run analyze:clean",
13 | "analyze": "node ./node_modules/strapi-helper-plugin/lib/internals/scripts/analyze.js",
14 | "prebuild": "npm run build:clean",
15 | "build:dev": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
16 | "build": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=production node node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
17 | "build:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf admin/build",
18 | "start": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/lib/server",
19 | "generate": "node ./node_modules/strapi-helper-plugin/node_modules/plop --plopfile ./node_modules/strapi-helper-plugin/lib/internals/generators/index.js",
20 | "lint": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/eslint --ignore-path .gitignore --ignore-pattern '/admin/build/' --config ./node_modules/strapi-helper-plugin/lib/internals/eslint/.eslintrc.json admin",
21 | "prettier": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/prettier --single-quote --trailing-comma es5 --write \"{admin,__{tests,mocks}__}/**/*.js\"",
22 | "test": "jest",
23 | "test:watch": "jest --watch",
24 | "prepublishOnly": "npm run build"
25 | },
26 | "dependencies": {
27 | "jest": "^24.7.1",
28 | "jimp": "^0.6.4",
29 | "joi": "^14.3.1",
30 | "lodash": "^4.17.11",
31 | "request": "^2.88.0"
32 | },
33 | "devDependencies": {
34 | "strapi-helper-plugin": "3.0.0-alpha.26"
35 | },
36 | "author": {
37 | "name": "Joseph Beuckman",
38 | "email": "joe@beigerecords.com",
39 | "url": "https://github.com/jbeuckm"
40 | },
41 | "maintainers": [
42 | {
43 | "name": "Joseph Beuckman",
44 | "email": "joe@beigerecords.com",
45 | "url": "https://github.com/jbeuckm"
46 | }
47 | ],
48 | "engines": {
49 | "node": ">= 10.0.0",
50 | "npm": ">= 6.0.0"
51 | },
52 | "license": "MIT"
53 | }
54 |
--------------------------------------------------------------------------------
/services/ImageFormats.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const getUploadProvider = require("./utils/upload/getUploadProvider");
3 | const getFileDescriptor = require("./utils/upload/getFileDescriptor");
4 | const relateFileToContent = require("./utils/upload/relateFileToContent");
5 |
6 | const Jimp = require("jimp");
7 | const jimpMethods = require("./jimpMethods");
8 | const fs = require("fs");
9 |
10 | /**
11 | * ImageFormats.js service
12 | *
13 | * @description: A set of functions similar to controller's actions to avoid code duplication.
14 | */
15 |
16 | module.exports = {
17 | preview: async ({ imageFormat }) => {
18 | const image = await Jimp.read(`${__dirname}/sample_photo.jpg`);
19 |
20 | imageFormat.steps.forEach(({ method, params }) => {
21 | const methodFunction = image[method];
22 | const args = jimpMethods[method].getArgumentsArray(params);
23 | methodFunction.apply(image, args);
24 | });
25 |
26 | const buffer = await image.getBufferAsync(Jimp.AUTO);
27 | return { mime: image.getMIME(), buffer };
28 | },
29 |
30 | retrieveCachedFormattedImage: async ({ imageFormat, fileId }) => {
31 | const record = await strapi
32 | .query("formattedimage", "image-formats")
33 | .findOne({ imageFormatId: imageFormat.id, originalFileId: fileId });
34 |
35 | return record && record.file[0];
36 | },
37 |
38 | getFormattedImage: async ({ imageFormatName, fileId }) => {
39 | const [imageFormat, uploadProvider] = await Promise.all([
40 | strapi
41 | .query("imageformat", "image-formats")
42 | .findOne({ name: imageFormatName }),
43 | getUploadProvider()
44 | ]);
45 |
46 | const cachedImage = await strapi.plugins["image-formats"].services[
47 | "imageformats"
48 | ].retrieveCachedFormattedImage({ imageFormat, fileId });
49 |
50 | if (cachedImage) {
51 | const url = uploadProvider.getPath(cachedImage);
52 | const buffer = fs.readFileSync(url);
53 | return { mime: cachedImage.mime, buffer };
54 | }
55 |
56 | const file = await strapi.plugins["upload"].services["upload"].fetch({
57 | id: fileId
58 | });
59 |
60 | const url = uploadProvider.getPath(file);
61 | const image = await Jimp.read(url);
62 |
63 | const steps =
64 | typeof imageFormat.steps === "string"
65 | ? JSON.parse(imageFormat.steps)
66 | : imageFormat.steps;
67 |
68 | steps.forEach(({ method, params }) => {
69 | const methodFunction = image[method];
70 | const args = jimpMethods[method].getArgumentsArray(params);
71 | methodFunction.apply(image, args);
72 | });
73 |
74 | const buffer = await image.getBufferAsync(Jimp.AUTO);
75 |
76 | strapi.plugins["image-formats"].services[
77 | "imageformats"
78 | ].cacheFormattedImage({ imageFormat, originalFile: file, buffer });
79 |
80 | return { mime: image.getMIME(), buffer };
81 | },
82 |
83 | cacheFormattedImage: async ({ imageFormat, originalFile, buffer }) => {
84 | const fileDescriptor = getFileDescriptor({
85 | mimeType: originalFile.mime,
86 | extension: originalFile.ext.replace(".", ""),
87 | buffer
88 | });
89 |
90 | const { actions, provider } = await getUploadProvider();
91 |
92 | await actions.upload(fileDescriptor);
93 |
94 | delete fileDescriptor.buffer;
95 |
96 | fileDescriptor.provider = provider.provider;
97 |
98 | const formattedImage = await strapi
99 | .query("formattedimage", "image-formats")
100 | .create({
101 | imageFormatId: imageFormat.id,
102 | originalFileId: originalFile.id
103 | });
104 |
105 | await relateFileToContent({
106 | fileDescriptor,
107 | referringField: "file",
108 | referringContentSource: "image-formats",
109 | referringModel: "formattedimage",
110 | referringId: formattedImage.id
111 | });
112 | }
113 | };
114 |
--------------------------------------------------------------------------------
/services/jimpMethods/JimpMethod.js:
--------------------------------------------------------------------------------
1 | const Joi = require('joi');
2 | const _ = require('lodash');
3 |
4 | class JimpMethod {
5 | constructor(method, fieldConfigs) {
6 | this.method = method;
7 | this.fieldConfigs = fieldConfigs;
8 |
9 | this.schema = this.buildMethodSchema(fieldConfigs);
10 | }
11 |
12 | buildFieldSchema(fieldConfig) {
13 | switch (fieldConfig.type) {
14 | case 'integer': {
15 | let schema = Joi.number().integer();
16 | if (fieldConfig.min) {
17 | schema = schema.min(fieldConfig.min);
18 | }
19 | if (fieldConfig.max) {
20 | schema = schema.max(fieldConfig.max);
21 | }
22 | if (fieldConfig.required) {
23 | schema = schema.required();
24 | }
25 | return schema;
26 | }
27 | case 'select': {
28 | return Joi.any().valid(fieldConfig.options);
29 | }
30 | }
31 | }
32 |
33 | buildMethodSchema(argumentFields) {
34 | const schemaKeys = _.mapValues(argumentFields, this.buildFieldSchema);
35 | return Joi.object()
36 | .keys(schemaKeys)
37 | .unknown(true);
38 | }
39 |
40 | getArgumentsArray(formValues) {
41 | const validated = Joi.validate(formValues, this.schema);
42 |
43 | if (validated.error) throw validated.error;
44 |
45 | const values = Object.keys(this.fieldConfigs).map(
46 | fieldName => validated.value[fieldName]
47 | );
48 |
49 | _.remove(values, _.isNil);
50 |
51 | return values;
52 | }
53 | }
54 |
55 | module.exports = JimpMethod;
56 |
--------------------------------------------------------------------------------
/services/jimpMethods/JimpMethod.spec.js:
--------------------------------------------------------------------------------
1 | const JimpMethod = require('./JimpMethod');
2 | const Jimp = require('jimp');
3 |
4 | const TEST_OPTIONS = ['ONE', 'TWO', 'THREE'];
5 |
6 | const TEST_FIELD_CONFIGS = {
7 | width: {
8 | type: 'integer',
9 | min: 1,
10 | max: 4096,
11 | required: true
12 | },
13 | height: {
14 | type: 'integer',
15 | min: 1,
16 | max: 4096,
17 | required: true
18 | },
19 | mode: {
20 | type: 'select',
21 | options: TEST_OPTIONS
22 | }
23 | };
24 |
25 | describe('JimpMethod', () => {
26 | it('saves the associated method', () => {
27 | const step = new JimpMethod('test', TEST_FIELD_CONFIGS);
28 |
29 | expect(step.method).toEqual('test');
30 | });
31 |
32 | it('preserves order of arguments in the field configs', () => {
33 | const step = new JimpMethod('test', TEST_FIELD_CONFIGS);
34 |
35 | expect(step.getArgumentsArray({ height: 1, width: 2 })).toEqual([2, 1]);
36 | });
37 |
38 | it('throws for non-allowed enum value', () => {
39 | const step = new JimpMethod('test', TEST_FIELD_CONFIGS);
40 |
41 | expect(() =>
42 | step.getArgumentsArray({ mode: '💩', height: 1, width: 1 })
43 | ).toThrow();
44 | });
45 |
46 | it('passes allowed enum value', () => {
47 | const step = new JimpMethod('test', TEST_FIELD_CONFIGS);
48 |
49 | expect(
50 | step.getArgumentsArray({
51 | height: 1,
52 | width: 2,
53 | mode: TEST_OPTIONS[0]
54 | })
55 | ).toEqual([2, 1, TEST_OPTIONS[0]]);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/services/jimpMethods/index.js:
--------------------------------------------------------------------------------
1 | const JimpMethod = require('./JimpMethod');
2 | const fieldConfigs = require('../../admin/src/jimpMethodConfigs');
3 |
4 | module.exports = {
5 | contain: new JimpMethod('contain', fieldConfigs['contain']),
6 | cover: new JimpMethod('cover', fieldConfigs['cover']),
7 | resize: new JimpMethod('resize', fieldConfigs['resize']),
8 | scaleToFit: new JimpMethod('scaleToFit', fieldConfigs['scaleToFit']),
9 | crop: new JimpMethod('crop', fieldConfigs['crop']),
10 | invert: new JimpMethod('invert', {}),
11 | greyscale: new JimpMethod('greyscale', {}),
12 | sepia: new JimpMethod('sepia', {}),
13 | normalize: new JimpMethod('normalize', {}),
14 | dither565: new JimpMethod('dither565', {}),
15 | blur: new JimpMethod('blur', fieldConfigs['blur']),
16 | gaussian: new JimpMethod('gaussian', fieldConfigs['gaussian']),
17 | pixelate: new JimpMethod('pixelate', fieldConfigs['pixelate'])
18 | };
19 |
--------------------------------------------------------------------------------
/services/sample_photo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-image-formats/982ee8bb9ed050de6f9e9d207087d090713e701c/services/sample_photo.jpg
--------------------------------------------------------------------------------
/services/utils/upload/getFileDescriptor.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const uuid = require('uuid/v4');
3 |
4 | function niceHash(buffer) {
5 | return crypto
6 | .createHash('sha256')
7 | .update(buffer)
8 | .digest('base64')
9 | .replace(/=/g, '')
10 | .replace(/\//g, '-')
11 | .replace(/\+/, '_');
12 | }
13 |
14 | const getFileDescriptor = ({ mimeType, extension, buffer }) => {
15 | const fid = uuid();
16 |
17 | return {
18 | buffer,
19 | sha256: niceHash(buffer),
20 | hash: fid.replace(/-/g, ''),
21 |
22 | name: `${fid}.${extension}`,
23 | ext: `.${extension}`,
24 | mime: mimeType,
25 | size: (buffer.length / 1000).toFixed(2)
26 | };
27 | };
28 |
29 | module.exports = getFileDescriptor;
30 |
--------------------------------------------------------------------------------
/services/utils/upload/getUploadProvider.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const path = require('path');
3 |
4 | module.exports = async () => {
5 | const uploadProviderConfig = await strapi
6 | .store({
7 | environment: strapi.config.environment,
8 | type: 'plugin',
9 | name: 'upload'
10 | })
11 | .get({ key: 'provider' });
12 |
13 | // Get upload provider settings to configure the provider to use.
14 | const provider = _.find(strapi.plugins.upload.config.providers, {
15 | provider: uploadProviderConfig.provider
16 | });
17 |
18 | if (!provider) {
19 | throw new Error(
20 | `The provider package isn't installed. Please run \`npm install strapi-provider-upload-${
21 | uploadProviderConfig.provider
22 | }\``
23 | );
24 | }
25 |
26 | const getPath = file =>
27 | uploadProviderConfig.provider === 'local'
28 | ? path.join(strapi.config.appPath, strapi.config.public.path, file.url)
29 | : file.url;
30 |
31 | return {
32 | config: uploadProviderConfig,
33 | provider,
34 | actions: provider.init(uploadProviderConfig),
35 | getPath
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/services/utils/upload/relateFileToContent.js:
--------------------------------------------------------------------------------
1 | const relateFileToContent = async ({
2 | fileDescriptor,
3 | referringField,
4 | referringContentSource,
5 | referringModel,
6 | referringId
7 | }) => {
8 | fileDescriptor.related = [
9 | {
10 | refId: referringId,
11 | ref: referringModel,
12 | source: referringContentSource,
13 | field: referringField
14 | }
15 | ];
16 |
17 | return await strapi.plugins['upload'].services.upload.add(fileDescriptor);
18 | };
19 |
20 | module.exports = relateFileToContent;
21 |
--------------------------------------------------------------------------------
/video_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-image-formats/982ee8bb9ed050de6f9e9d207087d090713e701c/video_thumbnail.png
--------------------------------------------------------------------------------