PageRenderTime 80ms CodeModel.GetById 18ms app.highlight 57ms RepoModel.GetById 1ms app.codeStats 0ms

/src/support/z_media_preview.erl

https://code.google.com/p/zotonic/
Erlang | 446 lines | 342 code | 46 blank | 58 comment | 19 complexity | 9cf8e3eeae6865686dc35ed938d322c9 MD5 | raw file
  1%% @author Marc Worrell <marc@worrell.nl>
  2%% @copyright 2009 Marc Worrell
  3%% Date: 2009-03-02
  4%% @doc Make still previews of media, using image manipulation functions.  Resize, crop, gray, etc.
  5%% This uses the command line imagemagick tools for all image manipulation.
  6%% This code is adapted from PHP GD2 code, so the resize/crop could've been done more efficiently, but it works :-)
  7
  8%% Copyright 2009 Marc Worrell
  9%%
 10%% Licensed under the Apache License, Version 2.0 (the "License");
 11%% you may not use this file except in compliance with the License.
 12%% You may obtain a copy of the License at
 13%% 
 14%%     http://www.apache.org/licenses/LICENSE-2.0
 15%% 
 16%% Unless required by applicable law or agreed to in writing, software
 17%% distributed under the License is distributed on an "AS IS" BASIS,
 18%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 19%% See the License for the specific language governing permissions and
 20%% limitations under the License.
 21
 22-module(z_media_preview).
 23-author("Marc Worrell <marc@worrell.nl").
 24
 25%% interface functions
 26-export([
 27    convert/4,
 28    size/3,
 29    can_generate_preview/1,
 30	out_mime/2,
 31    string2filter/2,
 32    cmd_args/3,
 33    calc_size/7
 34]).
 35
 36-define(MAX_WIDTH,  5000).
 37-define(MAX_HEIGHT, 5000).
 38
 39-include_lib("zotonic.hrl").
 40
 41
 42%% @spec convert(InFile, OutFile, Filters, Context) -> ok | {error, Reason}
 43%% @doc Convert the Infile to an outfile with a still image using the filters.
 44%% @todo Check if the conversion has been done
 45%% @todo Check if the target /= source
 46convert(InFile, OutFile, Filters, Context) ->
 47    case z_media_identify:identify(InFile, Context) of
 48        {ok, FileProps} ->
 49            {mime, Mime} = proplists:lookup(mime, FileProps),
 50            case can_generate_preview(Mime) of
 51                true ->
 52                    OutMime = z_media_identify:guess_mime(OutFile),
 53                    {EndWidth, EndHeight, CmdArgs} = cmd_args(FileProps, Filters, OutMime),
 54                    z_utils:assert(EndWidth  < ?MAX_WIDTH, image_too_wide),
 55                    z_utils:assert(EndHeight < ?MAX_HEIGHT, image_too_high),
 56                    Args1   = lists:flatten(z_utils:combine(32, CmdArgs)),
 57                    Cmd     = ["convert ", z_utils:os_filename(InFile++infile_suffix(Mime)), " ", Args1, " ", z_utils:os_filename(OutFile)],
 58                    file:delete(OutFile),
 59                    ok = filelib:ensure_dir(OutFile),
 60                    Result  = z_media_preview_server:exec(lists:flatten(Cmd), OutFile),
 61                    case filelib:is_regular(OutFile) of
 62                        true ->
 63                            ok;
 64                        false -> 
 65                            ?LOG("convert cmd ~p failed, result ~p", [Cmd, Result]),
 66                            {error, "Error during convert."}
 67                    end;
 68                false ->
 69                    {error, "Can not convert "++Mime}
 70            end;
 71        {error, Reason} ->
 72            {error, Reason}
 73    end.
 74
 75%% Return the ImageMagick input-file suffix.
 76%% @spec infile_suffix(Mime) -> Suffix::string()
 77infile_suffix(<<"image/gif">>) -> [];
 78infile_suffix(_) -> "[0]".
 79     
 80
 81%% @spec size(MediaRef, Filters, Context) -> {size, Width, Height, ResizedMime} | {error, Reason}
 82%%   MediaRef = Filename | MediaProps
 83%% @doc Calculate the size of the resulting image.
 84size([{_Prop, _Value}|_] = Props, Filters, _Context) ->
 85    size_props(Props, Filters);
 86size(InFile, Filters, Context) ->
 87    case z_media_identify:identify(InFile, Context) of
 88        {ok, FileProps} ->
 89            size_props(FileProps, Filters);
 90        {error, Reason} ->
 91            {error, Reason}
 92    end.
 93    
 94    
 95    size_props(FileProps, Filters) ->
 96        {mime, Mime} = proplists:lookup(mime, FileProps),
 97        case can_generate_preview(Mime) of
 98            true ->
 99                {width, ImageWidth}   = proplists:lookup(width, FileProps),
100                {height, ImageHeight} = proplists:lookup(height, FileProps),
101                {orientation, Orientation} = proplists:lookup(orientation, FileProps),
102                
103                ReqWidth   = z_convert:to_integer(proplists:get_value(width, Filters)),
104                ReqHeight  = z_convert:to_integer(proplists:get_value(height, Filters)),
105                {CropPar,_Filters1} = fetch_crop(Filters),
106                {ResizeWidth,ResizeHeight,CropArgs} = calc_size(ReqWidth, ReqHeight, ImageWidth, ImageHeight, CropPar, Orientation, is_enabled(upscale, Filters)),
107                case CropArgs of
108                    none -> 
109                        case is_enabled(extent, Filters) of
110                            true when is_integer(ReqWidth) andalso is_integer(ReqHeight) ->
111                                {size, ReqWidth, ReqHeight, "image/jpeg"};
112                            _ ->
113                                {size, ResizeWidth, ResizeHeight, "image/jpeg"}
114                        end;
115                    {_CropL, _CropT, CropWidth, CropHeight} -> {size, CropWidth, CropHeight, "image/jpeg"}
116                end;
117            false ->
118                {error, {no_preview_for_mimetype, Mime}}
119        end.
120
121
122%% @spec can_generate_preview(Mime) -> true | false
123%% @doc Check if we can generate a preview image of the given mime type
124can_generate_preview(B) when is_binary(B) -> can_generate_preview(binary_to_list(B));
125can_generate_preview("image/" ++ _) -> true;
126can_generate_preview("application/pdf") -> true;
127can_generate_preview("application/postscript") -> true;
128can_generate_preview(_Mime) -> false.
129
130
131%% @doc Map filters to commandline options
132cmd_args(FileProps, Filters, OutMime) ->
133    {width, ImageWidth}   = proplists:lookup(width, FileProps),
134    {height, ImageHeight} = proplists:lookup(height, FileProps),
135    {mime, Mime0} = proplists:lookup(mime, FileProps),
136    Mime = z_convert:to_list(Mime0),
137    {orientation, Orientation} = proplists:lookup(orientation, FileProps),
138    ReqWidth   = proplists:get_value(width, Filters),
139    ReqHeight  = proplists:get_value(height, Filters),
140    {CropPar,Filters1} = fetch_crop(Filters),
141    {ResizeWidth,ResizeHeight,CropArgs} = calc_size(ReqWidth, ReqHeight, ImageWidth, ImageHeight, CropPar, Orientation, is_enabled(upscale, Filters)),
142    Filters2   = [  {make_image, Mime},
143                    {correct_orientation, Orientation},
144                    {resize, ResizeWidth, ResizeHeight, is_enabled(upscale, Filters)}, 
145                    {crop, CropArgs},
146                    {colorspace, "RGB"} | Filters1],
147    Filters3 = case {CropArgs,is_enabled(extent, Filters)} of
148                    {none,true} -> Filters2 ++ [{extent, ReqWidth, ReqHeight}];
149                    _ -> Filters2
150                end,
151    Filters4 = case is_blurred(Filters3) of
152                    true ->  Filters3;
153                    false -> case Mime of
154                                 "image/gif" -> Filters3;
155                                 "image/png" -> Filters3;
156                                 _ -> Filters3 ++ [sharpen_small]
157                             end
158               end,
159    Filters5 = case proplists:get_value(background, Filters4) of
160                    undefined -> default_background(OutMime) ++ Filters4;
161                    _ -> Filters4
162               end,
163
164    {EndWidth,EndHeight,Args} = lists:foldl(fun (Filter, {W,H,Acc}) -> 
165                                                {NewW,NewH,Arg} = filter2arg(Filter, W, H),
166                                                {NewW,NewH,[Arg|Acc]} 
167                                            end,
168                                            {ImageWidth,ImageHeight,[]},
169                                            Filters5),
170    {EndWidth, EndHeight, lists:reverse(Args)}.
171
172
173default_background("image/gif") -> [coalesce];
174default_background("image/png") -> [coalesce];
175default_background(_) -> [{background,"white"}, {layers,"flatten"}].
176
177%% @doc Check if there is a blurring filter that prevents us from sharpening the resulting image
178is_blurred([]) -> false;
179is_blurred([blur|_]) -> true;
180is_blurred([{blur, _}|_]) -> true;
181is_blurred([_|Rest]) -> is_blurred(Rest).
182
183
184is_enabled(_F, []) -> false;
185is_enabled(F, [F|_]) -> true;
186is_enabled(F, [{F, Val}|_]) -> z_convert:to_bool(Val);
187is_enabled(F, [_|R]) -> is_enabled(F, R).
188
189
190%% @spec out_mime(Mime, Options) -> {Mime, Extension}
191%% @doc Return the preferred mime type of the image generated by resizing an image of a certain type and size.
192out_mime("image/gif", _) ->
193    %% gif is gif, daar kan je gif op innemen
194    {"image/gif", ".gif"};
195out_mime(_Mime, Options) ->
196	case lists:member("lossless", Options) orelse proplists:is_defined(lossless, Options) of
197		false -> {"image/jpeg", ".jpg"};
198		true  -> {"image/png", ".png"}
199	end.
200
201
202%% @spec filter2arg(Filter, Width, Height) -> {NewWidth, NewHeight, Filter::string}
203%% @doc Map filters to an ImageMagick argument
204filter2arg({make_image, "application/pdf"}, Width, Height) ->
205    RArg = ["-resize ", integer_to_list(Width),$x,integer_to_list(Height)],
206    {Width, Height, RArg};
207filter2arg(coalesce, Width, Height) ->
208    {Width, Height, "-coalesce"};
209filter2arg({make_image, _Mime}, Width, Height) ->
210    {Width, Height, []};
211filter2arg({correct_orientation, Orientation}, Width, Height) ->
212    case Orientation of
213    	2 -> {Width, Height, "-flip"};
214    	3 -> {Width, Height, "-rotate 180"};
215    	4 -> {Width, Height, "-flop"};
216    	5 -> {Width, Height, "-transpose"};
217    	6 -> {Width, Height, "-rotate 90"};
218    	7 -> {Width, Height, "-transverse"};
219    	8 -> {Width, Height, "-rotate 270"};
220        _ -> {Width, Height, []}
221    end;
222filter2arg({background, Color}, Width, Height) ->
223    {Width, Height, ["-background ", $", z_utils:os_escape(Color), $"]};
224filter2arg({layers, Method}, Width, Height) ->
225    {Width, Height, ["-layers ", $", z_utils:os_escape(Method), $"]};
226filter2arg({colorspace, Colorspace}, Width, Height) ->
227    {Width, Height, ["-colorspace ", $", z_utils:os_escape(Colorspace), $"]};
228filter2arg({width, _}, Width, Height) ->
229    {Width, Height, []};
230filter2arg({height, _}, Width, Height) ->
231    {Width, Height, []};
232filter2arg({resize, Width, Height, _}, Width, Height) ->
233    {Width, Height, []};
234filter2arg({resize, EndWidth, EndHeight, false}, Width, Height) 
235  when Width =< EndWidth andalso Height =< EndHeight ->
236    % Prevent scaling up, perform an extent instead
237    GArg = "-gravity West",
238    EArg = ["-extent ", integer_to_list(EndWidth),$x,integer_to_list(EndHeight)],
239    % Still thumbnail to remove extra info from the image
240    RArg = ["-thumbnail ", z_utils:os_escape([integer_to_list(EndWidth),$x,integer_to_list(EndHeight),$!])],
241    {EndWidth, EndHeight, [GArg, 32, EArg, 32, RArg]};
242filter2arg({resize, EndWidth, EndHeight, true}, Width, Height) 
243  when Width < EndWidth andalso Height < EndHeight ->
244    % Scale up
245    EArg = ["-resize ", integer_to_list(EndWidth),$x,integer_to_list(EndHeight)],
246    RArg = ["-thumbnail ", z_utils:os_escape([integer_to_list(EndWidth),$x,integer_to_list(EndHeight),$!])],
247    {EndWidth, EndHeight, [EArg, 32, RArg]};
248filter2arg({extent, EndWidth, EndHeight}, Width, Height) when EndWidth == undefined orelse EndHeight == undefined ->
249    {Width, Height, []};
250filter2arg({extent, EndWidth, EndHeight}, Width, Height) when Width /= EndWidth orelse Height /= EndHeight ->
251    GArg = "-gravity Center",
252    EArg = ["-extent ", integer_to_list(EndWidth),$x,integer_to_list(EndHeight)],
253    {EndWidth, EndHeight, [GArg, 32, EArg]};
254filter2arg({resize, EndWidth, EndHeight, _}, _Width, _Height) ->
255    GArg = "-gravity NorthWest",
256    RArg = ["-thumbnail ", z_utils:os_escape([integer_to_list(EndWidth),$x,integer_to_list(EndHeight),$!])],
257    {EndWidth, EndHeight, [GArg, 32, RArg]};
258filter2arg({crop, none}, Width, Height) ->
259    {Width, Height, []};
260filter2arg({crop, {CropL, CropT, CropWidth, CropHeight}}, _Width, _Height) ->
261    GArg = "-gravity NorthWest",
262    CArg = ["-crop ",   integer_to_list(CropWidth),$x,integer_to_list(CropHeight), 
263                        $+,integer_to_list(CropL),$+,integer_to_list(CropT)],
264    RArg = "+repage",
265    {CropWidth, CropHeight, [GArg,32,CArg,32,RArg]};
266filter2arg(grey, Width, Height) ->
267    {Width, Height, "-colorspace Gray"};
268filter2arg(mono, Width, Height) ->
269    {Width, Height, "-monochrome"};
270filter2arg(flip, Width, Height) ->
271    {Width, Height, "-flip"};
272filter2arg(flop, Width, Height) ->
273    {Width, Height, "-flop"};
274filter2arg(blur, Width, Height) ->
275    filter2arg({blur, 10}, Width, Height);
276filter2arg({blur, Blur}, Width, Height) when is_integer(Blur) ->
277    {Width, Height, ["-blur ", integer_to_list(Blur)]};
278filter2arg({blur, Blur}, Width, Height) when is_list(Blur) ->
279    case string:tokens(Blur, "x") of
280        [A,B] -> {Width, Height, ["-blur ", ensure_integer(A), $x, ensure_integer(B)]};
281        [A] ->   {Width, Height, ["-blur ", ensure_integer(A)]}
282    end;
283filter2arg(sharpen_small, Width, Height) when Width < 400 andalso Height < 400 ->
284    {Width, Height, "-unsharp 0.3x0.7 "}; % 6x3+1+0
285filter2arg(sharpen_small, Width, Height) ->
286    {Width, Height, []};
287filter2arg(lossless, Width, Height) ->
288    {Width, Height, []};
289filter2arg({quality, Q}, Width, Height) ->
290    {Width,Height, ["-quality ",integer_to_list(Q)]};
291filter2arg({removebg, Fuzz}, Width, Height) ->
292    {Width, Height, ["-matte -fill none -fuzz ", integer_to_list(Fuzz), "% ",
293                     "-draw 'matte 0,0 floodfill' ",
294                     "-draw 'matte 0,", integer_to_list(Height-1), " floodfill' ",
295                     "-draw 'matte ", integer_to_list(Width-1), ",0 floodfill' ",
296                     "-draw 'matte ", integer_to_list(Width-1), ",", integer_to_list(Height-1), " floodfill' "
297                    ]};
298% Ignore these (are already handled as other filter args)
299filter2arg(extent, Width, Height) ->
300    {Width, Height, []};
301filter2arg({extent, _}, Width, Height) ->
302    {Width, Height, []};
303filter2arg({extent, _, _}, Width, Height) ->
304    {Width, Height, []};
305filter2arg(upscale, Width, Height) ->
306    {Width, Height, []}.
307
308
309%% @spec fetch_crop(Filters) -> {Crop, Filters}
310%% @doc Split the filters into size/crop and image manipulation filters.
311fetch_crop(Filters) ->
312    {Crop,OtherFilters} = lists:partition(
313                                fun (crop) -> true;
314                                    (F) when is_tuple(F) -> element(1,F) == 'crop';
315                                    (_) -> false
316                                end, Filters),
317    CropPar = case Crop of
318                  [{crop,None}] when None == false; None == undefined; None == ""; None == <<>> -> none;
319                  [{crop,Gravity}] -> Gravity; % center or one of the wind directions
320                  _ -> none
321              end,
322    {CropPar,OtherFilters}.
323
324
325%%@doc Calculate the size of the resulting image, depends on the crop and the original image size
326calc_size(Width, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale) when Orientation >= 5 ->
327    calc_size(Width, Height, ImageHeight, ImageWidth, CropPar, 1, IsUpscale);
328
329calc_size(undefined, undefined, ImageWidth, ImageHeight, _CropPar, _Orientation, _IsUpscale) ->
330    {ImageWidth, ImageHeight, none};
331
332calc_size(Width, undefined, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale) when CropPar /= none ->
333    calc_size(Width, Width, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale);
334
335calc_size(undefined, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale) when CropPar /= none ->
336    calc_size(Height, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale);
337
338calc_size(undefined, Height, ImageWidth, ImageHeight, none, 1, false) when ImageHeight < Height ->
339    % Image will be extented
340    {ImageWidth, Height, none};
341
342calc_size(undefined, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale) ->
343    Width = round((ImageWidth / ImageHeight) * Height),
344    calc_size(Width, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale);
345
346calc_size(Width, undefined, ImageWidth, ImageHeight, none, 1, false) when ImageWidth < Width ->
347    % Image will be extented
348    {Width, ImageHeight, none};
349
350calc_size(Width, undefined, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale) ->
351    Height = round((ImageHeight / ImageWidth) * Width),
352    calc_size(Width, Height, ImageWidth, ImageHeight, CropPar, Orientation, IsUpscale);
353
354calc_size(Width, Height, ImageWidth, ImageHeight, CropPar, _Orientation, false) 
355    when CropPar /= none, Width > ImageWidth, Height > ImageHeight ->
356    {Width, Height, none};
357
358calc_size(Width, Height, ImageWidth, ImageHeight, CropPar, _Orientation, _IsUpscale) ->
359    ImageAspect = ImageWidth / ImageHeight,
360    Aspect      = Width / Height,
361    case CropPar of
362        none ->
363            case Aspect > ImageAspect of
364                true  -> {ceil(ImageAspect * Height), Height, none};
365                false -> {Width, ceil(Width / ImageAspect), none}
366            end;
367        _ ->
368			% When we are doing a crop then we have to calculate the
369			% maximum inner bounding box, and not the maximum outer 
370			% bounding box for the image
371		    {W,H} = case Aspect > ImageAspect of
372        		        true ->
373        				    % width is the larger one
374        				    {Width, Width / ImageAspect};
375        				false ->
376        				    % height is the larger one
377        				    {ImageAspect * Height, Height}
378        			end,
379        	CropL = case CropPar of
380        	            X when X == north_west; X == west; X == south_west -> 0;
381        	            X when X == north_east; X == east; X == south_east -> ceil(W - Width);
382        	            _ -> ceil((W - Width) / 2)
383    	            end,
384    	    CropT = case CropPar of
385    	                Y when Y == north_west; Y == north; Y == north_east -> 0;
386    	                Y when Y == south_west; Y == south; Y == south_east -> ceil(H - Height);
387    	                _ -> ceil((H - Height) / 2)
388	                end,
389
390	        % @todo Prevent scaleup of the image, but preserve the result size
391	        % The crop is relative to the original image
392	        {ceil(W), ceil(H), {CropL, CropT, Width, Height}}
393    end.
394
395
396%% @spec string2filter(Filter, Arg) -> FilterTuple
397%% @doc Map the list of known filters and known args to atoms.  Used when mapping preview urls back to filter args.
398string2filter("crop", []) ->
399    {crop,center};
400string2filter("crop", Where) -> 
401    Dir = case Where of
402            "north"      -> north;
403            "north_east" -> north_east;
404            "east"       -> east;
405            "south_east" -> south_east;
406            "south"      -> south;
407            "south_west" -> south_west;
408            "west"       -> west;
409            "north_west" -> north_west;
410            "center"     -> center
411          end,
412    {crop,Dir};
413string2filter("grey",[]) ->
414    grey;
415string2filter("mono",[]) ->
416    mono;
417string2filter("flip",[]) ->
418    flip;
419string2filter("flop",[]) ->
420    flop;
421string2filter("extent",[]) ->
422    extent;
423string2filter("upscale",[]) ->
424    upscale;
425string2filter("blur",[]) ->
426    blur;
427string2filter("blur",Arg) ->
428    {blur,Arg};
429string2filter("quality", Arg) ->
430    {quality,list_to_integer(Arg)};
431string2filter("background", Arg) ->
432    {background,Arg};
433string2filter("lossless", []) ->
434    lossless;
435string2filter("removebg", []) ->
436    {removebg, 5};
437string2filter("removebg", Arg) ->
438    {removebg, list_to_integer(Arg)}.
439
440
441% simple ceil for positive numbers
442ceil(A)  -> round(A + 0.499999).
443%floor(A) -> round(A - 0.499999).
444
445ensure_integer(A) ->
446    integer_to_list(list_to_integer(A)).