├── .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 | 
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 |
--------------------------------------------------------------------------------