PageRenderTime 53ms CodeModel.GetById 15ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/src/support/z_media_tag.erl

https://code.google.com/p/zotonic/
Erlang | 376 lines | 285 code | 37 blank | 54 comment | 15 complexity | dded4d6416bb8c16c9ae68c2d8441a53 MD5 | raw file
  1%% @author Marc Worrell <marc@worrell.nl>
  2%% @copyright 2009 Marc Worrell
  3%% Date: 2009-03-03
  4%% @doc Generate media urls and html for viewing media, based on the filename, size and optional filters.
  5%% Does not generate media previews itself, this is done when fetching the image.
  6%%
  7%% Typical urls are like: 
  8%% /image/2007/03/31/wedding.jpg(300x300)(crop-center)(a3ab6605e5c8ce801ac77eb76289ac12).jpg
  9%% /media/inline/2007/03/31/wedding.jpg
 10%% /media/attachment/2007/03/31/wedding.jpg
 11
 12%% Copyright 2009 Marc Worrell
 13%%
 14%% Licensed under the Apache License, Version 2.0 (the "License");
 15%% you may not use this file except in compliance with the License.
 16%% You may obtain a copy of the License at
 17%% 
 18%%     http://www.apache.org/licenses/LICENSE-2.0
 19%% 
 20%% Unless required by applicable law or agreed to in writing, software
 21%% distributed under the License is distributed on an "AS IS" BASIS,
 22%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 23%% See the License for the specific language governing permissions and
 24%% limitations under the License.
 25
 26-module(z_media_tag).
 27-author("Marc Worrell <marc@worrell.nl").
 28
 29%% interface functions
 30-export([
 31    viewer/3,
 32    tag/3,
 33    url/3,
 34    props2url/1,
 35    url2props/2
 36]).
 37
 38-include_lib("zotonic.hrl").
 39
 40
 41%% @spec viewer(MediaReference, Options, Context) -> {ok, HtmlFragMent} | {error, Reason}
 42%%   MediaReference = Filename | RscId | MediaPropList
 43%% @doc Generate a html fragment for displaying a medium.  This can generate audio or video player html.
 44viewer(undefined, _Options, _Context) ->
 45    {ok, []};
 46viewer([], _Options, _Context) ->
 47    {ok, []};
 48viewer(Name, Options, Context) when is_atom(Name) ->
 49    case m_rsc:name_to_id(Name, Context) of
 50        {ok, Id} -> viewer(Id, Options, Context);
 51        _ -> {ok, []}
 52    end;
 53viewer(Id, Options, Context) when is_integer(Id) ->
 54    case m_media:get(Id, Context) of
 55        MediaProps when is_list(MediaProps) -> viewer(MediaProps, Options, Context);
 56        undefined -> viewer1(Id, [], undefined, Options, Context)
 57    end;
 58viewer([{_Prop, _Value}|_] = Props, Options, Context) ->
 59    Id = proplists:get_value(id, Props),
 60    case z_convert:to_list(proplists:get_value(filename, Props)) of
 61        None when None == []; None == undefined ->
 62            viewer1(Id, Props, undefined, Options, Context);
 63        Filename ->
 64            FilePath = filename_to_filepath(Filename, Context),
 65            viewer1(Id, Props, FilePath, Options, Context)
 66    end;
 67viewer(Filename, Options, Context) when is_binary(Filename) ->
 68    viewer(binary_to_list(Filename), Options, Context);
 69viewer(Filename, Options, Context) ->
 70    FilePath = filename_to_filepath(Filename, Context),
 71    case z_media_identify:identify(FilePath, Context) of
 72        {ok, Props} ->
 73            viewer1(undefined, Props, FilePath, Options, Context);
 74        {error, _} -> 
 75            % Unknown content type, we just can't display it.
 76            {ok, []}
 77    end.
 78
 79    
 80    %% @doc Try to generate Html for the media reference.  First check if a module can do this, then 
 81    %% check the normal image tag.
 82    viewer1(Id, Props, FilePath, Options, Context) ->
 83        case z_notifier:first({media_viewer, Id, Props, FilePath, Options}, Context) of
 84            {ok, Html} -> {ok, Html};
 85            undefined -> tag(Props, Options, Context)
 86        end.
 87
 88
 89%% @spec tag(MediaReference, Options, Context) -> {ok, TagString} | {error, Reason}
 90%%   MediaReference = Filename | RscId | MediaPropList
 91%% @doc Generate a HTML image tag for the image with the filename and options. The medium _must_ be in
 92%% a format for which we can generate a preview.  Note that this will never generate video or audio.
 93tag(What, Options, Context) ->
 94    tag(What, Options, Context, []).
 95
 96tag(undefined, _Options, _Context, _Visited) ->
 97    {ok, []};
 98tag([], _Options, _Context, _Visited) ->
 99    {ok, []};
100tag(Name, Options, Context, Visited) when is_atom(Name) ->
101    case m_rsc:name_to_id(Name, Context) of
102        {ok, Id} -> tag(Id, Options, Context, Visited);
103        _ -> {ok, []}
104    end;
105tag(Id, Options, Context, Visited) when is_integer(Id) ->
106    case m_media:get(Id, Context) of
107        Props when is_list(Props) ->
108            case mediaprops_filename(Id, Props, Context) of
109                [] -> {ok, []};
110                Filename -> tag1(Props, Filename, Options, Context)
111            end;
112        undefined ->
113            NewId = case z_notifier:first({media_stillimage, Id, []}, Context) of
114                        {ok, N} -> N;
115                        _ ->
116                            %% Use the first depiction edge
117                            m_edge:object(Id, depiction, 1, Context)
118                    end,
119            case NewId of
120                undefined -> {ok, []};
121                _ -> case lists:member(NewId, Visited) of
122                         true -> {ok, []}; %% cycle detected
123                         false -> tag(NewId, Options, Context, [NewId|Visited]) %% recurse
124                     end
125            end
126    end;
127tag([{_Prop, _Value}|_] = Props, Options, Context, _Visited) ->
128    case mediaprops_filename(proplists:get_value(id, Props), Props, Context) of
129        [] -> {ok, []};
130        Filename -> tag1(Props, Filename, Options, Context)
131    end;
132tag(Filename, Options, Context, _Visited) when is_binary(Filename) ->
133    tag(binary_to_list(Filename), Options, Context);
134tag(Filename, Options, Context, _Visited) when is_list(Filename) ->
135    FilePath = filename_to_filepath(Filename, Context),
136    tag1(FilePath, Filename, Options, Context);
137tag({filepath, Filename, FilePath}, Options, Context, _Visited) ->
138    tag1(FilePath, Filename, Options, Context).
139    
140
141    tag1(_MediaRef, {filepath, Filename, FilePath}, Options, Context) ->
142        tag1(FilePath, Filename, Options, Context);
143    tag1(MediaRef, Filename, Options, Context) ->
144        {url, Url, TagOpts, ImageOpts} = url1(Filename, Options, Context),
145        % Calculate the real size of the image using the options
146        TagOpts1 = case z_media_preview:size(MediaRef, ImageOpts, Context) of
147                        {size, Width, Height, _Mime} ->
148                            [{width,Width},{height,Height}|TagOpts];
149                        _ ->
150                            TagOpts
151                    end,
152        % Make sure the required alt tag is present
153        TagOpts2 =  case proplists:get_value(alt, TagOpts1) of
154                        undefined -> [{alt,""}|TagOpts1];
155                        _ -> TagOpts1
156                    end,
157        % Filter some opts
158        case proplists:get_value(link, TagOpts) of
159            Empty when Empty == undefined; Empty == []; Empty == <<>> ->
160                {ok, z_tags:render_tag("img", [{src,Url}|TagOpts2])};
161            Link ->
162                HRef = iolist_to_binary(get_link(MediaRef, Link, Context)),
163                Tag = z_tags:render_tag("img", [{src,Url}|proplists:delete(link, TagOpts2)]),
164                {ok, z_tags:render_tag("a", [{href,HRef}], Tag)}
165        end.
166
167
168    % Given the media properties of an id, find the depicting file
169    mediaprops_filename(Id, undefined, Context) ->
170        case z_notifier:first({media_stillimage, Id, []}, Context) of
171            {ok, Filename} -> Filename;
172            undefined -> undefined
173        end;
174    mediaprops_filename(Id, Props, Context) ->
175        case z_notifier:first({media_stillimage, Id, Props}, Context) of
176            {ok, Filename} -> Filename;
177            _ -> case z_convert:to_list(proplists:get_value(preview_filename, Props)) of
178                     [] -> z_convert:to_list(proplists:get_value(filename, Props));
179                     Filename -> Filename
180                 end
181        end.
182
183
184    get_link(Media, true, Context) ->
185        Id = media_id(Media),
186        case m_rsc:p(Id, website, Context) of
187            Empty when Empty == undefined; Empty == <<>>; Empty == [] ->
188                m_rsc:p(Id, page_url, Context);
189            Website ->
190                Website
191        end;
192    get_link(_Media, Id, Context) when is_integer(Id) ->
193        m_rsc:p(Id, page_url, Context);
194    get_link(_Media, HRef, _Context) when is_binary(HRef); is_list(HRef) ->
195        HRef.
196
197    media_id([{_,_}|_] = List) ->
198        proplists:get_value(id, List).
199
200%% @doc Give the filepath for the filename being served.
201%% @todo Ensure the file is really in the given directory (ie. no ..'s)
202filename_to_filepath(Filename, #context{host=Host} = Context) ->
203    case Filename of
204        "/" ++ _ ->
205            Filename;
206        "lib/" ++ RelFilename -> 
207            case z_module_indexer:find(lib, RelFilename, Context) of
208                {ok, Libfile} -> Libfile;
209                _ -> Filename
210            end;
211        _ ->
212            filename:join([z_utils:lib_dir(priv), "sites", Host, "files", "archive", Filename])
213    end.
214
215
216%% @doc Give the base url for the filename being served
217%% @todo Use the dispatch rules to find the correct image path (when we want that...)
218filename_to_urlpath(Filename) ->
219    filename:join("/image/", Filename).
220
221
222%% @spec url(MediaRef, Options, Context) -> {ok, Url::binary()} | {error, Reason}
223%% @doc Generate the url for the image with the filename and options
224url(undefined, _Options, _Context) ->
225    {error, enoent};
226url(Id, Options, Context) when is_integer(Id) ->
227    case m_media:get(Id, Context) of
228        Props when is_list(Props) ->
229            url(Props, Options, Context);
230        undefined ->
231            case z_notifier:first({media_stillimage, Id, []}, Context) of
232                {ok, Filename} ->
233                    {url, Url, _TagOptions, _ImageOptions} = url1(Filename, Options, Context),
234                    {ok, Url};
235                _ -> {ok, <<>>}
236            end
237    end;
238url([{_Prop, _Value}|_] = Props, Options, Context) ->
239    case z_convert:to_list(proplists:get_value(filename, Props)) of
240        None when None == undefined; None == <<>>; None == [] -> 
241            case z_notifier:first({media_stillimage, proplists:get_value(id, Props), Props}, Context) of
242                {ok, Filename} ->
243                    {url, Url, _TagOptions, _ImageOptions} = url1(Filename, Options, Context),
244                    {ok, Url};
245                _ ->
246                    {ok, <<>>}
247            end;
248        Filename -> 
249            {url, Url, _TagOptions, _ImageOptions} = url1(Filename, Options, Context),
250            {ok, Url}
251    end;
252url(Filename, Options, Context) ->
253    {url, Url, _TagOptions, _ImageOptions} = url1(Filename, Options, Context),
254    {ok, Url}.
255    
256
257%% @spec url1(Filename, Options, Context) -> {url, Url::binary(), TagOptions, ImageOpts} | {error, Reason}
258%% @doc Creates an url for the given filename and filters.  This does not check the filename or if it is convertible.
259url1(File, Options, Context) ->
260    Filename = z_convert:to_list(File),
261    {TagOpts, ImageOpts} = lists:partition(fun is_tagopt/1, Options),
262    % Map all ImageOpts to an opt string
263    UrlProps = props2url(ImageOpts),
264	MimeFile = z_media_identify:guess_mime(Filename),
265	{_Mime,Extension} = z_media_preview:out_mime(MimeFile, ImageOpts),
266    Checksum = z_utils:checksum([Filename,UrlProps,Extension], Context),
267    PropCheck = mochiweb_util:quote_plus(lists:flatten([UrlProps,$(,Checksum,$)])),
268    {url, list_to_binary(filename_to_urlpath(lists:flatten([Filename,PropCheck,Extension]))), 
269          TagOpts,
270          ImageOpts}.
271
272
273is_tagopt({link,  _}) -> true;
274is_tagopt({alt,   _}) -> true;
275is_tagopt({title, _}) -> true;
276is_tagopt({class, _}) -> true;
277is_tagopt({style, _}) -> true;
278% Some preview args we definitely know exist (just an optimization)
279is_tagopt({width, _}) -> false;
280is_tagopt({height, _}) -> false;
281is_tagopt({crop, _}) -> false;
282is_tagopt({gray, _}) -> false;
283is_tagopt({mono, _}) -> false;
284is_tagopt({extent, _}) -> false;
285is_tagopt({upscale, _}) -> false;
286is_tagopt({blur, _}) -> false;
287is_tagopt({quality, _}) -> false;
288is_tagopt({background, _}) -> false;
289is_tagopt({lossless, _}) -> false;
290is_tagopt({removebg, _}) -> false;
291% And be sure to keep the data-xxxx args in the tag
292is_tagopt({Prop, _}) ->
293    case z_convert:to_list(Prop) of
294        "data_"++_ -> true;
295        _ -> false
296    end.
297
298
299props2url(Props) -> 
300    props2url(Props, undefined, undefined, []).
301
302props2url([], Width, Height, Acc) ->
303    Size =  case {Width,Height} of
304                {undefined,undefined} -> [];
305                {_W,undefined} -> [integer_to_list(Width)] ++ "x";
306                {undefined,_H} -> [$x|integer_to_list(Height)];
307                {_W,_H} -> integer_to_list(Width) ++ [$x|integer_to_list(Height)]
308            end,
309    lists:flatten([$(, z_utils:combine(")(", [Size|lists:reverse(Acc)]), $)]);
310
311props2url([{crop,None}|Rest], Width, Height, Acc) when None == false; None == undefined; None == <<>>; None == [] ->
312    props2url(Rest, Width, Height, Acc);
313props2url([{width,Width}|Rest], _Width, Height, Acc) ->
314    props2url(Rest, z_convert:to_integer(Width), Height, Acc);
315props2url([{height,Height}|Rest], Width, _Height, Acc) ->
316    props2url(Rest, Width, z_convert:to_integer(Height), Acc);
317props2url([{Prop}|Rest], Width, Height, Acc) ->
318    props2url(Rest, Width, Height, [atom_to_list(Prop)|Acc]);
319props2url([{_Prop,undefined}|Rest], Width, Height, Acc) ->
320    props2url(Rest, Width, Height, Acc);
321props2url([{Prop,true}|Rest], Width, Height, Acc) ->
322    props2url(Rest, Width, Height, [atom_to_list(Prop)|Acc]);
323props2url([{Prop,Value}|Rest], Width, Height, Acc) ->
324    props2url(Rest, Width, Height, [[atom_to_list(Prop),$-,z_convert:to_list(Value)]|Acc]).
325
326
327%% @spec url2props(Url, Context) -> {Filepath,PreviewPropList,Checksum,ChecksumBaseString} | error
328%% @doc Translate an url of the format "image.jpg(300x300)(crop-center)(checksum).jpg" to parts
329%% @todo Map the extension to the format of the preview (.jpg or .png)
330url2props(Url, Context) ->
331    {Filepath,Rest} = lists:splitwith(fun(C) -> C =/= $( end, Url),
332    PropsRoot = filename:rootname(Rest),
333    % Take the checksum from the string
334    case string:rchr(PropsRoot, $() of
335        0 ->
336            error;
337        LastParen ->
338            {Props,[$(|Check]} = lists:split(LastParen-1, PropsRoot),
339            Check1 = string:strip(Check, right, $)),
340            PropList = case Props of
341                           "()" ++ _ -> [""|string:tokens(Props, ")(")];
342                           _ -> string:tokens(Props, ")(")
343                       end,
344            FileMime = z_media_identify:guess_mime(Rest),
345            {_Mime, Extension} = z_media_preview:out_mime(FileMime, PropList),
346            z_utils:checksum_assert([Filepath,Props,Extension], Check1, Context),
347            PropList1       = case PropList of
348                                [] -> [];
349                                [Size|RestProps]->
350                                    {W,XH} = lists:splitwith(fun(C) -> C >= $0 andalso C =< $9 end, Size),
351                                    SizeProps = case {W,XH} of
352                                                    {"", "x"}            -> [];
353                                                    {"", ""}             -> [];
354                                                    {Width, ""}          -> [{width,list_to_integer(Width)}]; 
355                                                    {Width, "x"}         -> [{width,list_to_integer(Width)}]; 
356                                                    {"", [$x|Height]}    -> [{height,list_to_integer(Height)}]; 
357                                                    {Width, [$x|Height]} -> [{width,list_to_integer(Width)},{height,list_to_integer(Height)}]
358                                                end,
359                                    SizeProps ++ url2props1(RestProps, [])
360                              end,
361            {Filepath,PropList1,Check1,Props}
362    end.
363
364url2props1([], Acc) ->
365    lists:reverse(Acc);
366url2props1([P|Rest], Acc) ->
367    {Prop,Arg} = lists:splitwith(fun(C) -> C =/= $- end, P),
368    Arg1 =  case Arg of
369                [$-|A] -> A;
370                _ -> Arg
371            end,
372    Filter = z_media_preview:string2filter(Prop, Arg1),
373    url2props1(Rest, [Filter|Acc]).
374
375
376