├── .esformatter ├── .gitignore ├── Gruntfile.js ├── README.md ├── assets └── js │ └── image-edit.js ├── package.json ├── php ├── class-abstract-plugin.php ├── class-image-editor-gd.php ├── class-image-editor-imagick.php ├── class-image-pixel-gd.php └── class-plugin.php ├── readme.txt ├── screenshot-1.jpg ├── twotonefx.php └── views └── media-templates.php /.esformatter: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "esformatter-wordpress" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | languages/twotonefx.pot 3 | node_modules 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 3 | module.exports = function( grunt ) { 4 | 'use strict'; 5 | 6 | grunt.loadNpmTasks( 'grunt-wp-i18n' ); 7 | 8 | grunt.initConfig({ 9 | 10 | makepot: { 11 | plugin: { 12 | options: { 13 | mainFile: 'twotonefx.php', 14 | potHeaders: { 15 | poedit: true 16 | }, 17 | type: 'wp-plugin', 18 | updatePoFiles: true, 19 | updateTimestamp: false, 20 | processPot: function( pot ) { 21 | var translation, 22 | excludedMeta = [ 23 | 'Plugin Name of the plugin/theme', 24 | 'Plugin URI of the plugin/theme', 25 | 'Author of the plugin/theme', 26 | 'Author URI of the plugin/theme' 27 | ]; 28 | 29 | for ( translation in pot.translations[''] ) { 30 | if ( 'undefined' !== typeof pot.translations[''][ translation ].comments.extracted ) { 31 | if ( 0 <= excludedMeta.indexOf( pot.translations[''][ translation ].comments.extracted ) ) { 32 | console.log( 'Excluded meta: ' + pot.translations[''][ translation ].comments.extracted ); 33 | delete pot.translations[''][ translation ]; 34 | } 35 | } 36 | } 37 | 38 | return pot; 39 | } 40 | } 41 | } 42 | } 43 | 44 | }); 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twotone FX 2 | 3 | Apply a duotone effect to photos in the WordPress media library. 4 | 5 | __Contributors:__ [Brady Vercher](https://twitter.com/bradyvercher), [Brody Vercher](https://twitter.com/brover), [Luke McDonald](https://twitter.com/thelukemcdonald) 6 | __Requires:__ WordPress 4.3+ 7 | __License:__ [GPL-2.0+](http://www.gnu.org/licenses/gpl-2.0.html) 8 | 9 | Inspired by the duotone effect employed in the [*Twotone*](https://audiotheme.com/view/twotone/?utm_source=github.com&utm_medium=link&utm_content=twotonefx-readme&utm_campaign=plugins) theme, Twotone FX makes it easy to preview and apply a similar filter to any photo in your Media Library. 10 | 11 | ![Image Editor](screenshot-1.jpg) 12 | _Image editor with a section for adding a duotone effect to a photo._ 13 | 14 | ## Installation 15 | 16 | *Twotone FX* is available in the [WordPress plugin directory](http://wordpress.org/plugins/twotonefx/), so it can be installed from your admin panel. 17 | -------------------------------------------------------------------------------- /assets/js/image-edit.js: -------------------------------------------------------------------------------- 1 | /*global _twotonefxAttachment:false */ 2 | 3 | (function( window, $ ) { 4 | 'use strict'; 5 | 6 | var ImageEditGroup, 7 | imagEditFilterHistory = window.imageEdit.filterHistory, 8 | imageEditOpen = window.imageEdit.open; 9 | 10 | // @todo Destroy this when closing the editor. 11 | ImageEditGroup = wp.media.View.extend({ 12 | className: 'imgedit-group', 13 | template: wp.template( 'twotonefx-image-edit-group' ), 14 | 15 | events: { 16 | 'click button': 'handleClick' 17 | }, 18 | 19 | initialize: function( options ) { 20 | this.editor = options.editor; 21 | this.model = options.model; 22 | this.postId = options.postId; 23 | this.editNonce = options.editNonce; 24 | }, 25 | 26 | render: function() { 27 | this.$el.html( this.template( this.model.toJSON() ) ); 28 | this.$start = this.$el.find( 'input[name="start"]' ).wpColorPicker(); 29 | this.$end = this.$el.find( 'input[name="end"]' ).wpColorPicker(); 30 | return this; 31 | }, 32 | 33 | handleClick: function( e ) { 34 | e.preventDefault(); 35 | 36 | this.editor.addStep( 37 | { 38 | twotonefx: { 39 | type: 'twotonefx', 40 | start: this.$start.wpColorPicker( 'color' ), 41 | end: this.$end.wpColorPicker( 'color' ) 42 | } 43 | }, 44 | this.postId, 45 | this.editNonce 46 | ); 47 | } 48 | }); 49 | 50 | /** 51 | * Retrieve a Backbone model for the image being edited. 52 | * 53 | * @param {Number} postId Attachment post ID. 54 | * @return {Backbone.model} 55 | */ 56 | function getCurrentImageModel( postId ) { 57 | var state, 58 | model = new Backbone.Model({ 59 | twotonefxStart: '#000000', 60 | twotonefxEnd: '#ffffff' 61 | }); 62 | 63 | // Used on the Edit Attachemnt screen since media assets 64 | // aren't typically enqueued. 65 | if ( 'undefined' !== typeof _twotonefxAttachment ) { 66 | model = new Backbone.Model({ 67 | id: postId, 68 | twotonefxStart: _twotonefxAttachment.startColor, 69 | twotonefxEnd: _twotonefxAttachment.endColor 70 | }); 71 | 72 | return model; 73 | } 74 | 75 | state = wp.media.frame.state(); 76 | 77 | // Media manager opened on the Manage Media screen in grid mode. 78 | if ( 'edit-attachment' === state.get( 'id' ) ) { 79 | model = state.get( 'model' ); 80 | } 81 | 82 | // Media manager when editing a post. 83 | else if ( 'edit-image' === state.get( 'id' ) ) { 84 | model = state.get( 'image' ); 85 | } 86 | 87 | return model; 88 | } 89 | 90 | /** 91 | * Proxy the image editor open method to set up the Twotone FX editor group. 92 | * 93 | * @param {Number} postId Attachment post Id. 94 | * @param {String} nonce Attachment edit nonce. 95 | * @param {Backbone.View} view Backbone view. 96 | * @return {$.promise} A jQuery promise representing the request to open the editor. 97 | */ 98 | window.imageEdit.open = function( postId, nonce, view ) { 99 | var dfd = imageEditOpen.apply( this, arguments ), 100 | editor = this, 101 | $el = $( '#image-editor-' + postId ); 102 | 103 | dfd.done(function() { 104 | var editGroup = new ImageEditGroup( { 105 | editor: editor, 106 | model: getCurrentImageModel( postId ), 107 | postId: postId, 108 | editNonce: nonce 109 | } ); 110 | 111 | $el.find( '.imgedit-settings' ).append( editGroup.render().$el ); 112 | }); 113 | 114 | return dfd; 115 | }; 116 | 117 | /** 118 | * Replace core method to whitelist the 'twotonefx' operation. 119 | * 120 | * @param {Number} postId Attachment post ID. 121 | * @param {bool} setSize Whether to set the image size. 122 | * @return {String} 123 | */ 124 | window.imageEdit.filterHistory = function( postId, setSize ) { 125 | var pop, n, o, i, 126 | history = $( '#imgedit-history-' + postId ).val(), 127 | op = []; 128 | 129 | if ( '' !== history ) { 130 | history = JSON.parse( history ); 131 | pop = parseInt( $( '#imgedit-undone-' + postId ).val(), 10 ); 132 | 133 | if ( pop > 0 ) { 134 | while ( pop > 0 ) { 135 | history.pop(); 136 | pop--; 137 | } 138 | } 139 | 140 | if ( setSize ) { 141 | if ( ! history.length ) { 142 | this.hold.w = this.hold.ow; 143 | this.hold.h = this.hold.oh; 144 | return ''; 145 | } 146 | 147 | // Restore. 148 | o = history[ history.length - 1 ]; 149 | o = o.c || o.r || o.f || false; 150 | 151 | if ( o ) { 152 | this.hold.w = o.fw; 153 | this.hold.h = o.fh; 154 | } 155 | } 156 | 157 | // Filter the values. 158 | // @todo Any way to make this play nice with other scripts? 159 | for ( n in history ) { 160 | i = history[ n ]; 161 | if ( i.hasOwnProperty( 'c' ) ) { 162 | op[ n ] = { c: { x: i.c.x, y: i.c.y, w: i.c.w, h: i.c.h } }; 163 | } else if ( i.hasOwnProperty( 'r' ) ) { 164 | op[ n ] = { r: i.r.r }; 165 | } else if ( i.hasOwnProperty( 'f' ) ) { 166 | op[ n ] = { f: i.f.f }; 167 | } else if ( i.hasOwnProperty( 'twotonefx' ) ) { 168 | op[ n ] = i.twotonefx; 169 | } 170 | } 171 | 172 | return JSON.stringify( op ); 173 | } 174 | 175 | return ''; 176 | }; 177 | })( window, jQuery ); 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twotonefx", 3 | "version": "1.0.0", 4 | "description": "A WordPress plugin for applying a duotone effect to photos in the media library.", 5 | "main": "Gruntfile.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/audiotheme/twotonefx" 9 | }, 10 | "author": { 11 | "name": "Brady Vercher", 12 | "email": "brady@blazersix.com", 13 | "url": "https://audiotheme.com/" 14 | }, 15 | "license": "GPL-2.0+", 16 | "bugs": { 17 | "url": "https://github.com/audiotheme/twotonefx/issues" 18 | }, 19 | "homepage": "https://github.com/audiotheme/twotonefx", 20 | "devDependencies": { 21 | "grunt": "^0.4.5", 22 | "grunt-wp-i18n": "^0.5.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /php/class-abstract-plugin.php: -------------------------------------------------------------------------------- 1 | directory; 59 | } 60 | 61 | /** 62 | * Set the plugin's directory. 63 | * 64 | * @since 1.0.0 65 | * 66 | * @param string $directory Absolute path to the main plugin directory. 67 | * @return $this 68 | */ 69 | public function set_directory( $directory ) { 70 | $this->directory = rtrim( $directory, '/' ) . '/'; 71 | return $this; 72 | } 73 | 74 | /** 75 | * Retrieve the path to a file in the plugin. 76 | * 77 | * @since 1.0.0 78 | * 79 | * @param string $path Optional. Path relative to the plugin root. 80 | * @return string 81 | */ 82 | public function get_path( $path = '' ) { 83 | return $this->directory . ltrim( $path, '/' ); 84 | } 85 | 86 | /** 87 | * Retrieve the absolute path for the main plugin file. 88 | * 89 | * @since 1.0.0 90 | * 91 | * @return string 92 | */ 93 | public function get_file() { 94 | return $this->file; 95 | } 96 | 97 | /** 98 | * Set the path to the main plugin file. 99 | * 100 | * @since 1.0.0 101 | * 102 | * @param string $file Absolute path to the main plugin file. 103 | * @return $this 104 | */ 105 | public function set_file( $file ) { 106 | $this->file = $file; 107 | return $this; 108 | } 109 | 110 | /** 111 | * Retrieve the plugin indentifier. 112 | * 113 | * @since 1.0.0 114 | * 115 | * @return string 116 | */ 117 | public function get_slug() { 118 | return $this->directory; 119 | } 120 | 121 | /** 122 | * Set the plugin identifier. 123 | * 124 | * @since 1.0.0 125 | * 126 | * @param string $slug Plugin identifier. 127 | * @return $this 128 | */ 129 | public function set_slug( $slug ) { 130 | $this->slug = $slug; 131 | return $this; 132 | } 133 | 134 | /** 135 | * Retrieve the URL for a file in the plugin. 136 | * 137 | * @since 1.0.0 138 | * 139 | * @param string $path Optional. Path relative to the plugin root. 140 | * @return string 141 | */ 142 | public function get_url( $path = '' ) { 143 | return $this->url . ltrim( $path, '/' ); 144 | } 145 | 146 | /** 147 | * Set the URL for plugin directory root. 148 | * 149 | * @since 1.0.0 150 | * 151 | * @param string $url URL to the root of the plugin directory. 152 | * @return $this 153 | */ 154 | public function set_url( $url ) { 155 | $this->url = rtrim( $url, '/' ) . '/'; 156 | return $this; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /php/class-image-editor-gd.php: -------------------------------------------------------------------------------- 1 | convert_to_grayscale() 54 | ->update_map( $start, $end ) 55 | ->each_pixel( array( $this, 'apply_map' ) ); 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Convert the image to grayscale. 62 | * 63 | * @since 1.0.0 64 | * 65 | * @return $this 66 | */ 67 | public function convert_to_grayscale() { 68 | imagefilter( $this->image, IMG_FILTER_GRAYSCALE ); 69 | return $this; 70 | } 71 | 72 | /** 73 | * Apply a callback method to each pixel in the image. 74 | * 75 | * @since 1.0.0 76 | * 77 | * @param callable $callback A callback function. 78 | * @return $this 79 | */ 80 | public function each_pixel( $callback ) { 81 | $size = $this->get_size(); 82 | 83 | for ( $x = 0; $x < $size['width']; $x++ ) { 84 | for ( $y = 0; $y < $size['height']; $y++ ) { 85 | call_user_func( $callback, $this->get_pixel( $x, $y ) ); 86 | } 87 | } 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Retrieve a gradient map. 94 | * 95 | * @since 1.0.0 96 | * 97 | * @param string $start Starting hex color. 98 | * @param string $end Ending hex color. 99 | * @return array 100 | */ 101 | protected function get_map( $start, $end ) { 102 | $map = array(); 103 | $start = $this->hex2rgb( $start ); 104 | $end = $this->hex2rgb( $end ); 105 | 106 | // @link http://stackoverflow.com/a/16503313 107 | for ( $i = 0; $i < 255; $i++ ) { 108 | $ratio = $i / 255; 109 | 110 | $r = $end[0] * $ratio + $start[0] * ( 1 - $ratio ); 111 | $g = $end[1] * $ratio + $start[1] * ( 1 - $ratio ); 112 | $b = $end[2] * $ratio + $start[2] * ( 1 - $ratio ); 113 | 114 | $map[] = array_map( 'floor', array( $r, $g, $b ) ); 115 | } 116 | 117 | return $map; 118 | } 119 | 120 | /** 121 | * Update the current gradient map. 122 | * 123 | * @since 1.0.0 124 | * 125 | * @param string $start Starting hex color. 126 | * @param string $end Ending hex color. 127 | * @return $this 128 | */ 129 | protected function update_map( $start, $end ) { 130 | $this->map = $this->get_map( $start, $end ); 131 | return $this; 132 | } 133 | 134 | /** 135 | * Convert a pixel based on the gradient map. 136 | * 137 | * @since 1.0.0 138 | * 139 | * @param TwontoneFX_Image_Pixel_GD $pixel Pixel object. 140 | */ 141 | protected function apply_map( $pixel ) { 142 | $rgb = $this->map[ $pixel->get_luma() ]; 143 | $pixel->set_rgb( $rgb[0], $rgb[1], $rgb[2] ); 144 | } 145 | 146 | /** 147 | * Retrieve a pixel object. 148 | * 149 | * @since 1.0.0 150 | * 151 | * @param int $x Position on the x-axis. 152 | * @param int $y Position on the y-axis. 153 | * @return TwotoneFX_Image_Pixel_GD 154 | */ 155 | protected function get_pixel( $x, $y ) { 156 | return new TwotoneFX_Image_Pixel_GD( $this->image, $x, $y ); 157 | } 158 | 159 | /** 160 | * Convert HEX to RGB. 161 | * 162 | * @since 1.0.0 163 | * 164 | * @param string $color The original color, in 3 or 6-digit hexadecimal form. 165 | * @return array Array containing RGB (red, green, and blue) values for the given HEX code, empty array otherwise. 166 | */ 167 | protected function hex2rgb( $color ) { 168 | if ( is_array( $color ) ) { 169 | return $color; 170 | } 171 | 172 | $color = trim( $color, '#' ); 173 | if ( strlen( $color ) == 3 ) { 174 | $r = hexdec( substr( $color, 0, 1 ) . substr( $color, 0, 1 ) ); 175 | $g = hexdec( substr( $color, 1, 1 ) . substr( $color, 1, 1 ) ); 176 | $b = hexdec( substr( $color, 2, 1 ) . substr( $color, 2, 1 ) ); 177 | } else if ( strlen( $color ) == 6 ) { 178 | $r = hexdec( substr( $color, 0, 2 ) ); 179 | $g = hexdec( substr( $color, 2, 2 ) ); 180 | $b = hexdec( substr( $color, 4, 2 ) ); 181 | } else { 182 | return array(); 183 | } 184 | 185 | return array( $r, $g, $b ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /php/class-image-editor-imagick.php: -------------------------------------------------------------------------------- 1 | convert_to_grayscale(); 55 | $this->image->transformImageColorspace( imagick::COLORSPACE_RGB ); 56 | 57 | $clut = $this->get_map( $start, $end ); 58 | $this->image->clutImage( $clut ); 59 | unset( $clut ); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Convert the image to grayscale. 66 | * 67 | * @since 1.0.0 68 | * 69 | * @return $this 70 | */ 71 | public function convert_to_grayscale() { 72 | #$this->image->modulateImage( 100, 0, 100 ); 73 | #$this->image->setColorspace( imagick::COLORSPACE_GRAY ); 74 | #$this->image->setImageColorspace( imagick::COLORSPACE_GRAY ); 75 | $this->image->transformImageColorspace( imagick::COLORSPACE_GRAY ); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Retrieve a gradient map. 82 | * 83 | * @since 1.0.0 84 | * 85 | * @link http://phpimagick.com/Imagick/clutImage 86 | * @link http://www.imagemagick.org/discourse-server/viewtopic.php?t=13181 87 | * 88 | * @param string $start Starting hex color. 89 | * @param string $end Ending hex color. 90 | * @return Imagick 91 | */ 92 | protected function get_map( $start, $end ) { 93 | $gradient = sprintf( 'gradient:%s-%s', $start, $end ); 94 | 95 | $clut = new Imagick(); 96 | $clut->newPseudoImage( 1, 256, $gradient ); 97 | 98 | return $clut; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /php/class-image-pixel-gd.php: -------------------------------------------------------------------------------- 1 | image = $image; 53 | $this->x = $x; 54 | $this->y = $y; 55 | } 56 | 57 | /** 58 | * Retrieve the pixel's RGB values. 59 | * 60 | * @since 1.0.0 61 | * 62 | * @return array 63 | */ 64 | public function get_rgba() { 65 | $rgb = imagecolorat( $this->image, $this->x, $this->y ); 66 | return array_values( imagecolorsforindex( $this->image, $rgb ) ); 67 | } 68 | 69 | /** 70 | * Set the pixel's RGB values. 71 | * 72 | * @since 1.0.0 73 | * 74 | * @param int $r Red color index (0 to 255). 75 | * @param int $g Green color index (0 to 255). 76 | * @param int $b Blue color index (0 to 255). 77 | * @return $this 78 | */ 79 | public function set_rgb( $r, $g, $b ) { 80 | $color_id = imagecolorallocate( $this->image, $r, $g, $b ); 81 | imagesetpixel( $this->image, $this->x, $this->y, $color_id ); 82 | return $this; 83 | } 84 | 85 | /** 86 | * Retrieve the pixel's luma. 87 | * 88 | * @since 1.0.0 89 | * 90 | * @link https://en.wikipedia.org/wiki/Luma_%28video%29 91 | * 92 | * @return int Luma value (0 to 255). 93 | */ 94 | public function get_luma() { 95 | $rgba = $this->get_rgba(); 96 | $luma = 0.2126 * $rgba[0] + 0.7152 * $rgba[1] + 0.0722 * $rgba[2]; 97 | return $luma; 98 | } 99 | 100 | /** 101 | * Convert the pixel to grayscale. 102 | * 103 | * @since 1.0.0 104 | * 105 | * @return $this 106 | */ 107 | protected function to_grayscale() { 108 | $luma = $this->get_luma(); 109 | $color_id = imagecolorallocate( $this->image, $luma, $luma, $luma ); 110 | imagesetpixel( $this->image, $this->x, $this->y, $color_id ); 111 | return $this; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /php/class-plugin.php: -------------------------------------------------------------------------------- 1 | get_path( 'php/class-image-editor-gd.php' ) ); 44 | include_once( $this->get_path( 'php/class-image-editor-imagick.php' ) ); 45 | include_once( $this->get_path( 'php/class-image-pixel-gd.php' ) ); 46 | 47 | array_unshift( $editors, 'TwotoneFX_Image_Editor_GD' ); 48 | array_unshift( $editors, 'TwotoneFX_Image_Editor_Imagick' ); 49 | 50 | return $editors; 51 | } 52 | 53 | /** 54 | * Process an image. 55 | * 56 | * @since 1.0.0 57 | * 58 | * @param WP_Image_Editor $image An image editor instance. 59 | * @param array $changes Array of operations to apply to the image. 60 | * @return WP_Image_Editor 61 | */ 62 | public function process_image( $image, $changes ) { 63 | // Ensure the editor can apply a duotone effect. 64 | if ( ! method_exists( $image, 'twotonefx' ) ) { 65 | return $image; 66 | } 67 | 68 | foreach ( array_reverse( $changes ) as $operation ) { 69 | if ( 'twotonefx' !== $operation->type ) { 70 | continue; 71 | } 72 | 73 | $start = self::sanitize_hex_color( $operation->start ); 74 | $end = self::sanitize_hex_color( $operation->end ); 75 | 76 | $image->twotonefx( $start, $end ); 77 | 78 | if ( isset( $_REQUEST['do'] ) && 'save' === $_REQUEST['do'] ) { 79 | $post_id = absint( $_REQUEST['postid'] ); 80 | 81 | update_post_meta( $post_id, '_twotonefx_start_color', $start ); 82 | update_post_meta( $post_id, '_twotonefx_end_color', $end ); 83 | } 84 | break; 85 | } 86 | 87 | return $image; 88 | } 89 | 90 | /** 91 | * Add data to attachemnts for use in JavaScript. 92 | * 93 | * @since 1.0.0 94 | * 95 | * @param array $response Attachment data. 96 | * @param WP_Post $attachment Attachment post object. 97 | * @param array $meta Attachment meta data. 98 | * @return array 99 | */ 100 | public function prepare_attachment_for_js( $response, $attachment, $meta ) { 101 | $start = get_post_meta( $attachment->ID, '_twotonefx_start_color', true ); 102 | $end = get_post_meta( $attachment->ID, '_twotonefx_end_color', true ); 103 | 104 | $response['twotonefxStart'] = empty( $start ) ? $this->get_default_start_color() : $start; 105 | $response['twotonefxEnd'] = empty( $end ) ? $this->get_default_end_color() : $end; 106 | 107 | return $response; 108 | } 109 | 110 | /** 111 | * Delete TwotoneFX colors when an image is restored to its original state. 112 | * 113 | * @since 1.0.0 114 | * 115 | * @param int $meta_id ID of updated metadata entry. 116 | * @param int $object_id Object ID. 117 | * @param string $meta_key Meta key. 118 | * @param mixed $meta_value Meta value. 119 | */ 120 | public function maybe_delete_post_meta( $meta_id, $post_id, $meta_key, $meta_value ) { 121 | if ( '_wp_attachment_backup_sizes' !== $meta_key ) { 122 | return; 123 | } 124 | 125 | $meta = maybe_unserialize( $meta_value ); 126 | $file = basename( get_attached_file( $post_id ) ); 127 | 128 | if ( isset( $meta['full-orig'] ) && $meta['full-orig']['file'] === $file ) { 129 | delete_post_meta( $post_id, '_twotonefx_start_color' ); 130 | delete_post_meta( $post_id, '_twotonefx_end_color' ); 131 | } 132 | } 133 | 134 | /** 135 | * Ensure the edit functionality is enqueued on the attachement edit screen. 136 | * 137 | * @since 1.0.0 138 | */ 139 | public function attachment_edit_screen_footer() { 140 | if ( 'attachment' !== get_current_screen()->id ) { 141 | return; 142 | } 143 | 144 | wp_enqueue_media(); 145 | $this->print_templates(); 146 | 147 | $post_id = get_post()->ID; 148 | $start = get_post_meta( $post_id, '_twotonefx_start_color', true ); 149 | $end = get_post_meta( $post_id, '_twotonefx_end_color', true ); 150 | 151 | wp_localize_script( 'twotonefx-image-edit', '_twotonefxAttachment', array( 152 | 'startColor' => empty( $start ) ? $this->get_default_start_color() : $start, 153 | 'endColor' => empty( $end ) ? $this->get_default_end_color() : $end, 154 | ) ); 155 | } 156 | 157 | /** 158 | * Enqueue scripts and styles. 159 | * 160 | * @since 1.0.0 161 | */ 162 | public function enqueue_assets() { 163 | wp_enqueue_style( 'wp-color-picker' ); 164 | 165 | wp_enqueue_script( 166 | 'twotonefx-image-edit', 167 | $this->get_url( 'assets/js/image-edit.js' ), 168 | array( 'image-edit', 'media-grid', 'wp-backbone', 'wp-color-picker', 'wp-util' ), 169 | '1.0.0', 170 | true 171 | ); 172 | } 173 | 174 | /** 175 | * Print Underscore.js templates. 176 | * 177 | * @since 1.0.0 178 | */ 179 | public function print_templates() { 180 | include_once( $this->get_path( 'views/media-templates.php' ) ); 181 | } 182 | 183 | /** 184 | * Sanitizes a hex color. 185 | * 186 | * Returns either '', a 3 or 6 digit hex color (with #), or nothing. 187 | * For sanitizing values without a #, see sanitize_hex_color_no_hash(). 188 | * 189 | * @since 1.0.0 190 | * 191 | * @param string $color 192 | * @return string 193 | */ 194 | public static function sanitize_hex_color( $color ) { 195 | if ( '' === $color ) { 196 | return ''; 197 | } 198 | 199 | // 3 or 6 hex digits, or the empty string. 200 | if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) { 201 | return $color; 202 | } 203 | } 204 | 205 | /** 206 | * Retrieve the default starting color (shadows). 207 | * 208 | * @since 1.0.0 209 | * 210 | * @return string Hex color string. 211 | */ 212 | protected function get_default_start_color() { 213 | return apply_filters( 'twotonefx_default_start_color', '#000000' ); 214 | } 215 | 216 | /** 217 | * Retrieve the default ending color (highlights). 218 | * 219 | * @since 1.0.0 220 | * 221 | * @return string Hex color string. 222 | */ 223 | protected function get_default_end_color() { 224 | return apply_filters( 'twotonefx_default_end_color', '#ffffff' ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Twotone FX === 2 | Contributors: audiotheme, bradyvercher, brodyvercher, thelukemcdonald 3 | Tags: duotone, photo, filter, image, media, fx 4 | Requires at least: 4.3 5 | Tested up to: 4.5 6 | Stable tag: trunk 7 | License: GPL-2.0+ 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Apply a duotone effect to photos in the media library. 11 | 12 | == Description == 13 | 14 | Inspired by the duotone effect employed in the [*Twotone*](https://audiotheme.com/view/twotone/?utm_source=wordpress.org&utm_medium=link&utm_content=twotonefx-readme&utm_campaign=plugins) theme, Twotone FX makes it easy to preview and apply a similar filter to any photo in your Media Library. 15 | 16 | = Support Policy = 17 | 18 | We'll do our best to keep this plugin up to date, fix bugs and implement features when possible, but technical support will only be provided for active AudioTheme customers. If you enjoy this plugin and would like to support its development, you can: 19 | 20 | * [Check out AudioTheme](https://audiotheme.com/?utm_source=wordpress.org&utm_medium=link&utm_content=twotonefx-readme&utm_campaign=plugins) and tell your friends! 21 | * Help out on the [support forums](https://wordpress.org/support/plugin/twotonefx). 22 | * Consider [contributing on GitHub](https://github.com/audiotheme/twotonefx). 23 | * [Leave a review](https://wordpress.org/support/view/plugin-reviews/twotonefx#postform) and let everyone know how much you love it. 24 | * [Follow @AudioTheme](https://twitter.com/AudioTheme) on Twitter. 25 | 26 | 27 | == Installation == 28 | 29 | Install like any other plugin. [Refer to the Codex](http://codex.wordpress.org/Managing_Plugins#Installing_Plugins) if you have any questions. 30 | 31 | = Accessing the Image Editor = 32 | 33 | Visit the Media Library at Media → Library in your admin panel. 34 | 35 | If you're using the default Grid Mode: 36 | 37 | 1. Click an image to open the Attachment Details popup 38 | 2. Click the Edit Image button below the image 39 | 3. Scroll the right sidebar down to the Twotone Effect section 40 | 41 | If you're using the List Mode: 42 | 43 | 1. Click an image to open the Edit Attachment screen 44 | 2. Click the Edit Image button below the image 45 | 3. Find the Twotone Effect section 46 | 47 | It's also possible to access the image editor in the Media Manager popup when editing any post or page. 48 | 49 | = Applying the Twotone Effect = 50 | 51 | The Twotone Effect section in the image editor consists of two color pickers and a button for applying the filter to the image to preview it before saving your changes. 52 | 53 | The Starting Color replaces shadows in the image, so choosing a dark color usually produces the best results. 54 | 55 | The Ending Color replaces highlights in the image. Choose a lighter color that contrasts with the starting color for the best results. 56 | 57 | After choosing colors, preview the image by clicking the Apply button. If you don't like the way the effect turned out, you can either use the Undo button on the image editor toolbar, or select different colors and apply them. 58 | 59 | Your changes won't be made permanent until you click the Save button. 60 | 61 | 62 | == Screenshots == 63 | 64 | 1. Image editor with a section for adding a duotone effect to a photo. 65 | 66 | 67 | == Notes == 68 | 69 | = How It works = 70 | 71 | Behind the scenes, a gradient map is generated representing values between the starting and ending colors selected for the filter. The photo is then converted to grayscale and each pixel is mapped to one of the colors in the gradient map based on its brightness. 72 | 73 | Two methods for editing images are available to support different server environments: 74 | 75 | * ImageMagick is the preferred method and provides the best performance. 76 | * A GD Graphics Library implementation is provided as a fallback, however, it can be resource intensive and may exhaust memory on some shared hosts, especially when processing large photos. 77 | 78 | 79 | == Changelog == 80 | 81 | = 1.0.0 = 82 | * Initial release. 83 | -------------------------------------------------------------------------------- /screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audiotheme/twotonefx/846e288663b24d461d710097c7b7be5dde99fe09/screenshot-1.jpg -------------------------------------------------------------------------------- /twotonefx.php: -------------------------------------------------------------------------------- 1 | set_directory( plugin_dir_path( __FILE__ ) ) 29 | ->set_file( __FILE__ ) 30 | ->set_slug( 'twotonefx' ) 31 | ->set_url( plugin_dir_url( __FILE__ ) ); 32 | 33 | /** 34 | * Localize the plugin. 35 | * 36 | * @since 1.0.0 37 | */ 38 | function twotonefx_load_textdomain() { 39 | $plugin_rel_path = dirname( plugin_basename( __FILE__ ) ) . '/languages'; 40 | load_plugin_textdomain( 'twotonefx', false, $plugin_rel_path ); 41 | } 42 | add_action( 'plugins_loaded', 'twotonefx_load_textdomain' ); 43 | 44 | /** 45 | * Load the plugin. 46 | */ 47 | add_action( 'plugins_loaded', array( $twotonefx_plugin, 'register_hooks' ) ); 48 | -------------------------------------------------------------------------------- /views/media-templates.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | --------------------------------------------------------------------------------