├── .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 | [![Build Status](https://dev.azure.com/joebeuckman0156/Strapi%20Plugins/_apis/build/status/jbeuckm.strapi-plugin-image-formats?branchName=master)](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 | [![Click for demo video](video_thumbnail.png)](https://youtu.be/tE8nNDoTiuk) 33 | -------------------------------------------------------------------------------- /admin/build/53771798877e88bccc275e15ba634a83.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/build/c6e5a6171e9789587d2e50f79a728506.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 |
114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 | 127 | 128 | 134 | 135 |

Steps

136 | 137 | {steps.map(step => ( 138 | 139 | 142 | 152 | 153 | ))} 154 | 155 | 162 | 163 |
140 | 141 | 143 | 151 |
156 |
164 |
165 |
166 | 167 |
168 |
169 |
176 |
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 |
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 --------------------------------------------------------------------------------