@@ -133,24 +133,30 @@ def recurse(bs):
133133 if_value = process_conditional (options .pop ("if" , None ))
134134
135135 params = {
136- "a" : angle ,
137- "ar" : aspect_ratio ,
136+ "a" : normalize_expression ( angle ) ,
137+ "ar" : normalize_expression ( aspect_ratio ) ,
138138 "b" : background ,
139139 "bo" : border ,
140140 "c" : crop ,
141141 "co" : color ,
142- "dpr" : dpr ,
143- "du" : duration ,
144- "e" : effect ,
145- "eo" : end_offset ,
142+ "dpr" : normalize_expression ( dpr ) ,
143+ "du" : normalize_expression ( duration ) ,
144+ "e" : normalize_expression ( effect ) ,
145+ "eo" : normalize_expression ( end_offset ) ,
146146 "fl" : flags ,
147- "h" : height ,
147+ "h" : normalize_expression ( height ) ,
148148 "l" : overlay ,
149- "so" : start_offset ,
149+ "o" : normalize_expression (options .pop ('opacity' ,None )),
150+ "q" : normalize_expression (options .pop ('quality' ,None )),
151+ "r" : normalize_expression (options .pop ('radius' ,None )),
152+ "so" : normalize_expression (start_offset ),
150153 "t" : named_transformation ,
151154 "u" : underlay ,
155+ "w" : normalize_expression (width ),
156+ "x" : normalize_expression (options .pop ('x' ,None )),
157+ "y" : normalize_expression (options .pop ('y' ,None )),
152158 "vc" : video_codec ,
153- "w " : width
159+ "z " : normalize_expression ( options . pop ( 'zoom' , None ))
154160 }
155161 simple_params = {
156162 "ac" : "audio_codec" ,
@@ -163,22 +169,34 @@ def recurse(bs):
163169 "f" : "fetch_format" ,
164170 "g" : "gravity" ,
165171 "ki" : "keyframe_interval" ,
166- "o" : "opacity" ,
167172 "p" : "prefix" ,
168173 "pg" : "page" ,
169- "q" : "quality" ,
170- "r" : "radius" ,
171174 "sp" : "streaming_profile" ,
172175 "vs" : "video_sampling" ,
173- "x" : "x" ,
174- "y" : "y" ,
175- "z" : "zoom"
176176 }
177177
178178 for param , option in simple_params .items ():
179179 params [param ] = options .pop (option , None )
180180
181+ variables = options .pop ('variables' ,{})
182+ var_params = []
183+ for key ,value in options .items ():
184+ if re .match (r'^\$' , key ):
185+ var_params .append (u"{0}_{1}" .format (key , normalize_expression (str (value ))))
186+
187+ var_params .sort ()
188+
189+ if variables :
190+ for var in variables :
191+ var_params .append (u"{0}_{1}" .format (var [0 ], normalize_expression (str (var [1 ]))))
192+
193+
194+ variables = ',' .join (var_params )
195+
181196 sorted_params = sorted ([param + "_" + str (value ) for param , value in params .items () if (value or value == 0 )])
197+ if variables :
198+ sorted_params .insert (0 , str (variables ))
199+
182200 if if_value is not None :
183201 sorted_params .insert (0 , "if_" + str (if_value ))
184202 transformation = "," .join (sorted_params )
@@ -441,10 +459,10 @@ def cloudinary_api_url(action='upload', **options):
441459
442460
443461# Based on ruby's CGI::unescape. In addition does not escape / :
444- def smart_escape (source ):
462+ def smart_escape (source , unsafe = r"([^a-zA-Z0-9_.\-\/:]+)" ):
445463 def pack (m ):
446464 return to_bytes ('%' + "%" .join (["%02X" % x for x in struct .unpack ('B' * len (m .group (1 )), m .group (1 ))]).upper ())
447- return to_string (re .sub (to_bytes (r"([^a-zA-Z0-9_.\-\/:]+)" ), pack , to_bytes (source )))
465+ return to_string (re .sub (to_bytes (unsafe ), pack , to_bytes (source )))
448466
449467
450468def random_public_id ():
@@ -666,7 +684,7 @@ def process_layer(layer, layer_parameter):
666684 if resource_type == "text" or resource_type == "subtitles" :
667685 if public_id is None and text is None :
668686 raise ValueError ("Must supply either text or public_id in " + layer_parameter )
669-
687+
670688 text_options = __process_text_options (layer , layer_parameter )
671689
672690 if text_options is not None :
@@ -677,9 +695,20 @@ def process_layer(layer, layer_parameter):
677695 components .append (public_id )
678696
679697 if text is not None :
680- text = smart_escape (text )
681- text = text .replace ("%2C" , "%252C" )
682- text = text .replace ("/" , "%252F" )
698+ var_pattern = r'(\$\([a-zA-Z]\w+\))'
699+ match = re .findall (var_pattern ,text )
700+
701+ parts = filter (lambda p : p is not None , re .split (var_pattern ,text ))
702+ encoded_text = []
703+ for part in parts :
704+ if re .match (var_pattern ,part ):
705+ encoded_text .append (part )
706+ else :
707+ encoded_text .append (smart_escape (smart_escape (part , r"([,/])" )))
708+
709+ text = '' .join (encoded_text )
710+ # text = text.replace("%2C", "%252C")
711+ # text = text.replace("/", "%252F")
683712 components .append (text )
684713 else :
685714 public_id = public_id .replace ("/" , ':' )
@@ -695,32 +724,55 @@ def process_layer(layer, layer_parameter):
695724 "<=" : 'lte' ,
696725 ">=" : 'gte' ,
697726 "&&" : 'and' ,
698- "||" : 'or'
727+ "||" : 'or' ,
728+ "*" : 'mul' ,
729+ "/" : 'div' ,
730+ "+" : 'add' ,
731+ "-" : 'min'
699732}
700- IF_PARAMETERS = {
701- "width" : 'w' ,
702- "height " : 'h' ,
703- "page_count " : "pc " ,
733+
734+ PREDEFINED_VARS = {
735+ "aspect_ratio " : "ar" ,
736+ "current_page " : "cp " ,
704737 "face_count" : "fc" ,
705- "aspect_ratio" : "ar"
738+ "height" : "h" ,
739+ "initial_aspect_ratio" : "iar" ,
740+ "initial_height" : "ih" ,
741+ "initial_width" : "iw" ,
742+ "page_count" : "pc" ,
743+ "page_x" : "px" ,
744+ "page_y" : "py" ,
745+ "tags" : "tags" ,
746+ "width" : "w"
706747}
707748
708- replaceRE = "(" + "|" .join (IF_PARAMETERS .keys ()) + "|[=<>&|!]+)"
749+ replaceRE = "(" + "|" .join (PREDEFINED_VARS .keys ())+ "|[=<>&|!]+)"
750+ replaceIF = "(" + '|' .join (map ( lambda key : re .escape (key ),IF_OPERATORS .keys ()))+ ")"
709751
710752
711753def translate_if (match ):
712754 name = match .group (0 )
713755 return IF_OPERATORS .get (name ,
714- IF_PARAMETERS .get (name ,
756+ PREDEFINED_VARS .get (name ,
715757 name ))
716758
717-
718759def process_conditional (conditional ):
719760 if conditional is None :
720761 return conditional
721- result = re . sub ( '[ _]+' , '_' , conditional )
722- return re . sub ( replaceRE , translate_if , result )
762+ result = normalize_expression ( conditional )
763+ return result
723764
765+ def normalize_expression (expression ):
766+ if re .match (r'^!.+!$' ,str (expression )): # quoted string
767+ return expression
768+ elif expression :
769+ result = str (expression )
770+ result = re .sub (replaceRE , translate_if , result )
771+ result = re .sub (replaceIF , translate_if , result )
772+ result = re .sub ('[ _]+' , '_' , result )
773+ return result
774+ else :
775+ return expression
724776
725777def __join_pair (key , value ):
726778 if value is None or value == "" :
0 commit comments