PageRenderTime 101ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/aviary_fx.rb

https://github.com/aviaryapi/AviaryFxRuby
Ruby | 478 lines | 235 code | 99 blank | 144 comment | 8 complexity | a23e7bf3b976a3a50806571586cf54bd MD5 | raw file
  1. require 'digest/md5'
  2. require 'rest-client'
  3. require 'nokogiri'
  4. require 'json'
  5. # We open the Nokogiri::XML module to add a attributes2hash method for an XML node.
  6. module Nokogiri::XML
  7. class Node
  8. # Returns:
  9. # +Hash+ of the attribute key/values of the node
  10. def attributes2hash
  11. a = {}
  12. self.attributes().each do |k, v|
  13. a[k.to_sym] = v.value
  14. end
  15. return a
  16. end
  17. end
  18. end
  19. # This module contains classes that are representations of AviaryFX objects, such as
  20. # Filter, FilterParameter, Render, RenderParameter, RenderParameterCollection, etc.
  21. #
  22. # It also contains an API class to use as an API wrapper for the AviaryFX API.
  23. module AviaryFX
  24. # Holds information about an AviaryFX filter, such as uid, label, description, and
  25. # an array of FilterParameter objects.
  26. class FilterInfo
  27. attr_reader :uid, :label, :description
  28. # Array of FilterParameter objects
  29. attr_reader :parameters
  30. def initialize(options = {})
  31. @uid = options[:uid]
  32. @label = options[:label]
  33. @description = options[:description]
  34. @parameters = options[:parameters] || []
  35. end
  36. def self.new_from_xml(xml_node)
  37. return self.new(:label => xml_node.attributes()["label"].value,
  38. :uid => xml_node.attributes()["uid"].value,
  39. :description => xml_node.xpath("description").text,
  40. :parameters => xml_node.xpath("filtermetadata/parameter").collect{|p| FilterParameter.new_from_xml(p)})
  41. end
  42. end
  43. # Holds information about an AviaryFX FilterParameter, with properties such as
  44. # uid, id, type, min, max, and value, although all of these will not always
  45. # be present for a given filter parameter.
  46. class FilterParameter
  47. attr_reader :uid, :id, :type, :min, :max, :value
  48. # All attributes available from the XML response from Aviary, including ones that may not
  49. # specifically be mentioned at the time of this writing.
  50. attr_reader :raw_attributes
  51. def initialize(options = {})
  52. @uid = options[:uid]
  53. @id = options[:id]
  54. @type = options[:type]
  55. @min = options[:min]
  56. @max = options[:max]
  57. @value = options[:value]
  58. @raw_attributes = options
  59. end
  60. def self.new_from_xml(xml_node)
  61. self.new(xml_node.attributes2hash)
  62. end
  63. end
  64. # Holds information about an AviaryFX RenderOptionsGrid, including a URL for the grid
  65. # and an array of the renders in the grid.
  66. class RenderOptionsGrid
  67. # Array of Render objects
  68. attr_reader :renders
  69. # String containing the URL to the rendered grid
  70. attr_reader :url
  71. def initialize(options = {})
  72. @url = options[:url]
  73. @renders = options[:renders] || []
  74. end
  75. def self.new_from_xml(xml_node)
  76. self.new(:url => xml_node.xpath("//ostrichrenderresponse/url").text,
  77. :renders => xml_node.xpath("//ostrichrenderresponse/renders/render").collect {|r| Render.new_from_xml(r) })
  78. end
  79. end
  80. # Holds information about an AviaryFX Render, such as the row and column corresponding to its
  81. # position in its parent grid as well as a RenderParmeterCollection object.
  82. class Render
  83. # Row in the corresponding RenderOptionsGrid
  84. attr_reader :row
  85. # Column in the corresponding RenderOptionsGrid
  86. attr_reader :col
  87. # RenderParameterCollection objection containing the render parameters for this render
  88. attr_reader :render_parameter_collection
  89. def initialize(options = {})
  90. @row = options[:row]
  91. @col = options[:col]
  92. @render_parameter_collection = options[:render_parameter_collection] || RenderParameterCollection.new
  93. end
  94. def self.new_from_xml(xml_node)
  95. self.new(:row => xml_node.attributes()["row"].value,
  96. :col => xml_node.attributes()["col"].value,
  97. :render_parameter_collection => RenderParameterCollection.new_from_xml(xml_node.xpath("parameters")))
  98. end
  99. end
  100. # A container class for holding an array of RenderParameter objects. It is its own class
  101. # primarily to provide an organized way to convert a set of RenderParameter objects into an XML
  102. # representation and to convert JSON to a collection of RenderParameter objects.
  103. class RenderParameterCollection
  104. # Array of RenderParameter objects
  105. attr_reader :parameters
  106. def initialize(options)
  107. @parameters = options[:parameters] || []
  108. end
  109. def self.new_from_xml(xml_node)
  110. self.new(:parameters => xml_node.xpath("parameter").collect {|p| RenderParameter.new_from_xml(p)})
  111. end
  112. # Create a collection of RenderParameter objects from a basic JSON string.
  113. # Example: '{"parameters":[{"id":"Color Count","value":"14"},{"id":"Saturation","value":"1.0353452422505582"},{"id":"Curve Smoothing","value":"3.8117529663284664"},{"id":"Posterization","value":"9"},{"id":"Pattern Channel","value":"7"}]}'
  114. def self.new_from_json(json)
  115. p_hash = JSON.parse(json)
  116. self.new(:parameters => p_hash["parameters"].collect {|p| RenderParameter.new(:uid => p["uid"], :id => p["id"], :value => p["value"]) })
  117. end
  118. def to_xml
  119. builder = Nokogiri::XML::Builder.new do |xml|
  120. xml.parameters {
  121. self.parameters.each do |p|
  122. xml.parameter({"uid" => p.uid, "id" => p.id, "value" => p.value})
  123. end
  124. }
  125. end
  126. return Nokogiri::XML(builder.to_xml).root.to_s.gsub(/\n /, "").gsub(/\n/, "")
  127. end
  128. end
  129. # Holds information about an AviaryFX render parameter, such as id, uid, and value.
  130. class RenderParameter
  131. attr_reader :id, :uid, :value
  132. # all attributes provided in the XML response from the Aviary server, including ones
  133. # that may not be mentioned specifically at the time of this writing.
  134. attr_reader :raw_attributes
  135. def initialize(options = {})
  136. @id = options[:id]
  137. @uid = options[:uid]
  138. @value = options[:value]
  139. @raw_attributes = options
  140. end
  141. def self.new_from_xml(xml_node)
  142. self.new(xml_node.attributes2hash)
  143. end
  144. end
  145. # The purpose of the AviaryFX::API class is to serve as a wrapper
  146. # for API calls to the Aviary API
  147. # First, insantiate the class, passing in your api_key and api_secret.
  148. #
  149. # afx = AviaryFX::API.new("your_key", "your_secret")
  150. #
  151. # Next, call one of the public methods:
  152. # * get_time
  153. # * get_filters
  154. # * upload
  155. # * render_options
  156. # * render
  157. #
  158. #
  159. # Example:
  160. # afx.get_filters()
  161. class API
  162. VERSION = "0.2"
  163. PLATFORM = "html"
  164. HARDWARE_VERSION = "1.0"
  165. SOFTWARE_VERSION = "Ruby"
  166. APP_VERSION = "1.0"
  167. # The Base URL For API calls
  168. SERVER = "http://cartonapi.aviary.com/services"
  169. # The URL fragment for the get_time method
  170. GET_TIME_URL = "/util/getTime"
  171. # The URL fragment for the get_filters method
  172. GET_FILTERS_URL = "/filter/getFilters"
  173. # The URL fragment for the upload method
  174. UPLOAD_URL = "/ostrich/upload"
  175. # The URL fragment for the render method
  176. RENDER_URL = "/ostrich/render"
  177. # Initialize the API wrapper class
  178. #
  179. # Params:
  180. # +api_key+:: +String+ containing your API key
  181. # +api_secret+:: +String+ containing your API secret
  182. def initialize(api_key, api_secret)
  183. @api_key = api_key
  184. @api_secret = api_secret
  185. end
  186. # Uploads an image to the Aviary server.
  187. #
  188. # Params:
  189. # +filename+:: +String+ containing the full local path to the file to upload
  190. #
  191. # Returns:
  192. # +Hash+ containing the URL on the server of the uploaded file.
  193. def upload(filename)
  194. params_hash = standard_params
  195. params_hash[:api_sig] = get_api_signature(params_hash)
  196. f = File.new(filename, "rb")
  197. params_hash[:file] = f
  198. uri = SERVER + UPLOAD_URL
  199. xml_response = RestClient.post(uri, params_hash)
  200. xml_doc = load_xml_response(xml_response)
  201. return {:url => xml_doc.xpath("//file").first.attributes()["url"].value}
  202. end
  203. # Renders image based on render parameters.
  204. #
  205. # Params:
  206. # +backgroundcolor+:: +String+ containing the background color
  207. # +format+:: +String+
  208. # +quality+:: +String+
  209. # +scale+:: +String+
  210. # +filepath+:: +String+ containing the URL of the image to provide options for
  211. # +filterid+:: +String+ containing the ID of the filter to use
  212. # +width+:: +String+ containing the width of the image to return
  213. # +height+:: +String+ containing the height of the image to return
  214. # +renderparameters+:: +RenderParameterCollection+ object
  215. #
  216. # Returns:
  217. # +Hash+ containing the URL to the rendered image
  218. def render(backgroundcolor, format, quality, scale, filepath, filterid, width, height, render_parameter_collection)
  219. params_hash = {
  220. :calltype => "filteruse",
  221. :cols => "0",
  222. :rows => "0",
  223. :backgroundcolor => backgroundcolor,
  224. :cellwidth => width,
  225. :cellheight => height,
  226. :filterid => filterid,
  227. :filepath => filepath,
  228. :quality => quality,
  229. :scale => scale,
  230. :format => format,
  231. :renderparameters => render_parameter_collection.to_xml}
  232. uri = SERVER + RENDER_URL
  233. xml_doc = call_api(uri, params_hash)
  234. return {:url => xml_doc.xpath("//ostrichrenderresponse/url").text}
  235. end
  236. # Renders a filter options thumbnail grid and returns render parameters for each option.
  237. #
  238. # Params:
  239. # +backgroundcolor+:: +String+ containing the background color
  240. # +format+:: +String+
  241. # +quality+:: +String+
  242. # +scale+:: +String+
  243. # +filepath+:: +String+ containing the URL of the image to provide options for
  244. # +filterid+:: +String+ containing the ID of the filter to use
  245. # +cols+:: +String+ containing the number of columns to provide in the RenderOptionsGrid
  246. # +rows+:: +String+ containing the number of rows to provide in the RenderOptionsGrid
  247. # +cellwidth+:: +String+ containing the width of each cell returned in the grid.
  248. # +cellheight+:: +String+ containing the height of each cell returned in the grid.
  249. #
  250. # Returns:
  251. # +RenderOptionsGrid+ object
  252. def render_options(backgroundcolor, format, quality, scale, filepath, filterid, cols, rows, cellwidth, cellheight)
  253. params_hash = {
  254. :calltype => "previewRender",
  255. :backgroundcolor => backgroundcolor,
  256. :format => format,
  257. :quality => quality,
  258. :scale => scale,
  259. :filepath => filepath,
  260. :filterid => filterid,
  261. :cols => cols,
  262. :rows => rows,
  263. :cellwidth => cellwidth,
  264. :cellheight => cellheight }
  265. uri = SERVER + RENDER_URL;
  266. xml_doc = call_api(uri, params_hash)
  267. return RenderOptionsGrid.new_from_xml(xml_doc)
  268. end
  269. # Gets a list of available filters.
  270. #
  271. # Returns:
  272. # +Array+ of Filter objects.
  273. def get_filters
  274. params_hash = {}
  275. uri = SERVER + GET_FILTERS_URL
  276. xml_doc = call_api(uri, params_hash)
  277. filters = []
  278. xml_doc.xpath("//filter").each do |f|
  279. filter= FilterInfo.new_from_xml(f)
  280. filters.push filter
  281. end
  282. return filters
  283. end
  284. # Returns the current server time from the AviaryFX API server
  285. #
  286. # Returns:
  287. # +String+ containing the current time on the server
  288. def get_time
  289. params_hash = {}
  290. uri = SERVER + GET_TIME_URL
  291. xml_doc = call_api(uri, params_hash)
  292. servertimes = xml_doc.root.xpath("servertime")
  293. return servertimes.first.text.to_s
  294. end
  295. private
  296. #
  297. # Returns hash of the standard parameters that must be sent on every API call
  298. #
  299. # Returns:
  300. # +Hash+ of standard parameters that must be sent on every API call
  301. def standard_params
  302. params_hash = {}
  303. params_hash[:api_key] = @api_key
  304. params_hash[:ts] = Time.now.to_i
  305. params_hash[:version] = VERSION
  306. params_hash[:platform] = PLATFORM
  307. params_hash[:hardware_version] = HARDWARE_VERSION
  308. params_hash[:software_version] = SOFTWARE_VERSION
  309. params_hash[:app_version] = APP_VERSION
  310. return params_hash
  311. end
  312. # Calls the API at the given URL with the given parameters and returns the resultant XML document
  313. #
  314. # Params:
  315. # +uri+:: +String+ object containing URL of the API method to call
  316. # +params_hash+:: +Hash+ object containing the parameters to be sent to the API method
  317. #
  318. # Returns:
  319. # +Nokogiri::XML::Document+ Object
  320. def call_api(uri, params_hash)
  321. params_hash = standard_params.merge(params_hash)
  322. params_hash[:api_sig] = get_api_signature(params_hash)
  323. xml_response = request(uri, params_hash)
  324. return load_xml_response(xml_response)
  325. end
  326. # Converts an XML string into a Nokogiri XML Document
  327. # Also raises error if the XML cannot be loaded or the response contains errors from the API
  328. #
  329. # Params:
  330. # +xml_response+:: +String+ object containing XML
  331. #
  332. # Returns:
  333. # +Nokogiri::XML::Document+ object
  334. def load_xml_response(xml_response)
  335. begin
  336. xml_doc = Nokogiri::XML(xml_response)
  337. rescue
  338. raise StandardError, "Unable to load XML response from API"
  339. end
  340. response = xml_doc.xpath("response")
  341. if !response || response.empty? || response.first.attributes()["status"].value != "ok"
  342. raise StandardError, "Error loading response or error returned from API"
  343. end
  344. return xml_doc
  345. end
  346. #
  347. # Sends a standard POST to the given URL with the given POST parameters and returns the response
  348. #
  349. # Params:
  350. # +uri+:: +String+ containing the URL to which to POST
  351. # +post_hash+:: +Hash+ containing the hash of parameters to POST
  352. #
  353. # Returns:
  354. # +String+ containing the response from the server.
  355. def request(uri, post_hash)
  356. url = URI.parse(uri)
  357. req = Net::HTTP::Post.new(url.path)
  358. req.set_form_data(post_hash)
  359. sock = Net::HTTP.new(url.host, url.port)
  360. res = sock.start {|http| http.request(req)}
  361. return res.body
  362. end
  363. #
  364. # Returns the API signature for the params
  365. #
  366. # Params:
  367. # +params_hash+:: +Hash+ containing the base parameters to be sent to the server, not including the API signature
  368. #
  369. # Returns:
  370. # +String+ containing the API signature to be used with the given params hash.
  371. #
  372. def get_api_signature(params_hash)
  373. params_string = params_hash.to_a.sort { |a,b| a.first.to_s <=> b.first.to_s }.collect {|e| "#{e.first}#{e.last}" }.join("")
  374. string_to_md5 = @api_secret + params_string
  375. api_sig = Digest::MD5.hexdigest(string_to_md5)
  376. return api_sig
  377. end
  378. end
  379. end