%% @copyright 2021 Marc Worrell %% @author Marc Worrell %% @doc Add functionality to the admin to edit images. %% Copyright 2021 Marc Worrell %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -module(mod_image_edit). -author("Marc Worrell "). -mod_title("Image Edit"). -mod_description("Edit uploaded images."). -mod_prio(500). -include_lib("zotonic_core/include/zotonic.hrl"). -export([ event/2, observe_media_upload_rsc_props/3, observe_rsc_update/3, observe_media_preview_options/3 ]). event(#submit{ message = {edit_form, Args} }, Context) -> {id, ArgId} = proplists:lookup(id, Args), OnSuccess = proplists:get_all_values(on_success, Args), Id = m_rsc:rid(ArgId, Context), case z_acl:rsc_editable(Id, Context) of true -> Settings = z_context:get_q_all_noz(Context), case m_image_edit:save_settings(Id, Settings, Context) of {ok, _} -> Context1 = z_render:growl(?__("Saved the image settings.", Context), Context), z_render:wire(OnSuccess, Context1); {error, _} -> z_render:growl_error(?__("Could not save the image settings.", Context), Context) end; false -> z_render:growl_error(?__("You don't have permission to edit this page.", Context), Context) end. observe_media_upload_rsc_props(#media_upload_rsc_props{ }, Props, _Context) -> case maps:get(<<"medium_edit_settings">>, Props, undefined) of M when is_map(M) -> Props; _ -> Props#{ <<"medium_edit_settings">> => #{} } end. observe_rsc_update(#rsc_update{}, {ok, #{ <<"medium_edit_settings">> := Settings } = Props}, _Context) -> Sanitized = m_image_edit:sanitize(Settings), {ok, Props#{ <<"medium_edit_settings">> => Sanitized }}; observe_rsc_update(_, Acc, _Context) -> Acc. %% @doc Expand the image modifications that influence the generated preview image. %% The order of the operations is important. As they are added in front of the %% options list the last added option will be performed first. %% The order of the operations is: %% %%
    %%
  1. Lossless (just an option for later image compression)
  2. %%
  3. Angle
  4. %%
  5. Tilt
  6. %%
  7. Pan
  8. %%
  9. Rotate (90deg increments)
  10. %%
  11. Crop
  12. %%
  13. Contrast
  14. %%
  15. Brightness
  16. %%
%% observe_media_preview_options(#media_preview_options{ id = undefined }, Options, _Context) -> Options; observe_media_preview_options(#media_preview_options{ id = Id, width = W, height = H }, Options, Context) -> {ok, Map} = m_image_edit:get_settings(Id, Context), expand_edit_settings(Map, W, H, Options, Context). expand_edit_settings(Settings, W, H, Options, Context) -> % Contrast & brightness are last OptionsB = case maps:get(<<"brightness">>, Settings, 0) of 0 -> Options; Brightness -> [ {brightness, Brightness} | Options ] end, OptionsCB = case maps:get(<<"contrast">>, Settings, 0) of 0 -> OptionsB; Contrast -> [ {contrast, Contrast} | OptionsB ] end, % Crop before contrast & brightness CL = maps:get(<<"crop_left">>, Settings, 0.0), CR = maps:get(<<"crop_right">>, Settings, 0.0), CT = maps:get(<<"crop_top">>, Settings, 0.0), CB = maps:get(<<"crop_bottom">>, Settings, 0.0), OptionsCrop = case nz(CL) orelse nz(CR) orelse nz(CT) orelse nz(CB) of true -> [ {cropp, [ CL, CR, CT, CB]} | OptionsCB ]; false -> OptionsCB end, % Rotate before crop Options6 = case maps:get(<<"rotate">>, Settings, 0) of 0 -> OptionsCrop; R -> [ {rotate, R} | OptionsCrop ] end, % Tilt, angle and pan before rotate Roll = maps:get(<<"roll">>, Settings, 0), Tilt = maps:get(<<"tilt">>, Settings, 0), Pan = maps:get(<<"pan">>, Settings, 0), Options7 = case {Roll, Tilt, Pan} of {0, 0, 0} -> Options6; _ -> [{rotate3d, [ Roll, Tilt, Pan ]} | Options6 ] end, % Lossless as first OptionsLL = case maps:get(<<"is_lossless">>, Settings, false) of true -> [ {lossless, true} | Options7 ]; false -> Options7 end, % Crop center - defined in fractions - recalculated to pixels on the original image. % The image preview routines will map the crop coordinate using cropp and rotation. CropX = maps:get(<<"crop_center_x">>, Settings, -1), CropY = maps:get(<<"crop_center_y">>, Settings, -1), OptionsCropCenter = case CropX >= 0.0 andalso CropY >= 0.0 of true -> CropXPix = trunc(CropX * W), CropYPix = trunc(CropY * H), opt_crop_center([ CropXPix, CropYPix ], OptionsLL, Context); false -> OptionsLL end, OptionsCropCenter. % Test if a floating point value is non-zero nz(V) -> V >= 0.1 orelse V =< -0.1. opt_crop_center(CropXY, Options, Context) -> Crop = proplists:get_value(crop, Options), case is_crop_center(Crop) of true -> add_crop_center(CropXY, Options); false when Crop =:= undefined -> opt_crop_center_mediaclass(proplists:get_value(mediaclass, Options), CropXY, Options, Context); false -> Options end. opt_crop_center_mediaclass(undefined, _CropXY, Options, _Context) -> Options; opt_crop_center_mediaclass(Mediaclass, CropXY, Options, Context) -> {ok, Props, _Hash} = z_mediaclass:get(Mediaclass, Context), case is_crop_center(proplists:get_value(crop, Props)) of true -> add_crop_center(CropXY, Options); false -> Options end. add_crop_center(CropXY, Options) -> z_utils:prop_replace(crop, CropXY, Options). is_crop_center(true) -> true; is_crop_center(center) -> true; is_crop_center(<<"center">>) -> true; is_crop_center("center") -> true; is_crop_center(_) -> false.